User:Andy M. Wang/pageswap.js
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <syntaxhighlight lang="javascript">
// [[WP:PMRC#4]] round-robin history swap
// by [[User:Andy M. Wang]]
// 1.6.1.2018.0920
$(document).ready(function() {
mw.loader.using( [
'mediawiki.api',
'mediawiki.util',
] ).then( function() {
"use strict";
/**
* If user is able to perform swaps
*/
function checkUserPermissions() {
var ret = {};
ret.canSwap = true;
var reslt = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
alert("Swapping pages unavailable."); return ret; },
data: { action:'query', format:'json', meta:'userinfo', uiprop:'rights' }
}).responseText).query.userinfo;
// check userrights for suppressredirect and move-subpages
var rightslist = reslt.rights;
ret.canSwap =
$.inArray('suppressredirect', rightslist) > -1
&& $.inArray('move-subpages', rightslist) > -1;
ret.allowSwapTemplates =
$.inArray('templateeditor', rightslist) > -1;
return ret;
}
/**
* Given namespace data, title, title namespace, returns expected title of page
* Along with title without prefix
* Precondition, title, titleNs is a subject page!
*/
function getTalkPageName(nsData, title, titleNs) {
var ret = {};
var prefixLength = nsData['' + titleNs]['*'].length === 0
? 0 : nsData['' + titleNs]['*'].length + 1;
ret.titleWithoutPrefix = title.substring(prefixLength, title.length);
ret.talkTitle = nsData['' + (titleNs + 1)]['*'] + ':'
+ ret.titleWithoutPrefix;
return ret;
}
/**
* Given two (normalized) titles, find their namespaces, if they are redirects,
* if have a talk page, whether the current user can move the pages, suggests
* whether movesubpages should be allowed, whether talk pages need to be checked
*/
function swapValidate(titleOne, titleTwo, pagesData, nsData, uPerms) {
var ret = {};
ret.valid = true;
if (titleOne === null || titleTwo === null || pagesData === null) {
ret.valid = false;
ret.invalidReason = "Unable to validate swap.";
return ret;
}
ret.allowMoveSubpages = true;
ret.checkTalk = true;
var count = 0;
for (var k in pagesData) {
++count;
if (k == "-1" || pagesData[k].ns < 0) {
ret.valid = false;
ret.invalidReason = ("Page " + pagesData[k].title + " does not exist.");
return ret;
}
// enable only in ns 0..5,12,13,118,119 (Main,Talk,U,UT,WP,WT,H,HT,D,DT)
if ((pagesData[k].ns >= 6 && pagesData[k].ns <= 9)
|| (pagesData[k].ns >= 10 && pagesData[k].ns <= 11 && !uPerms.allowSwapTemplates)
|| (pagesData[k].ns >= 14 && pagesData[k].ns <= 117)
|| (pagesData[k].ns >= 120)) {
ret.valid = false;
ret.invalidReason = ("Namespace of " + pagesData[k].title + " ("
+ pagesData[k].ns + ") not supported.\n\nLikely reasons:\n"
+ "- Names of pages in this namespace relies on other pages\n"
+ "- Namespace features heavily-transcluded pages\n"
+ "- Namespace involves subpages: swaps produce many redlinks\n"
+ "\n\nIf the move is legitimate, consider a careful manual swap.");
return ret;
}
if (titleOne == pagesData[k].title) {
ret.currTitle = pagesData[k].title;
ret.currNs = pagesData[k].ns;
ret.currTalkId = pagesData[k].talkid; // could be undefined
ret.currCanMove = pagesData[k].actions.move === '';
ret.currIsRedir = pagesData[k].redirect === '';
}
if (titleTwo == pagesData[k].title) {
ret.destTitle = pagesData[k].title;
ret.destNs = pagesData[k].ns;
ret.destTalkId = pagesData[k].talkid; // could be undefined
ret.destCanMove = pagesData[k].actions.move === '';
ret.destIsRedir = pagesData[k].redirect === '';
}
}
if (!ret.valid) return ret;
if (!ret.currCanMove) {
ret.valid = false;
ret.invalidReason = ('' + ret.currTitle + " is immovable. Aborting");
return ret;
}
if (!ret.destCanMove) {
ret.valid = false;
ret.invalidReason = ('' + ret.destTitle + " is immovable. Aborting");
return ret;
}
if (ret.currNs % 2 !== ret.destNs % 2) {
ret.valid = false;
ret.invalidReason = "Namespaces don't match: one is a talk page.";
return ret;
}
if (count !== 2) {
ret.valid = false;
ret.invalidReason = "Pages have the same title. Aborting.";
return ret;
}
ret.currNsAllowSubpages = nsData['' + ret.currNs].subpages !== '';
ret.destNsAllowSubpages = nsData['' + ret.destNs].subpages !== '';
// if same namespace (subpages allowed), if one is subpage of another,
// disallow movesubpages
if (ret.currTitle.startsWith(ret.destTitle + '/')
|| ret.destTitle.startsWith(ret.currTitle + '/')) {
if (ret.currNs !== ret.destNs) {
ret.valid = false;
ret.invalidReason = "Strange.\n" + ret.currTitle + " in ns "
+ ret.currNs + "\n" + ret.destTitle + " in ns " + ret.destNs
+ ". Disallowing.";
return ret;
}
ret.allowMoveSubpages = ret.currNsAllowSubpages;
if (!ret.allowMoveSubpages)
ret.addlInfo = "One page is a subpage. Disallowing move-subpages";
}
if (ret.currNs % 2 === 1) {
ret.checkTalk = false; // no need to check talks, already talk pages
} else { // ret.checkTalk = true;
var currTPData = getTalkPageName(nsData, ret.currTitle, ret.currNs);
ret.currTitleWithoutPrefix = currTPData.titleWithoutPrefix;
ret.currTalkName = currTPData.talkTitle;
var destTPData = getTalkPageName(nsData, ret.destTitle, ret.destNs);
ret.destTitleWithoutPrefix = destTPData.titleWithoutPrefix;
ret.destTalkName = destTPData.talkTitle;
// possible: ret.currTalkId undefined, but subject page has talk subpages
}
return ret;
}
/**
* Given two talk page titles (may be undefined), retrieves their pages for comparison
* Assumes that talk pages always have subpages enabled.
* Assumes that pages are not identical (subject pages were already verified)
* Assumes namespaces are okay (subject pages already checked)
* (Currently) assumes that the malicious case of subject pages
* not detected as subpages and the talk pages ARE subpages
* (i.e. A and A/B vs. Talk:A and Talk:A/B) does not happen / does not handle
* Returns structure indicating whether move talk should be allowed
*/
function talkValidate(checkTalk, talk1, talk2) {
var ret = {};
ret.allowMoveTalk = true;
if (!checkTalk) { return ret; } // currTitle destTitle already talk pages
if (talk1 === undefined || talk2 === undefined) {
alert("Unable to validate talk. Disallowing movetalk to be safe");
ret.allowMoveTalk = false;
return ret;
}
ret.currTDNE = true;
ret.destTDNE = true;
ret.currTCanCreate = true;
ret.destTCanCreate = true;
var talkTitleArr = [talk1, talk2];
if (talkTitleArr.length !== 0) {
var talkData = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
alert("Unable to get info on talk pages."); return ret; },
data: { action:'query', format:'json', prop:'info',
intestactions:'move|create', titles:talkTitleArr.join('|') }
}).responseText).query.pages;
for (var id in talkData) {
if (talkData[id].title === talk1) {
ret.currTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
ret.currTTitle = talkData[id].title;
ret.currTCanMove = talkData[id].actions.move === '';
ret.currTCanCreate = talkData[id].actions.create === '';
ret.currTalkIsRedir = talkData[id].redirect === '';
} else if (talkData[id].title === talk2) {
ret.destTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
ret.destTTitle = talkData[id].title;
ret.destTCanMove = talkData[id].actions.move === '';
ret.destTCanCreate = talkData[id].actions.create === '';
ret.destTalkIsRedir = talkData[id].redirect === '';
} else {
alert("Found pageid not matching given ids."); return {};
}
}
}
ret.allowMoveTalk = (ret.currTCanCreate && ret.currTCanMove)
&& (ret.destTCanCreate && ret.destTCanMove);
return ret;
}
/**
* Given existing title (not prefixed with "/"), optionally searching for talk,
* finds subpages (incl. those that are redirs) and whether limits are exceeded
* As of 2016-08, uses 2 api get calls to get needed details:
* whether the page can be moved, whether the page is a redirect
*/
function getSubpages(nsData, title, titleNs, isTalk) {
if ((!isTalk) && nsData['' + titleNs].subpages !== '') { return { data:[] }; }
var titlePageData = getTalkPageName(nsData, title, titleNs);
var subpages = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
return { error:"Unable to search for subpages. They may exist" }; },
data: { action:'query', format:'json', list:'allpages',
apnamespace:(isTalk ? (titleNs + 1) : titleNs),
apfrom:(titlePageData.titleWithoutPrefix + '/'),
apto:(titlePageData.titleWithoutPrefix + '0'),
aplimit:101 }
}).responseText).query.allpages;
// put first 50 in first arr (need 2 queries due to api limits)
var subpageids = [[],[]];
for (var idx in subpages) {
subpageids[idx < 50 ? 0 : 1].push( subpages[idx].pageid );
}
if (subpageids[0].length === 0) { return { data:[] }; }
if (subpageids[1].length === 51) { return { error:"100+ subpages. Aborting" }; }
var dataret = [];
var subpageData0 = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
return { error:"Unable to fetch subpage data." }; },
data: { action:'query', format:'json', prop:'info', intestactions:'move|create',
pageids:subpageids[0].join('|') }
}).responseText).query.pages;
for (var k0 in subpageData0) {
dataret.push({
title:subpageData0[k0].title,
isRedir:subpageData0[k0].redirect === '',
canMove:subpageData0[k0].actions.move === ''
});
}
if (subpageids[1].length === 0) { return { data:dataret }; }
var subpageData1 = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
return { error:"Unable to fetch subpage data." }; },
data: { action:'query', format:'json', prop:'info', intestactions:'move|create',
pageids:subpageids[1].join('|') }
}).responseText).query.pages;
for (var k1 in subpageData1) {
dataret.push({
title:subpageData1[k1].title,
isRedir:subpageData1[k1].redirect === '',
canMove:subpageData1[k1].actions.move === ''
});
}
return { data:dataret };
}
/**
* Prints subpage data given retrieved subpage information returned by getSubpages
* Returns a suggestion whether movesubpages should be allowed
*/
function printSubpageInfo(basepage, currSp) {
var ret = {};
var currSpArr = [];
var currSpCannotMove = [];
var redirCount = 0;
for (var kcs in currSp.data) {
if (!currSp.data[kcs].canMove) {
currSpCannotMove.push(currSp.data[kcs].title);
}
currSpArr.push((currSp.data[kcs].isRedir ? "(R) " : " ")
+ currSp.data[kcs].title);
if (currSp.data[kcs].isRedir)
redirCount++;
}
if (currSpArr.length > 0) {
alert((currSpCannotMove.length > 0
? "Disabling move-subpages.\n"
+ "The following " + currSpCannotMove.length + " (of "
+ currSpArr.length + ") total subpages of "
+ basepage + " CANNOT be moved:\n\n "
+ currSpCannotMove.join("\n ") + '\n\n'
: (currSpArr.length + " total subpages of " + basepage + ".\n"
+ (redirCount !== 0 ? ('' + redirCount + " redirects, labeled (R)\n") : '')
+ '\n' + currSpArr.join('\n'))));
}
ret.allowMoveSubpages = currSpCannotMove.length === 0;
ret.noNeed = currSpArr.length === 0;
return ret;
}
/**
* After successful page swap, post-move cleanup:
* Make talk page redirect
* TODO more reasonable cleanup/reporting as necessary
* vData.(curr|dest)IsRedir
*/
function doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData) {
if (movedTalk && vTData.currTDNE && confirm("Create redirect "
+ vData.currTalkName + " → " + vData.destTalkName + " if possible?")) {
// means that destination talk now is redlinked TODO
} else if (movedTalk && vTData.destTDNE && confirm("Create redirect "
+ vData.destTalkName + " → " + vData.currTalkName + " if possible?")) {
// curr talk now is redlinked TODO
}
}
/**
* Swaps the two pages (given all prerequisite checks)
* Optionally moves talk pages and subpages
*/
function swapPages(titleOne, titleTwo, moveReason, intermediateTitlePrefix,
moveTalk, moveSubpages, vData, vTData) {
if (titleOne === null || titleTwo === null
|| moveReason === null || moveReason === '') {
alert("Titles are null, or move reason given was empty. Swap not done");
return false;
}
var intermediateTitle = intermediateTitlePrefix + titleOne;
var pOne = { action:'move', from:titleTwo, to:intermediateTitle,
reason:"[[WP:PMRC#4|Round-robin history swap]] step 1 using [[User:Andy M. Wang/pageswap|pageswap]]",
watchlist:"unwatch", noredirect:1 };
var pTwo = { action:'move', from:titleOne, to:titleTwo,
reason:moveReason,
watchlist:"unwatch", noredirect:1 };
var pTre = { action:'move', from:intermediateTitle, to:titleOne,
reason:"[[WP:PMRC#4|Round-robin history swap]] step 3 using [[User:Andy M. Wang/pageswap|pageswap]]",
watchlist:"unwatch", noredirect:1 };
if (moveTalk) {
pOne.movetalk = 1; pTwo.movetalk = 1; pTre.movetalk = 1;
}
if (moveSubpages) {
pOne.movesubpages = 1; pTwo.movesubpages = 1; pTre.movesubpages = 1;
}
new mw.Api().postWithToken("csrf", pOne).done(function (reslt1) {
new mw.Api().postWithToken("csrf", pTwo).done(function (reslt2) {
new mw.Api().postWithToken("csrf", pTre).done(function (reslt3) {
alert("Moves completed successfully.\n"
+ "Please create new red-linked talk pages/subpages if there are incoming links\n"
+ " (check your contribs for \"Talk:\" redlinks),\n"
+ " correct any moved redirects, and do post-move cleanup if necessary.");
//doPostMoveCleanup(moveTalk, moveSubpages, vData, vTData);
}).fail(function (reslt3) {
alert("Fail on third move " + intermediateTitle + " → " + titleOne);
});
}).fail(function (reslt2) {
alert("Fail on second move " + titleOne + " → " + titleTwo);
});
}).fail(function (reslt1) {
alert("Fail on first move " + titleTwo + " → " + intermediateTitle);
});
}
/**
* Given two titles, normalizes, does prerequisite checks for talk/subpages,
* prompts user for config before swapping the titles
*/
function roundrobin(uPerms, currNs, currTitle, destTitle, intermediateTitlePrefix) {
// get ns info (nsData.query.namespaces)
var nsData = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
error: function (jsondata) { alert("Unable to get info about namespaces"); },
data: { action:'query', format:'json', meta:'siteinfo', siprop:'namespaces' }
}).responseText).query.namespaces;
// get page data, normalize titles
var relevantTitles = currTitle + "|" + destTitle;
var pagesData = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
alert("Unable to get info about " + currTitle + " or " + destTitle);
},
data: { action:'query', format:'json', prop:'info', inprop:'talkid',
intestactions:'move|create', titles:relevantTitles }
}).responseText).query;
for (var kp in pagesData.normalized) {
if (currTitle == pagesData.normalized[kp].from) { currTitle = pagesData.normalized[kp].to; }
if (destTitle == pagesData.normalized[kp].from) { destTitle = pagesData.normalized[kp].to; }
}
// validate namespaces, not identical, can move
var vData = swapValidate(currTitle, destTitle, pagesData.pages, nsData, uPerms);
if (!vData.valid) { alert(vData.invalidReason); return; }
if (vData.addlInfo !== undefined) { alert(vData.addlInfo); }
// subj subpages
var currSp = getSubpages(nsData, vData.currTitle, vData.currNs, false);
if (currSp.error !== undefined) { alert(currSp.error); return; }
var currSpFlags = printSubpageInfo(vData.currTitle, currSp);
var destSp = getSubpages(nsData, vData.destTitle, vData.destNs, false);
if (destSp.error !== undefined) { alert(destSp.error); return; }
var destSpFlags = printSubpageInfo(vData.destTitle, destSp);
var vTData = talkValidate(vData.checkTalk, vData.currTalkName, vData.destTalkName);
// future goal: check empty subpage DESTINATIONS on both sides (subj, talk)
// for create protection. disallow move-subpages if any destination is salted
var currTSp = getSubpages(nsData, vData.currTitle, vData.currNs, true);
if (currTSp.error !== undefined) { alert(currTSp.error); return; }
var currTSpFlags = printSubpageInfo(vData.currTalkName, currTSp);
var destTSp = getSubpages(nsData, vData.destTitle, vData.destNs, true);
if (destTSp.error !== undefined) { alert(destTSp.error); return; }
var destTSpFlags = printSubpageInfo(vData.destTalkName, destTSp);
var noSubpages = currSpFlags.noNeed && destSpFlags.noNeed
&& currTSpFlags.noNeed && destTSpFlags.noNeed;
// If one ns disables subpages, other enables subpages, AND HAS subpages,
// consider abort. Assume talk pages always safe (TODO fix)
var subpageCollision = (vData.currNsAllowSubpages && !destSpFlags.noNeed)
|| (vData.destNsAllowSubpages && !currSpFlags.noNeed);
var moveTalk = false;
// TODO: count subpages and make restrictions?
if (vData.checkTalk && vTData.allowMoveTalk) {
moveTalk = confirm("Move talk page(s)? (OK for yes, Cancel for no)");
} else if (vData.checkTalk) {
alert("Disallowing moving talk. "
+ (!vTData.currTCanCreate ? (vData.currTalkName + " is create-protected")
: (!vTData.destTCanCreate ? (vData.destTalkName + " is create-protected")
: "Talk page is immovable")));
}
var moveSubpages = false;
// TODO future: currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages
// needs to be separate check. If talk subpages immovable, should not affect subjspace
if (!subpageCollision && !noSubpages && vData.allowMoveSubpages
&& (currSpFlags.allowMoveSubpages && destSpFlags.allowMoveSubpages)
&& (currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages)) {
moveSubpages = confirm("Move subpages? (OK for yes, Cancel for no)");
} else if (subpageCollision) {
alert("One namespace does not have subpages enabled. Disallowing move subpages");
}
var moveReason = '';
if (typeof moveReasonDefault === 'string') {
moveReason = prompt("Move reason:", moveReasonDefault);
} else {
moveReason = prompt("Move reason:");
}
var confirmString = "Round-robin configuration:\n "
+ currTitle + " → " + destTitle + "\n : " + moveReason
+ "\n with movetalk:" + moveTalk + ", movesubpages:" + moveSubpages
+ "\n\nProceed? (Cancel to abort)";
if (confirm(confirmString)) {
swapPages(currTitle, destTitle, moveReason, intermediateTitlePrefix,
moveTalk, moveSubpages, vData, vTData);
}
}
var currNs = mw.config.get("wgNamespaceNumber");
if (currNs < 0 || currNs >= 120
|| (currNs >= 6 && currNs <= 9)
|| (currNs >= 14 && currNs <= 99))
return; // special/other page
var portletLink = mw.util.addPortletLink("p-cactions", "#", "Swap",
"ca-swappages", "Perform a revision history swap / round-robin move");
$( portletLink ).click(function(e) {
e.preventDefault();
var userPermissions = checkUserPermissions();
if (!userPermissions.canSwap) {
alert("User rights insufficient for action."); return;
}
var currTitle = mw.config.get("wgPageName");
var destTitle = prompt("Swap \"" + (currTitle.replace(/_/g, ' ')) + "\" with:");
return roundrobin(userPermissions, currNs, currTitle, destTitle, "Draft:Move/");
});
});
});
// </syntaxhighlight>