User:Ahecht/Scripts/pageswap 1.5.2.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
// Based on [[:en:User:Andy M. Wang/pageswap.js]] by [[User:Andy M. Wang]] 1.6.1.2018.0920
// Modified by [[User:Ahecht]] -- v1.5.2
/*jshint esversion: 6 */

/**
 * Initialize variables
 */
if (typeof pagemoveDoPostMoveCleanup === 'undefined') { pagemoveDoPostMoveCleanup = true; }
var pagemoveLink = "[[:en:User:Ahecht/Scripts/pageswap|pageswap]]";

$(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) { mw.notify("Swapping pages unavailable.", { title: 'Page Swap Error', type: 'error' }); 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) {
		mw.notify("Unable to validate talk. Disallowing movetalk to be safe", { title: 'Page Swap Error', type: 'warn' });
		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) { mw.notify("Unable to get info on talk pages.", { title: 'Page Swap Error', type: 'error' }); 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 {
				mw.notify("Found pageid ("+talkData[id].title+") not matching given ids ("+talk1+" and "+talk2+").", { title: 'Page Swap Error', type: 'error' }); 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;
	ret.spArr = currSpArr;
	return ret;
}

function doDoneMsg(doneMsg, vData) {
	if (/failed/ig.test(doneMsg)) { 
		mw.notify(doneMsg, { tag: 'status', title: 'Page Swap Status', type: 'warn' });
	} else { 
		mw.notify(doneMsg, { tag: 'status', title: 'Page Swap Status', type: 'success' });
	}
	
	var spString = "";
	
	if (vData.allSpArr.length) {
		spString = "\nThe following subpages were moved, and may need new or updated redirects:\n  " + 
			vData.allSpArr.join("\n  ") + "\n";
	}
	
	setTimeout(() => {
		if(confirm(doneMsg +
			"\nPlease create new red-linked talk pages/subpages if there are " +
			"incoming links (check your contribs for \"Talk:\" redlinks), " +
			"correct any moved redirects, and do post-move cleanup if necessary.\n" +
			spString + "\nOpen contribs page?")) {
				window.open(mw.util.getUrl("Special:Contributions")+'/'+mw.util.wikiUrlencode(mw.user.getName()));
				location.reload();
		} else {
			location.reload();
		}
	}, 250);
}

function createMissingTalk(movedTalk, movedSubpages, vData, vTData, doneMsg) {
	if (movedTalk) {
		var fromTalk, toTalk;
		if (vTData.currTDNE && !vTData.destTDNE) {
			fromTalk = vData.destTalkName;
			toTalk = vData.currTalkName;
		} else if (vTData.destTDNE && !vTData.currTDNE) {
			fromTalk = vData.currTalkName;
			toTalk = vData.destTalkName;
		}
		
		if (fromTalk && toTalk) {
			mw.notify("Talk page moved...", { tag: 'status', title: 'Page Swap Status' });
			setTimeout(() => {
				if (confirm(doneMsg + "\nCreate redirect " + fromTalk +
						"\n→ " + toTalk + " if possible?")) {
					var talkRedirect = {
						action:'edit',
						title:fromTalk,
						createonly: true,
						text: "#REDIRECT [[" + toTalk + "]]\n{{R from move}}",
						summary: "Create redirect to [[" + toTalk + "]] using " + pagemoveLink,
						watchlist:"unwatch"
					};
					mw.notify("Creating talk page redirect...", { tag: 'status', title: 'Page Swap Status' });
					new mw.Api().postWithToken("csrf", talkRedirect).done(function (resltc) {
						doDoneMsg(doneMsg + "Redirect " + fromTalk +
							"\n→ " +toTalk + " created.\n", vData);
					}).fail(function (resltc) {
						doDoneMsg(doneMsg + "Failed to create redirect: " + resltc + ".\n", vData);
					});
				} else { doDoneMsg("", vData); }
			}, 250);
		} else { doDoneMsg(doneMsg, vData); }
	} else { doDoneMsg(doneMsg, vData); }
}

/**
 * After successful page swap, post-move cleanup:
 * Make talk page redirect
 * TODO more reasonable cleanup/reporting as necessary
 * vData.(curr|dest)IsRedir
 */
/** TO DO:
 *Check if talk is self redirect
 */
function doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData, doneMsg, current = "currTitle", destination = "destTitle") {
	if (typeof doneMsg === 'undefined') {
		doneMsg = "Moves completed successfully.\n";
		mw.notify(doneMsg, { tag: 'status', title: 'Page Swap Status', type: 'success' });
	}
	
	// Check for self redirect
	var rData = JSON.parse($.ajax({
		url: mw.util.wikiScript('api'), async:false,
		error: function (jsondata) { mw.notify("Unable to get info about " + vData[current] + ".\n", { title: 'Page Swap Error', type: 'error' }); },
		data: { action:'query', format:'json', redirects:'true', titles: vData[current] }
	}).responseText).query;
		
	if (rData && rData.redirects && rData.redirects[0].from == rData.redirects[0].to){
		var parseData = JSON.parse($.ajax({
			url: mw.util.wikiScript('api'), async:false,
			error: function (jsondata) { mw.notify("Unable to fetch contents of " + vData[current] + ".\n", { title: 'Page Swap Error', type: 'error' });	},
			data: {action:'parse', format:'json', prop:'wikitext', page: vData[current] }
		}).responseText).parse;
		
		if (parseData) {
			var newWikitext = parseData.wikitext['*'].replace(/^\s*#REDIRECT +\[\[ *.* *\]\]/i, ('#REDIRECT [[' + vData[destination] + ']]'));
			if (newWikitext != parseData.wikitext['*']) {
				mw.notify("Retargeting redirect at " + vData[current] + " to "	+ vData[destination] + "...", { tag: 'status', title: 'Page Swap Status' });
				new mw.Api().postWithToken("csrf", {
					action:'edit',
					title: vData[current],
					text: newWikitext,
					summary : "Retarget redirect to [[" +
						vData[destination] + "]] using " +
						pagemoveLink,
					watchlist: "unwatch"
				} ).done(function (resltc) {
					doneMsg = doneMsg + "Redirect at " +
						vData[current] + " retargeted to " +
						vData[destination] + ".\n";
					if (current == "currTitle") {
						doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData, doneMsg, "currTalkName", "destTalkName");
					} else {
						createMissingTalk(movedTalk, movedSubpages, vData, vTData, doneMsg);
					}
				} ).fail(function (resltc) {
					doneMsg = doneMsg + "Failed to retarget redirect at " +
						vData[current] + " to " +
						vData[destination] + ": " + resltc + ".\n";
					if (current == "currTitle") {
						doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData, doneMsg, "currTalkName", "destTalkName");
					} else {
						createMissingTalk(movedTalk, movedSubpages, vData, vTData, doneMsg);
					}
				} );
				
				return;
			} else {
				doneMsg = doneMsg + "Failed to retarget redirect at " +
					vData[current] + " to " + vData[destination] +
					": String not found.\n";
			}
		} else {
			doneMsg = doneMsg + "Failed to check contents of" +
				vData[current] + ": " + err + ".\n";
		}
	}
	
	if (current == "currTitle") {
		doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData, doneMsg, "currTalkName", "destTalkName");
	} else {
		createMissingTalk(movedTalk, movedSubpages, vData, vTData, doneMsg);
	}
}


/**
 * 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 === '') {
		mw.notify("Titles are null, or move reason given was empty. Swap not done", { title: 'Page Swap Error', type: 'error' });
		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 " + pagemoveLink,
		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 " + pagemoveLink,
		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;
	}
	
	mw.notify("Doing round-robin history swap step 1...", { tag: 'status', title: 'Page Swap Status' });
	new mw.Api().postWithToken("csrf", pOne).done(function (reslt1) {
		mw.notify("Doing round-robin history swap step 2...", { tag: 'status', title: 'Page Swap Status' });
		new mw.Api().postWithToken("csrf", pTwo).done(function (reslt2) {
			mw.notify("Doing round-robin history swap step 3...", { tag: 'status', title: 'Page Swap Status' });
			new mw.Api().postWithToken("csrf", pTre).done(function (reslt3) {
				if (pagemoveDoPostMoveCleanup) {
					doPostMoveCleanup(moveTalk, moveSubpages, vData, vTData);
				} else {
					doDoneMsg("Moves completed successfully.\n", vData);
				}
			}).fail(function (reslt3) {
				doDoneMsg("Fail on third move " + intermediateTitle + " → " + titleOne + "\n", vData);
			});
		}).fail(function (reslt2) {
			doDoneMsg("Fail on second move " + titleOne + " → " + titleTwo + "\n", vData);
		});
	}).fail(function (reslt1) {
		doDoneMsg("Fail on first move " + titleTwo + " → " + intermediateTitle + "\n", vData);
	});
}

/**
 * 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) { mw.notify("Unable to get info about namespaces", { title: 'Page Swap Error', type: 'error' }); },
		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) {
			mw.notify("Unable to get info about " + currTitle + " or " + destTitle, { title: 'Page Swap Error', type: 'error' });
		},
		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) { mw.notify(vData.invalidReason, { title: 'Page Swap Error', type: 'error' }); return; }
	if (vData.addlInfo !== undefined) { mw.notify(vData.addlInfo, { title: 'Page Swap Error', type: 'error' }); }

	// subj subpages
	var currSp = getSubpages(nsData, vData.currTitle, vData.currNs, false);
	if (currSp.error !== undefined) { mw.notify(currSp.error, { title: 'Page Swap Error', type: 'error' }); return; }
	var currSpFlags = printSubpageInfo(vData.currTitle, currSp);
	var destSp = getSubpages(nsData, vData.destTitle, vData.destNs, false);
	if (destSp.error !== undefined) { mw.notify(destSp.error, { title: 'Page Swap Error', type: '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) { mw.notify(currTSp.error, { title: 'Page Swap Error', type: 'error' }); return; }
	var currTSpFlags = printSubpageInfo(vData.currTalkName, currTSp);
	var destTSp = getSubpages(nsData, vData.destTitle, vData.destNs, true);
	if (destTSp.error !== undefined) { mw.notify(destTSp.error, { title: 'Page Swap Error', type: '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 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) {
		mw.notify("One namespace does not have subpages enabled. Disallowing move subpages", { title: 'Page Swap Error', type: 'error' });
	}
	
	if (moveSubpages) { 
		vData.allSpArr = currSpFlags.spArr.concat(destSpFlags.spArr, currTSpFlags.spArr, destTSpFlags.spArr);
	} else {
		vData.allSpArr = [];
	}

	var moveTalk = false;
	// TODO: count subpages and make restrictions?
	if (vData.checkTalk && (!vTData.currTDNE || !vTData.destTDNE || moveSubpages)) {
		if (vTData.allowMoveTalk) {
			moveTalk = confirm("Move talk page(s)? (OK for yes, Cancel for no)");
		} else {
			alert("Disallowing moving talk. " +
				(!vTData.currTCanCreate ? (vData.currTalkName + " is create-protected")
				: (!vTData.destTCanCreate ? (vData.destTalkName + " is create-protected")
				: "Talk page is immovable")));
		}
	}
	
	var moveReason = '';
	var moveReasonPrompt = '';
	if (typeof mw.util.getParamValue("wpReason") === 'string') {
		moveReasonPrompt = mw.util.getParamValue("wpReason");
	} else if (document.getElementsByName("wpReason")[0] && document.getElementsByName("wpReason")[0].value != '') {
		moveReasonPrompt = document.getElementsByName("wpReason")[0].value;
	} else if (typeof moveReasonDefault === 'string') {
		moveReasonPrompt = moveReasonDefault;
	}
	moveReason = prompt("Move reason:", moveReasonPrompt);
	
	var spToMoveString = "";
	
	if (moveSubpages) {
		spToMoveString = "Subpages to move:\n  "+ vData.allSpArr.join("\n  ") + "\n\n";
	}
	
	var confirmString = "Round-robin configuration:\n  " +
		currTitle + " → " + destTitle + "\n    : " + moveReason +
		"\n      with movetalk:" + moveTalk + ", movesubpages:" + moveSubpages +
		"\n\n" + spToMoveString +
		"Proceed? (Cancel to abort)";

	if (confirm(confirmString)) {
		swapPages(currTitle, destTitle, moveReason, intermediateTitlePrefix,
			moveTalk, moveSubpages, vData, vTData);
	}
}

	var currNs = mw.config.get("wgNamespaceNumber");
	var wpOldTitle = mw.util.getParamValue("wpOldTitle");
	if (!wpOldTitle && document.getElementsByName("wpOldTitle")[0] && document.getElementsByName("wpOldTitle")[0].value != ''){
		wpOldTitle = document.getElementsByName("wpOldTitle")[0].value;
	}
	var wpNewTitle = mw.util.getParamValue("wpNewTitle");
	if (!wpNewTitle && document.getElementsByName("wpNewTitleMain")[0] && document.getElementsByName("wpNewTitleMain")[0].value != '' && document.getElementsByName("wpNewTitleNs")[0]){
		wpNewTitle = document.getElementsByName("wpNewTitleMain")[0].value;
		var nsid = document.getElementsByName("wpNewTitleNs")[0].value;
		if (nsid != 0) {
			wpNewTitle = mw.config.get("wgFormattedNamespaces")[nsid] + ":" + wpNewTitle;
		}
	}
	if (currNs < -1 || currNs >= 120 ||
			(currNs >=  6 && currNs <= 9) ||
			(currNs >= 14 && currNs <= 99) ||
			(currNs == -1 && mw.config.get("wgCanonicalSpecialPageName") != "Movepage") ||
			(mw.config.get("wgCanonicalSpecialPageName") == "Movepage" && !wpOldTitle)
			)
		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) {
			mw.notify("User rights insufficient for action.", { title: 'Page Swap Error', type: 'error' }); return;
		}
		
		var currTitle = wpOldTitle || mw.config.get("wgPageName");
		var destTitle = wpNewTitle || prompt("Swap \"" + (currTitle.replace(/_/g, ' ')) + "\" with:", (currTitle.replace(/_/g, ' ')));
		
		return roundrobin(userPermissions, currNs, currTitle, destTitle, "Draft:Move/");
	});
	
	if (mw.config.get("wgCanonicalSpecialPageName") == "Movepage" &&
			$( "div.mw-message-box-error" ).find( "p" ).eq(1).is( ":contains('name already exists')" ) &&
			wpOldTitle)
	{
		$( "div.mw-message-box-error" ).find( "p" ).eq(2).html( 'Please choose another name, or perform a <a title="Perform a revision history swap / round-robin move" href="#" id="pageswapLink">swap</a>.' );
		$( "#pageswapLink" ).click(function(e) {
			e.preventDefault();
			$( portletLink ).click();
		});
	}

});
});
// </syntaxhighlight>