diff --git a/modules/twinklearv.js b/modules/twinklearv.js index 81019b349..f08426c2f 100644 --- a/modules/twinklearv.js +++ b/modules/twinklearv.js @@ -379,9 +379,8 @@ Twinkle.arv.callback.changeCategory = function (e) { const $input = $('', { type: 'checkbox', name: 's_' + field, - value: rev.revid + value: JSON.stringify(rev) }); - $input.data('revinfo', rev); $input.appendTo($entry); let comment = ''; // revdel/os @@ -472,8 +471,6 @@ Twinkle.arv.callback.evaluate = function(e) { let reason = ''; const input = Morebits.QuickForm.getInputData(form); - const uid = form.uid.value; - switch (input.category) { // Report user for vandalism @@ -591,7 +588,7 @@ Twinkle.arv.callback.evaluate = function(e) { Morebits.wiki.actionCompleted.redirect = reportpage; Morebits.wiki.actionCompleted.notice = 'Reporting complete'; - var spiPage = new Morebits.wiki.page(reportpage, 'Retrieving discussion page'); + var spiPage = new Morebits.wiki.Page(reportpage, 'Retrieving discussion page'); spiPage.setFollowRedirect(true); spiPage.setEditSummary('Adding new report for [[Special:Contributions/' + reportData.sockmaster + '|' + reportData.sockmaster + ']].'); spiPage.setChangeTags(Twinkle.changeTags); @@ -603,129 +600,47 @@ Twinkle.arv.callback.evaluate = function(e) { break; case 'an3': - var diffs = $.map($('input:checkbox[name=s_diffs]:checked', form), (o) => $(o).data('revinfo')); + // prepare the AN3 report, then post and notify + Twinkle.arv.callback.getAn3ReportData(input).then((data) => { + // If there are any reasons why the user might want to cancel the report, check with them about each reason and cancel if they choose to + for (const confirmation of data.confirmations) { + if (!confirm(confirmation)) { + return; + } + } - if (diffs.length < 3 && !confirm('You have selected fewer than three offending edits. Do you wish to make the report anyway?')) { - return; - } + Morebits.SimpleWindow.setButtonsEnabled(false); + Morebits.Status.init(form); - var warnings = $.map($('input:checkbox[name=s_warnings]:checked', form), (o) => $(o).data('revinfo')); + Morebits.wiki.addCheckpoint(); // prevent notification events from causing an erronous "action completed" - if (!warnings.length && !confirm('You have not selected any edits where you warned the offender. Do you wish to make the report anyway?')) { - return; - } + const reportpage = 'Wikipedia:Administrators\' noticeboard/Edit warring'; - var resolves = $.map($('input:checkbox[name=s_resolves]:checked', form), (o) => $(o).data('revinfo')); - var free_resolves = $('input[name=s_resolves_free]').val(); + Morebits.wiki.actionCompleted.redirect = reportpage; + Morebits.wiki.actionCompleted.notice = 'Reporting complete'; - var an3_next = function(free_resolves) { - if (!resolves.length && !free_resolves && !confirm('You have not selected any edits where you tried to resolve the issue. Do you wish to make the report anyway?')) { - return; - } + const an3Page = new Morebits.wiki.Page(reportpage, 'Retrieving discussion page'); + an3Page.setFollowRedirect(true); + an3Page.setEditSummary('Adding new report for [[Special:Contributions/' + data.uid + '|' + data.uid + ']].'); + an3Page.setChangeTags(Twinkle.changeTags); + an3Page.setAppendText(data.reportWikitext); + an3Page.append(); - const an3Parameters = { - uid: uid, - page: form.page.value.trim(), - comment: form.comment.value.trim(), - diffs: diffs, - warnings: warnings, - resolves: resolves, - free_resolves: free_resolves - }; + // notify user - Morebits.SimpleWindow.setButtonsEnabled(false); - Morebits.Status.init(form); - Twinkle.arv.processAN3(an3Parameters); - }; - - if (free_resolves) { - let query; - let diff, oldid; - const specialDiff = /Special:Diff\/(\d+)(?:\/(\S+))?/i.exec(free_resolves); - if (specialDiff) { - if (specialDiff[2]) { - oldid = specialDiff[1]; - diff = specialDiff[2]; - } else { - diff = specialDiff[1]; - } - } else { - diff = mw.util.getParamValue('diff', free_resolves); - oldid = mw.util.getParamValue('oldid', free_resolves); - } - const title = mw.util.getParamValue('title', free_resolves); - const diffNum = /^\d+$/.test(diff); // used repeatedly - - // rvdiffto in prop=revisions is deprecated, but action=compare doesn't return - // timestamps ([[phab:T247686]]) so we can't rely on it unless necessary. - // Likewise, we can't rely on a meaningful comment for diff=cur. - // Additionally, links like Special:Diff/123/next, Special:Diff/123/456, or ?diff=next&oldid=123 - // would each require making use of rvdir=newer in the revisions API. - // That requires a title parameter, so we have to use compare instead of revisions. - if (oldid && (diff === 'cur' || (!title && (diff === 'next' || diffNum)))) { - query = { - action: 'compare', - fromrev: oldid, - prop: 'ids|title', - format: 'json' - }; - if (diffNum) { - query.torev = diff; - } else { - query.torelative = diff; - } - } else { - query = { - action: 'query', - prop: 'revisions', - rvprop: 'ids|timestamp|comment', - format: 'json', - indexpageids: true - }; - - if (diff && oldid) { - if (diff === 'prev') { - query.revids = oldid; - } else { - query.titles = title; - query.rvdir = 'newer'; - query.rvstartid = oldid; - - if (diff === 'next' && title) { - query.rvlimit = 2; - } else if (diffNum) { - // Diffs may or may not be consecutive, no limit - query.rvendid = diff; - } - } - } else { - // diff=next|prev|cur with no oldid - // Implies title= exists otherwise it's not a valid diff link (well, it is, but to the Main Page) - if (diff && /^\D+$/.test(diff)) { - query.titles = title; - } else { - query.revids = diff || oldid; - } - } - } + const notifyText = '\n\n{{subst:an3-notice|1=' + mw.util.wikiUrlencode(data.uid) + '|auto=1}} ~~~~'; - new mw.Api().get(query).done((data) => { - let page; - if (data.compare && data.compare.fromtitle === data.compare.totitle) { - page = data; - } else if (data.query) { - const pageid = data.query.pageids[0]; - page = data.query.pages[pageid]; - } else { - return; - } - an3_next(page); - }).fail((data) => { - console.log('API failed :(', data); // eslint-disable-line no-console - }); - } else { - an3_next(); - } + const talkPage = new Morebits.wiki.Page('User talk:' + data.uid, 'Notifying edit warrior'); + talkPage.setFollowRedirect(true); + talkPage.setEditSummary('Notifying about edit warring noticeboard discussion.'); + talkPage.setChangeTags(Twinkle.changeTags); + talkPage.setAppendText(notifyText); + talkPage.append(); + Morebits.wiki.removeCheckpoint(); // all page updates have been started + }).catch((error) => { + console.error('Error occurred while preparing AN3 report.', error); // eslint-disable-line no-console + alert('Error occurred while preparing AN3 report: ' + error.message); + }); break; } }; @@ -854,49 +769,171 @@ Twinkle.arv.callback.getSpiReportData = function(input) { }; }; -Twinkle.arv.processAN3 = function(params) { - // prepare the AN3 report - let minid; - for (let i = 0; i < params.diffs.length; ++i) { - if (params.diffs[i].parentid && (!minid || params.diffs[i].parentid < minid)) { - minid = params.diffs[i].parentid; - } +Twinkle.arv.callback.getAn3ReportData = function(input) { + let data; + const confirmations = []; + + const diffs = input.s_diffs ? input.s_diffs.map(JSON.parse) : []; + + if (diffs.length < 3) { + confirmations.push('You have selected fewer than three offending edits. Do you wish to make the report anyway?'); } - new mw.Api().get({ - action: 'query', - prop: 'revisions', - format: 'json', - rvprop: 'sha1|ids|timestamp|comment', - rvlimit: 100, // intentionally limited - rvstartid: minid, - rvexcludeuser: params.uid, - indexpageids: true, - titles: params.page - }).done((data) => { - Morebits.wiki.addCheckpoint(); // prevent notification events from causing an erronous "action completed" + const warnings = input.s_warnings ? input.s_warnings.map(JSON.parse) : []; + + if (!warnings.length) { + confirmations.push('You have not selected any edits where you warned the offender. Do you wish to make the report anyway?'); + } + + const resolves = input.s_resolves ? input.s_resolves.map(JSON.parse) : []; + const free_resolves = input.s_resolves_free; + + return new Promise((resolve, reject) => { + if (free_resolves) { + let query; + let diff, oldid; + const specialDiff = /Special:Diff\/(\d+)(?:\/(\S+))?/i.exec(free_resolves); + if (specialDiff) { + if (specialDiff[2]) { + oldid = specialDiff[1]; + diff = specialDiff[2]; + } else { + diff = specialDiff[1]; + } + } else { + diff = mw.util.getParamValue('diff', free_resolves); + oldid = mw.util.getParamValue('oldid', free_resolves); + } + const title = mw.util.getParamValue('title', free_resolves); + const diffNum = /^\d+$/.test(diff); // used repeatedly + + // rvdiffto in prop=revisions is deprecated, but action=compare doesn't return + // timestamps ([[phab:T247686]]) so we can't rely on it unless necessary. + // Likewise, we can't rely on a meaningful comment for diff=cur. + // Additionally, links like Special:Diff/123/next, Special:Diff/123/456, or ?diff=next&oldid=123 + // would each require making use of rvdir=newer in the revisions API. + // That requires a title parameter, so we have to use compare instead of revisions. + if (oldid && (diff === 'cur' || (!title && (diff === 'next' || diffNum)))) { + query = { + action: 'compare', + fromrev: oldid, + prop: 'ids|title', + format: 'json' + }; + if (diffNum) { + query.torev = diff; + } else { + query.torelative = diff; + } + } else { + query = { + action: 'query', + prop: 'revisions', + rvprop: 'ids|timestamp|comment', + format: 'json', + indexpageids: true + }; + + if (diff && oldid) { + if (diff === 'prev') { + query.revids = oldid; + } else { + query.titles = title; + query.rvdir = 'newer'; + query.rvstartid = oldid; + + if (diff === 'next' && title) { + query.rvlimit = 2; + } else if (diffNum) { + // Diffs may or may not be consecutive, no limit + query.rvendid = diff; + } + } + } else { + // diff=next|prev|cur with no oldid + // Implies title= exists otherwise it's not a valid diff link (well, it is, but to the Main Page) + if (diff && /^\D+$/.test(diff)) { + query.titles = title; + } else { + query.revids = diff || oldid; + } + } + } + new mw.Api().get(query).then((queryResponse) => { + let page; + if (queryResponse.compare && queryResponse.compare.fromtitle === queryResponse.compare.totitle) { + page = queryResponse; + } else if (queryResponse.query) { + const pageIds = queryResponse.query.pageids; + if (!Array.isArray(pageIds) || pageIds.length !== 1) reject({ message: 'Error parsing diff.', data: queryResponse }); + page = queryResponse.query.pages[pageIds[0]]; + } else { + reject({ message: 'Could not find any diff associated with the URL provided.', data: queryResponse }); + } + resolve(page); + }).catch((queryResponse) => { + reject({ message: 'Call to MediaWiki API failed.', data: queryResponse }); + }); + } else { + resolve(); + } + }).then((free_resolves_data) => { + if (!resolves.length && !free_resolves_data) { + confirmations.push('You have not selected any edits where you tried to resolve the issue. Do you wish to make the report anyway?'); + } + + data = { + uid: input.uid, + page: input.page, + comment: input.comment, + diffs: diffs, + warnings: warnings, + resolves: resolves, + free_resolves: free_resolves_data, + confirmations: confirmations + }; + + let minid; + for (let i = 0; i < data.diffs.length; ++i) { + if (data.diffs[i].parentid && (!minid || data.diffs[i].parentid < minid)) { + minid = data.diffs[i].parentid; + } + } + + return new mw.Api().get({ + action: 'query', + prop: 'revisions', + format: 'json', + rvprop: 'sha1|ids|timestamp|comment', + rvlimit: 100, // intentionally limited + rvstartid: minid, + rvexcludeuser: data.uid, + indexpageids: true, + titles: data.page + }).catch((queryResponse) => Promise.reject({ message: 'Call to MediaWiki API failed.', data: queryResponse })); + }).then((queryResponse) => { // In case an edit summary was revdel'd const hasHiddenComment = function(rev) { if (!rev.comment && typeof rev.commenthidden === 'string') { return '(comment hidden)'; } - return '"' + rev.comment + '"'; - + // swap curly braces for HTML entities to avoid templates being rendered if they were included in an edit summary + return '"' + rev.comment.replace(/\{/g, '{').replace(/\}/g, '}') + '"'; }; let orig; - if (data.length) { - const sha1 = data[0].sha1; - for (let i = 1; i < data.length; ++i) { - if (data[i].sha1 === sha1) { - orig = data[i]; + if (queryResponse.length) { + const sha1 = queryResponse[0].sha1; + for (let i = 1; i < queryResponse.length; ++i) { + if (queryResponse[i].sha1 === sha1) { + orig = queryResponse[i]; break; } } if (!orig) { - orig = data[0]; + orig = queryResponse[0]; } } @@ -908,8 +945,8 @@ Twinkle.arv.processAN3 = function(params) { const grouped_diffs = {}; let parentid, lastid; - for (let j = 0; j < params.diffs.length; ++j) { - const cur = params.diffs[j]; + for (let j = 0; j < data.diffs.length; ++j) { + const cur = data.diffs[j]; if ((cur.revid && cur.revid !== parentid) || lastid === null) { lastid = cur.revid; grouped_diffs[lastid] = []; @@ -927,21 +964,23 @@ Twinkle.arv.processAN3 = function(params) { ret = '# {{diff|oldid=' + first.parentid + '|diff=' + last.revid + '|label=' + label + '}}\n'; } ret += sub.reverse().map((v) => (sub.length >= 2 ? '#' : '') + '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n'); + return ret; }).reverse().join('\n'); - const warningtext = params.warnings.reverse().map((v) => '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n'); - let resolvetext = params.resolves.reverse().map((v) => '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n'); + const warningtext = data.warnings.reverse().map((v) => '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n'); - if (params.free_resolves) { - const page = params.free_resolves; + let resolvetext = data.resolves.reverse().map((v) => '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n'); + + if (data.free_resolves) { + const page = data.free_resolves; if (page.compare) { - resolvetext += '\n# {{diff|oldid=' + page.compare.fromrevid + '|diff=' + page.compare.torevid + '|label=Consecutive edits on ' + page.compare.totitle + '}}'; + resolvetext += '\n# {{diff|oldid=' + page.compare.fromrevid + '|diff=' + page.compare.torevid + '|label=Consecutive edits on ' + page.compare.totitle + '}}'; } else if (page.revisions) { const revCount = page.revisions.length; let rev; if (revCount < 3) { // diff=prev or next rev = revCount === 1 ? page.revisions[0] : page.revisions[1]; - resolvetext += '\n# {{diff2|' + rev.revid + '|' + new Morebits.Date(rev.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) on ' + page.title + '}} ' + hasHiddenComment(rev); + resolvetext += '\n# {{diff2|' + rev.revid + '|' + new Morebits.Date(rev.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) on ' + page.title + '}} ' + hasHiddenComment(rev); } else { // diff and oldid are nonconsecutive rev = page.revisions[0]; const revLatest = page.revisions[revCount - 1]; @@ -951,39 +990,25 @@ Twinkle.arv.processAN3 = function(params) { } } - let comment = params.comment.replace(/~*$/g, '').trim(); + let comment = data.comment.replace(/~*$/g, '').trim(); if (comment) { comment += ' ~~~~'; } - const text = '\n\n{{subst:AN3 report|diffs=' + difftext + '|warnings=' + warningtext + '|resolves=' + resolvetext + '|pagename=' + params.page + '|orig=' + origtext + '|comment=' + comment + '|uid=' + params.uid + '}}'; - - const reportpage = 'Wikipedia:Administrators\' noticeboard/Edit warring'; + const reportWikitext = '\n\n{{subst:AN3 report|diffs=' + difftext + '|warnings=' + warningtext + '|resolves=' + resolvetext + '|pagename=' + data.page + '|orig=' + origtext + '|comment=' + comment + '|uid=' + data.uid + '}}'; - Morebits.wiki.actionCompleted.redirect = reportpage; - Morebits.wiki.actionCompleted.notice = 'Reporting complete'; - - const an3Page = new Morebits.wiki.Page(reportpage, 'Retrieving discussion page'); - an3Page.setFollowRedirect(true); - an3Page.setEditSummary('Adding new report for [[Special:Contributions/' + params.uid + '|' + params.uid + ']].'); - an3Page.setChangeTags(Twinkle.changeTags); - an3Page.setAppendText(text); - an3Page.append(); - - // notify user - - const notifyText = '\n\n{{subst:an3-notice|1=' + mw.util.wikiUrlencode(params.uid) + '|auto=1}} ~~~~'; + return { + uid: data.uid, + reportWikitext: reportWikitext, + confirmations: data.confirmations + }; + }).catch((errorData) => { + if (typeof errorData !== 'object' || !Object.prototype.hasOwnProperty.call(errorData, 'message')) { + return Promise.reject({ message: 'An unknown error occurred while generating the report.', data: errorData}); + } - const talkPage = new Morebits.wiki.Page('User talk:' + params.uid, 'Notifying edit warrior'); - talkPage.setFollowRedirect(true); - talkPage.setEditSummary('Notifying about edit warring noticeboard discussion.'); - talkPage.setChangeTags(Twinkle.changeTags); - talkPage.setAppendText(notifyText); - talkPage.append(); - Morebits.wiki.removeCheckpoint(); // all page updates have been started - }).fail((data) => { - console.log('API failed :(', data); // eslint-disable-line no-console + return Promise.reject(errorData); }); };