User:Andy M. Wang/pageswap.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. |
This user script seems to have a documentation page at User:Andy M. Wang/pageswap. |
// <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>