User:Ahecht/Scripts/pageswap-core.js

//jshint -W083

function pageSwap(prefix, moveReason, debug) { var config = { link: "pageswap", intermediatePrefix: "Draft:Move/", portletLink: "Swap" + (debug ? " (debug)" : ""), portletAlt: "Perform a revision history swap / round-robin move", swapButton: 'Swap pages' + (debug ? " (debug)" : ""), confirmButton: 'Confirm' + (debug ? " (debug)" : ""), confirmMessageHeader: "Round-robin configuration:\n*", confirmMessageFooter: '\nPress "Confirm" to proceed.', statusMessageHeader: "Performing page swap:\n", introText: " '''Please post bug reports/comments/suggestions for " + 'the pageswap script at User talk:Ahecht. To revert to the ' + 'previous dialogue-based version of this script, use ' + "User:Ahecht/Scripts/pageswap_1.5.2.js instead.''' \n\n" + 'Using the form below will swap two pages using the pageswap script, moving all of their histories to ' + "the new names. '''Links to the old page titles will not be " + "changed. Be sure to check Special:MyContributions''' " + 'for double or broken redirects and red ' +			'links. You are responsible for making sure that links continue' + ' to point where they are supposed to go and for doing all post-' + 'move cleanup listed under Out of scope in the script\'s documentation.\n\n' + "Note: This can be a drastic and unexpected change for a " + 'popular page; please be sure you understand the consequences of ' + 'this before proceeding. Please read Moving a page ' + 'for more detailed instructions.', doneMsgCleanup: 'Please do post-move cleanup as necessary', doneMsgRedlink: 'create new red-linked talk pages/subpages if ' + 'there are incoming links (check your ' +			'contribs for "Talk:" and subpage redlinks)', doneMsgRedir: 'correct any moved redirects (including on talk pages ' +			'and subpages)', doneSubpages: '*The following subpages were moved, and may need new ' + 'or updated redirects:\n', errorMsg: 'Error adding swap form to page!', redirTempls: "", types: ['notice', 'success', 'warning', 'error'] }, params = { currTitle: {}, destTitle: {}, confirmMessages: [], statusMessages: [], defaultMoveTalk: true, done: false, idempotency: {psConfirm: 0, psStatus: 0}, cleanup: (			typeof pagemoveDoPostMoveCleanup === 'undefined' ?			true : 			pagemoveDoPostMoveCleanup		) };	function filterHtml(rawHtml) { $value=$($.parseHTML(rawHtml)); $value.filter("div.mw-parser-output").contents.each(function {			if(this.nodeType === Node.COMMENT_NODE || this.nodeType === Node.TEXT_NODE) {				$(this).remove;			}		}).find('a.mw-redirect').each(function {			$(this).attr('href', $(this).attr('href') + "?redirect=no");		}); return $value.html; }	function setLabel(container, label, idempotency) { label = new OO.ui.HtmlSnippet(label); if (idempotency == params.idempotency[container.elementId]) { container.setLabel(label); container.toggle(true); container.scrollElementIntoView; }	}	function parseError(ps, label, codetr, reslttr, idempotency) { label = "Error parsing wikitext:\n\n" + label + "\n\n" + (reslttr.error.info || (codetr + ".")); console.warn(label); ps.setType('error'); setLabel(ps, label, idempotency); }	function showConfirm(message, type='notice', done=false) { var idempotency = ++params.idempotency.psConfirm; if (config.types.indexOf(type) > config.types.indexOf(psConfirm.type)) { psConfirm.setType(type); }		if (message !== '') { params.confirmMessages.push(message.replace("WP:RM/TR", "WP:RM/TR")); }		var label = config.confirmMessageHeader + params.confirmMessages.join("\n*")+ (done ? config.confirmMessageFooter : ''); new mw.Api.parse(label).done(			(parsedText) => setLabel(psConfirm, filterHtml(parsedText), idempotency)		).fail(			(codetr, reslttr) => parseError(psConfirm, label, codetr, reslttr, idempotency)		); }	function showStatus(message, type='notice', done=false, indent=false, topic=false) { var idempotency = ++params.idempotency.psStatus; if (config.types.indexOf(type) > config.types.indexOf(psStatus.type)) { psStatus.setType(type); }		if (!params.done && done) { params.done = true; }		if (message !== '') { var topicFlag = topic ? "" : false; var topicIndex = params.statusMessages.findIndex((str) => str.indexOf(topicFlag) > -1); message = "*"+(indent ? "*" : "") + message.replace("WP:RM/TR",				"WP:RM/TR") + "\n" + (topicFlag || ""); if (topicIndex > -1) { params.statusMessages[topicIndex] = params.statusMessages[topicIndex].replace(topicFlag, message); } else { params.statusMessages.push(message); }		}

var doneSubpagesMessage = "", doneMessage = ""; if (params.done) { if (params.allSpArr.length) { doneSubpagesMessage = config.doneSubpages + "**" + 					params.allSpArr.join("\n**") + "\n"; }			psContribsButton.toggle(true); var doneMessages = [config.doneMsgCleanup]; if (!params.talkRedirect || params.moveSubpages) {doneMessages.push(config.doneMsgRedlink)} if (!params.fixSelfRedirect || params.moveSubpages) {doneMessages.push(config.doneMsgRedir)} if (doneMessages.length < 3) { doneMessage = doneMessages.join(" and ") + "."; } else { doneMessage = doneMessages.slice(0, -1).join(', ') + ', and ' + doneMessages.slice(-1) + "."; }		}		var label = config.statusMessageHeader + params.statusMessages.join('') + doneSubpagesMessage + doneMessage; new mw.Api.parse(label).done(			(parsedText) => setLabel(psStatus, filterHtml(parsedText), idempotency)		).fail(			(codetr, reslttr) => parseError(psStatus, label, codetr, reslttr, idempotency)		); }	function getPagesData { // get page data, normalize titles var ret = {valid: true, invalidReason: ''}; var titlesString = " "+params.currTitle.title+" or "+params.destTitle.title+". "; var queryData = {action:'query', format:'json', prop:'info', inprop:'talkid', intestactions:'move|create', titles: (params.currTitle.title + "|" + params.destTitle.title), list:'logevents', leprop:'timestamp', letype:'move', letitle: params.currTitle.title, lelimit:'1' };		var query = $.ajax({ 			url: mw.util.wikiScript('api'), async:false,			error: function (jqXHR, textStatus, errorThrown) {				var errStr = "Error '"+(jqXHR.status||textStatus)+					"' fetching API data on "+titlesString+". "+					(errorThrown||jqXHR.responseText).replace("\n","");				console.warn(errStr);console.log(queryData);console.log(jqXHR);				ret = {valid: false, invalidReason: errStr};			},			data: queryData		}).responseJSON; if (typeof query === 'undefined' || typeof query.query === 'undefined') { return {valid: false, invalidReason: ret.invalidReason+ "Error parsing API data on"+titlesString}; }		if (!ret.valid) {return ret;} query = query.query; if (typeof query.pages !== 'undefined' && typeof query.logevents !== 'undefined') { for (var kn in query.normalized) { if (params.currTitle.title == query.normalized[kn].from) { params.currTitle.title = query.normalized[kn].to; }				if (params.destTitle.title == query.normalized[kn].from) { params.destTitle.title = query.normalized[kn].to; }			}			for (var kp in query.pages) { if (params.currTitle.title == query.pages[kp].title) { params.currTitle = query.pages[kp]; }				if (params.destTitle.title == query.pages[kp].title) { params.destTitle = query.pages[kp]; }				if (kp < 0) { ret.valid = false; if (typeof query.pages[kp].missing !== 'undefined') { ret.invalidReason += "Unable to find "+query.pages[kp].title+". "; } else if (typeof query.pages[kp].invalid !== 'undefined' &&						typeof query.pages[kp].invalidreason !== 'undefined') { ret.invalidReason += query.pages[kp].invalidreason; } else { ret.invalidReason += "Unable to get page data for"+titlesString; }				}			}			for (var kl in query.logevents) { var lastMove = (Date.now-Date.parse(query.logevents[kl].timestamp))/(1000*60); if ( lastMove < 60 ) { // 1 hour showConfirm("Warning: " + params.currTitle.title + " was moved " +						Math.round(lastMove) + " minute(s) ago.",						'warning'); } else if ( lastMove < 1440 ) { // 1 day showConfirm("Note: " + params.currTitle.title + " was moved " +						Math.round(lastMove/60) + " hours(s) ago.",						'notice'); } else if ( lastMove < 43200 ) { // 30 days showConfirm("" + params.currTitle.title + " was last moved " +						Math.round(lastMove/1440) + " day(s) ago.",						'notice'); }			}		} else { ret = {valid: false, invalidReason: "Unable to get page data for"+titlesString}; }

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(title, titleNs) { var ret = {}; var nsData = mw.config.get("wgFormattedNamespaces"); var prefixLength = nsData['' + titleNs].length === 0 ? 0 : nsData['' + titleNs].length + 1; ret.titleWithoutPrefix = title.substring(prefixLength, title.length); ret.talkTitle = nsData['' + ((Math.floor(titleNs / 2)*2) + 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 { // get page data, normalize titles var ret =	getPagesData; if (ret.valid === false || params.currTitle.title === null ||			params.destTitle.title === null || params === null		) { ret.valid = false; ret.invalidReason += "Failed to validate swap."; return ret; }

ret.allowMoveSubpages = true; ret.checkTalk = true; for (const k of ["currTitle", "destTitle"]) { if (k == "-1" || params[k].ns < 0) { ret.valid = false; ret.invalidReason = ("Page " + params[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 ((params[k].ns >= 6 && params[k].ns <= 9) ||			 (params[k].ns >= 10 && params[k].ns <= 11 && !params.uPerms.allowSwapTemplates) ||			 (params[k].ns >= 14 && params[k].ns <= 117) ||			 (params[k].ns >= 120)) { ret.valid = false; ret.invalidReason = ("Namespace of " + params[k].title + " (" + params[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 (params.currTitle.title == params[k].title) { ret.currTitle  = params[k].title; ret.currNs     = params[k].ns; ret.currTalkId = params[k].talkid; // could be undefined ret.currCanMove = params[k].actions.move === ''; ret.currIsRedir = params[k].redirect === ''; }			if (params.destTitle.title == params[k].title) { ret.destTitle  = params[k].title; ret.destNs     = params[k].ns; ret.destTalkId = params[k].talkid; // could be undefined ret.destCanMove = params[k].actions.move === ''; ret.destIsRedir = params[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; }		ret.currNsAllowSubpages = params.nsData[ + ret.currNs].subpages !== ; ret.destNsAllowSubpages = params.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(ret.currTitle, ret.currNs); ret.currTitleWithoutPrefix = currTPData.titleWithoutPrefix; ret.currTalkName = currTPData.talkTitle; var destTPData = getTalkPageName(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) { showStatus("Unable to validate talk. Disallowing movetalk to be safe.", 'warning'); 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) { showStatus("Unable to get info on talk pages.", 'warning'); 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 { showStatus("Found pageid ("+talkData[id].title+") not matching given ids ("+ talk1+" and "+talk2+").", '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(title, titleNs); var queryData = { action:'query', format:'json', list:'allpages', apnamespace:(isTalk ? ((Math.floor(titleNs / 2)*2) + 1) : titleNs), apfrom:(titlePageData.titleWithoutPrefix + '/'), apto:(titlePageData.titleWithoutPrefix + '0'), aplimit:101 }; var subpages = JSON.parse($.ajax({ url: mw.util.wikiScript('api'), async:false, error: function (jqXHR, textStatus, errorThrown) { var errStr = "API error '"+(jqXHR.status||textStatus)+ "' when searching for subpages of "+title+". "+ (errorThrown||jqXHR.responseText).replace("\n",""); console.warn(errStr);console.log(queryData);console.log(jqXHR); return { error:errStr+" Subpages may exist." };			},			data: queryData }).responseText).query; if (typeof subpages === 'object' && typeof subpages.allpages !== 'undefined') { subpages = subpages.allpages; } else { console.warn("API did not return 'allpages' when querying subpage data:");console.log(subpages); return { error:"API did not return subpage data. Subpages may exist." };		}

// 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 = $.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('|') }		}).responseJSON.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 = $.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('|') }		}).responseJSON.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].title); if (currSp.data[kcs].isRedir) redirCount++; }

if (currSpArr.length > 0) { if (currSpCannotMove.length > 0) { showConfirm("Disabling move-subpages." +					"The following " + currSpCannotMove.length + " (of " + currSpArr.length + ") total subpages of " +					basepage + " CANNOT be moved:\n**" +					currSpCannotMove.join("\n**") + "",					'warning'); } else if (typeof basepage !== 'undefined') { showConfirm(currSpArr.length + " total subpages of " + basepage + "" +					(redirCount !== 0 ? (" (" + redirCount + " redirects):") : ":") +					"\n**" + currSpArr.join("\n**") + ""); }		}

ret.allowMoveSubpages = currSpCannotMove.length === 0; ret.noNeed = currSpArr.length === 0; ret.spArr = currSpArr; return ret; }

function createMissingTalk(vData, vTData) { if (params.moveTalk && params.talkRedirect) { 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) { setTimeout( => {					if (params.talkRedirect) {						var talkRedirect = {							action:'edit',							title:fromTalk,							createonly: true,							text: "#REDIRECT " + toTalk + "\n\n" + config.redirTempls,							summary: "Create redirect to " + toTalk + " using " + config.link,							watchlist: params.watch						};						showStatus("Creating talk page redirect "+fromTalk+" → "+toTalk+"...", 'notice', false, false, "TPR"+fromTalk);						if (debug) {							console.log(talkRedirect);							showStatus("Talk page redirect simulated!.", 'success', true, true, "TPR"+fromTalk);						} else {							new mw.Api.postWithEditToken(talkRedirect).done(function (reslttr) { showStatus("Talk page redirect created!", 'success', true,									true, "TPR"+fromTalk); }).fail(function (codetr, reslttr) { showStatus("Failed to create redirect! " + 									(reslttr.error.info || (codetr + ".")),									'error', true, true, "TPR"+fromTalk); });						}					} else { showStatus('', 'notice', true); }				}, 250); } else { showStatus('', 'notice', true); } } else { showStatus('', 'notice', true); } }

/**	 * 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(vData, vTData, current = "currTitle", destination = "destTitle") { if (params.fixSelfRedirect) {// Check for self redirect for (const thisPage of [current, destination]){ var otherPage = (thisPage == current) ? destination : current; var rData = $.ajax({					url: mw.util.wikiScript('api'), async:false,					error: function (jsondata) {						showStatus("Unable to get info about " + vData[thisPage] + ".", 'error');					},					data: { action:'query', format:'json', redirects:'true', titles: vData[thisPage] }				}).responseJSON.query; if (rData && rData.redirects &&					(rData.redirects[0].from == rData.redirects[0].to || (debug && rData.redirects[0].to == vData[otherPage]) )				) {					var parseData = $.ajax({						url: mw.util.wikiScript('api'), async:false,						error: function (jsondata) {							showStatus("Unable to fetch contents of " + vData[thisPage] + ".", 'error');						},						data: {							action:'parse', format:'json', prop:'wikitext', page: vData[thisPage]						}					}).responseJSON.parse; if (parseData) { var redirRE = new RegExp("^\\s*#REDIRECT +\\[\\[ *.* *\\]\\]", "i"); if (parseData.wikitext['*'].search(redirRE) > -1) { showStatus("Retargeting redirect at " + vData[thisPage] +								" to "	+ vData[otherPage] + "...",								'notice', false, false, "RT"+vData[thisPage]); var retargetRedirect = { action:'edit', title: vData[thisPage], text: parseData.wikitext['*'].replace(redirRE,										'#REDIRECT '+vData[otherPage]+''), summary : "Retarget redirect to " +										vData[otherPage] + " using " + config.link, watchlist: params.watch };							if (debug) { showStatus("Retargeting simulated!",'success', false, true, "RT"+vData[thisPage]); } else { new mw.Api.postWithEditToken(retargetRedirect).done(function (result, jqXHR) {									if (typeof result.edit !== 'undefined') {										new mw.Api.get( { action: 'query', prop: '', redirects: 1, titles: result.edit.title }).done( function (data) { if (typeof data.query.redirects !== 'undefined') { showStatus("Redirect retargeted!", 'success',													false, true, "RT"+vData[thisPage]); } else { console.warn("Error parsing redirects after retargeting:"); console.warn(data); }										}).fail( function (codeart, rsltart) { console.warn("Error fetching page after retargeting:"); console.warn(codeart);console.warn(rsltart); });									} else {										console.warn("Error parsing result of retargeting:");										console.warn(result);console.warn(jqXHR);									}								}).fail(function (codert, resultrt) {									showStatus("Retargeting failed. "+ (resultrt.error.info || (codert + ".")), 'error', false, true, "RT"+vData[thisPage]);								}); }						} else { showStatus("Retargeting failed: String not found.", 'warning', false,								true, "RT"+vData[thisPage]); }					} else { showStatus("Failed to check contents of " +							vData[thisPage] + ": " + err + ".", 'error',							false, true, "RT"+vData[thisPage]); }				}			}				if (current == "currTitle") { doPostMoveCleanup(vData, vTData,"currTalkName", "destTalkName"); } else { createMissingTalk(vData, vTData); }		} else { //Option to fix self-redirects not selected, skipping createMissingTalk(vData, vTData); }	}

/**	 * Swaps the two pages (given all prerequisite checks) * Optionally moves talk pages and subpages */	function swapPages(vData, vTData) { if (params.currTitle.title === null || params.destTitle.title === null ||				params.moveReason === null || params.moveReason === '') { showStatus("Titles are null, or move reason given was empty. Swap not done", 'error'); return false; }

var intermediateTitle = config.intermediatePrefix + params.currTitle.title; var pOne = { action:'move', from:params.destTitle.title, to:intermediateTitle, reason:"Round-robin history swap step 1 using " + config.link, watchlist:params.watch, noredirect:1 }; var pTwo = { action:'move', from:params.currTitle.title, to:params.destTitle.title, reason:params.moveReason, watchlist:params.watch, noredirect:1 }; var pTre = { action:'move', from:intermediateTitle, to:params.currTitle.title, reason:"Round-robin history swap step 3 using " + config.link, watchlist:params.watch, noredirect:1 }; if (params.moveTalk) { pOne.movetalk = 1; pTwo.movetalk = 1; pTre.movetalk = 1; }		if (params.moveSubpages) { pOne.movesubpages = 1; pTwo.movesubpages = 1; pTre.movesubpages = 1; }		var currTitle = params.currTitle.title; var destTitle = params.destTitle.title; if (debug) { showStatus("Simulating round-robin history swap..."); showStatus("Step 1 (" + destTitle + " → " + 				intermediateTitle + ")...", 'notice', false, true); showStatus("Step 2 (" + currTitle + " → " + 				destTitle + ")...", 'notice', false, true); showStatus("Step 3 (" + intermediateTitle + " → " + 				currTitle + ")...", 'notice', false, true); var completeMessage = "Round-robin history swap of " +				currTitle + " (links) and " +				destTitle + " (links) simulated successfully!"; if (params.talkRedirect || params.fixSelfRedirect) { showStatus(completeMessage, 'success', false, true); doPostMoveCleanup(vData, vTData); } else { showStatus(completeMessage, 'success', true, true); }		} else { showStatus("Doing round-robin history swap..."); showStatus("Step 1 (" + destTitle + " → " + 				intermediateTitle + ")...", 'notice', false, true); new mw.Api.postWithEditToken(pOne).done(function (reslt1) {				showStatus("Step 2 (" + currTitle + " → " + 					destTitle + ")...", 'notice', false, true);				new mw.Api.postWithEditToken(pTwo).done(function (reslt2) { showStatus("Step 3 (" + intermediateTitle + " → " + 						currTitle + ")...", 'notice', false, true); new mw.Api.postWithEditToken(pTre).done(function (reslt3) {						var completeMessage = "Round-robin history swap of " +							currTitle + " (links) and " +							destTitle + " (links) completed successfully!";						if (params.talkRedirect || params.fixSelfRedirect) {							showStatus(completeMessage, 'success', false, true);							doPostMoveCleanup(vData, vTData);						} else {							showStatus(completeMessage, 'success', true, true);						}					}).fail(function (code3, reslt3) {						showStatus("Failed on third move (" + 							intermediateTitle + " → " +							params.currTitle.title + ")! " + (reslt3.error.info || (code3 + ".")), 'error', true, true);					}); }).fail(function (code2, reslt2) { showStatus("Failed on second move (" + 						params.currTitle.title + " → " + 						params.destTitle.title + ")! " + 						(reslt2.error.info || (code2 + ".")),						'error', true, true); });			}).fail(function (code1, reslt1) {				showStatus("Failed on first move (" + 					params.destTitle.title + " → " +					intermediateTitle + ")! " + (reslt1.error.info || (code1 + ".")), 'error', true, true);			}); }	}

/**	 * Given two titles, normalizes, does prerequisite checks for talk/subpages, * prompts user for config before swapping the titles */	function roundrobin { // get ns info (nsData.query.namespaces) params.nsData = {}; try { params.nsData = $.ajax({ 				url: mw.util.wikiScript('api'), async:false,				error: function (jsondata) { showConfirm("Unable to get info about namespaces", 'error'); },				data: { action:'query', format:'json', meta:'siteinfo', siprop:'namespaces' }			}).responseJSON.query.namespaces; } catch (error) { console.error(error); showConfirm("Unable to get info about namespaces", 'error'); return; }		// validate namespaces, not identical, can move var vData = swapValidate; if (!vData.valid) { showConfirm(vData.invalidReason, 'error'); return; } if (vData.addlInfo !== undefined) { showConfirm(vData.addlInfo, 'warning'); }

// subj subpages var currSp = getSubpages(params.nsData, vData.currTitle, vData.currNs, false); if (currSp.error !== undefined) { showConfirm(currSp.error, 'error'); return; } var currSpFlags = printSubpageInfo(vData.currTitle, currSp); var destSp = getSubpages(params.nsData, vData.destTitle, vData.destNs, false); if (destSp.error !== undefined) { showConfirm(destSp.error, '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(params.nsData, vData.currTitle, vData.currNs, true); if (currTSp.error !== undefined) { showConfirm(currTSp.error, 'error'); return; } var currTSpFlags = printSubpageInfo(vData.currTalkName, currTSp); var destTSp = getSubpages(params.nsData, vData.destTitle, vData.destNs, true); if (destTSp.error !== undefined) { showConfirm(destTSp.error, '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);

// 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)) { console.log("Subpage move check OK"); } else if (subpageCollision) { params.moveSubpages = false; showConfirm("One namespace does not have subpages enabled. Disallowing move subpages",				'warning'); }		if (params.moveSubpages) { params.allSpArr = currSpFlags.spArr.concat(				destSpFlags.spArr,				currTSpFlags.spArr,				destTSpFlags.spArr			); } else { params.allSpArr = []; }		// TODO: count subpages and make restrictions? if (vData.checkTalk && (!vTData.currTDNE || !vTData.destTDNE || params.moveSubpages)) { if (vTData.allowMoveTalk) { console.log("Talk move check OK"); } else { params.moveTalk = false; showConfirm("Disallowing moving talk. " +					(!vTData.currTCanCreate ? (vData.currTalkName + " is create-protected. ") : (!vTData.destTCanCreate ? (vData.destTalkName + " is create-protected. ")					: "Talk page is immovable.")), 'warning'); }		}		showConfirm("Swapping "+params.currTitle.title+" → "+params.destTitle.title+""); showConfirm("Reason: "+params.moveReason); if (debug) { showConfirm("Move talk: "+params.moveTalk+", Move subpages: "+params.moveSubpages); showConfirm("Talk redirect: "+params.talkRedirect+				", Fix self-redirect: "+params.fixSelfRedirect); }		if (params.moveSubpages && params.allSpArr.length <= 0) { showConfirm("No subpages found to move."); }		showConfirm('', 'notice', true); psSwap.setDisabled(false).setLabel(config.confirmButton).off('click').on('click', function {				psSwap.setDisabled(true).setLabel(config.swapButton);				swapPages(vData, vTData);			}); }

function titleInput(title) { var nsObj = {value: title.ns || 0, $overlay: true}; var tObj = {value: title.title || '', $overlay: true}; if (typeof title.ns !== 'undefined' && typeof title.title !== 'undefined') { var re = '^'+mw.config.get("wgFormattedNamespaces")[title.ns]+':'; tObj.value = title.title.replace(new RegExp(re),''); }		return new mw.widgets.ComplexTitleInputWidget({namespace: nsObj, title: tObj}); }	function assembleTitle(field) { if (field.namespace.value == 0) { return {ns: 0, title: field.title.value}; } return { ns: field.namespace.value, title: mw.config.get("wgFormattedNamespaces")[field.namespace.value]+":"+field.title.value };	}	/**	 * Determine namespace of title */	function psParseTitle(title) { var ns, titleMain; title = title.replace("_"," "); for (var k in mw.config.get("wgFormattedNamespaces")) { var nsName = mw.config.get("wgFormattedNamespaces")[k], match = title.match(new RegExp("^"+nsName+":(.*)$","i")); if (match) { ns = k;				titleMain = match[1]; break; }		}		var ret = {ns: (ns || 0), title: title, titleMain: (titleMain || title)}; return ret; }	/**	 * If user is able to perform swaps */	function checkUserPermissions { var ret = {}; ret.canSwap = true; var reslt = $.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' }		}).responseJSON.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; }	/**	 * Script execution starts here: */	//Read the old title from the URL or the relevant pagename params.currTitle.title = mw.util.getParamValue("wpOldTitle") || mw.config.get("wgRelevantPageName"); if (document.getElementsByName("wpOldTitle")[0] &&		document.getElementsByName("wpOldTitle")[0].value != ''	){ //If the hidden form field element has a value, use that instead params.currTitle.title = document.getElementsByName("wpOldTitle")[0].value; }	//Parse out title and namespace params.currTitle = psParseTitle(params.currTitle.title); //Read the new title from the URL or make it blank params.destTitle.title = mw.util.getParamValue("wpNewTitle") || ''; //Parse out title and namespace params.destTitle = psParseTitle(params.destTitle.title); if (document.getElementsByName("wpNewTitleMain")[0] &&		document.getElementsByName("wpNewTitleMain")[0].value != '' &&		document.getElementsByName("wpNewTitleNs")[0]	){ //If the Move page form exists, use the values from that instead params.destTitle.title = document.getElementsByName("wpNewTitleMain")[0].value; params.destTitle.ns = document.getElementsByName("wpNewTitleNs")[0].value; if (params.destTitle.ns != 0) { params.destTitle.title = mw.config.get("wgFormattedNamespaces")[params.destTitle.ns] + ":" + params.destTitle.title; }	}	params.uPerms = checkUserPermissions; if (!params.uPerms.canSwap) { mw.loader.using( [ 'mediawiki.notification' ], function {			mw.notify("User rights insufficient for action.", { title: 'Page Swap Error', type: 'error' });			return;		} ); }	$( '#firstHeading' ).text(function(i, t) {return t.replace('Move', 'Swap');}); document.title = document.title.replace("Move", "Swap"); new mw.Api.parse(config.introText).done(function (parsedText) {		$( '#movepagetext' ).replaceWith( $($.parseHTML(parsedText)) );	}).fail(function (codetr, reslttr) {		console.warn( "Error parsing wikitext:\n\n" + config.introText + "\n\n" + (reslttr.error.info || (codetr + ".")) );		$( '#movepagetext' ).html( config.introText );	}); var reasonList = []; if ($( '#wpReasonList' )[0]) { reasonList.push({			data: $( '#wpReasonList' ).children("option").get(0).value,			label: $( '#wpReasonList' ).children("option").get(0).text		}); reasonList.push({optgroup: $( '#wpReasonList' ).children("optgroup").get(0).label}); $( '#wpReasonList' ).children("optgroup").children("option").get.forEach(			option => reasonList.push({data: option.value, label: option.text})		); }	var psFieldset = new OO.ui.FieldsetLayout({			label: 'Swap page', classes: ['container'], id: 'psFieldset'		}), psOldTitle = titleInput(params.currTitle), psNewTitle = titleInput(params.destTitle), psReasonList = new OO.ui.DropdownInputWidget({			options: reasonList, id: 'psReasonList', $overlay: true		}), psReasonOther = new OO.ui.TextInputWidget({value: moveReason, id: 'psReasonOther'}), psMovetalk = new OO.ui.CheckboxInputWidget({selected: params.defaultMoveTalk, id: 'psMovetalk'}), psMoveSubpages = new OO.ui.CheckboxInputWidget({selected: true, id: 'psMoveSubpages'}), psTalkRedirect = new OO.ui.CheckboxInputWidget({selected: params.cleanup, id: 'psTalkRedirect'}), psFixSelfRedirect = new OO.ui.CheckboxInputWidget({selected: params.cleanup, id: 'psFixSelfRedirect'}), psWatch = new OO.ui.CheckboxInputWidget({selected: false, id: 'psWatch'}), psConfirm = new OO.ui.MessageWidget({type: 'notice', showClose: false, id: 'psConfirm'}), psSwap = new OO.ui.ButtonInputWidget({			label: config.swapButton,			disabled: true, framed: true,			flags: ['primary','progressive'],			id: 'psSwap'		}), psStatus = new OO.ui.MessageWidget({type: 'notice', showClose: true, id: 'psStatus'}), psContribsButton = new OO.ui.ButtonWidget({			label: 'Open contribs page', title: 'Special:MyContributions',			href: mw.config.get("wgServer") +				mw.config.get("wgArticlePath").replace("$1", "Special:MyContributions"),			framed: true, flags: ['primary', 'progressive'],			id: 'psContribsButton', target: '_blank'		}); psFieldset.addItems( [		new OO.ui.FieldLayout(psOldTitle, {align: 'top', label: 'Old title:', id: 'psOldTitle'}),		new OO.ui.FieldLayout(psNewTitle, {align: 'top', label: 'New title:', id: 'psNewTitle'}),		new OO.ui.FieldLayout(psReasonList, {align: 'top', label: 'Reason:'}),		new OO.ui.FieldLayout(psReasonOther, {align: 'top', label: 'Other/additional reason:'}),		new OO.ui.FieldLayout(psMovetalk, {align: 'inline', label: 'Move associated talk page', title: 'Move associated talk page' }),		new OO.ui.FieldLayout(psMoveSubpages, {align: 'inline', label: 'Move subpages', title: 'Move up to 100 subpages of the source and/or target pages' }),		new OO.ui.FieldLayout(psTalkRedirect, {align: 'inline', label: 'Leave a redirect to new talk page if needed', title: 'If one of the pages you\'re swapping has a talk page and ' + 'the other doesn\'t, create a redirect from the missing talk ' + 'page to the new talk page location. This is useful when ' + 'swapping a page with its redirect so that links to the old ' + 'talk page will continue to work.' }),		new OO.ui.FieldLayout(psFixSelfRedirect, {align: 'inline', label: 'Fix self-redirects', title: 'When swapping a page with its redirect, update the ' + 'redirect to point to the new page name so that it is not ' + 'pointing to itself. This will not update redirects on subpages.' }),		new OO.ui.FieldLayout(psWatch, {align: 'inline', label: 'Watch source page and target page', title: 'Add both source page and target page to your watchlist' }),		new OO.ui.FieldLayout(psConfirm, {}),		new OO.ui.FieldLayout(psSwap, {}),		new OO.ui.FieldLayout(psStatus, {}),		new OO.ui.FieldLayout(psContribsButton, {})		]); function checkTitles { if (psOldTitle.namespace.value%2==1 || psNewTitle.namespace.value%2==1) { if (psMovetalk.isDisabled == false) { psMovetalk.setDisabled(true); params.defaultMoveTalk = psMovetalk.isSelected; psMovetalk.setSelected(false); }		} else if (psMovetalk.isDisabled) { psMovetalk.setDisabled(false); psMovetalk.setSelected(params.defaultMoveTalk); }		psConfirm.toggle(false).setType('notice'); params.currTitle = assembleTitle(psOldTitle); params.destTitle = assembleTitle(psNewTitle); var titlesMatch = (params.currTitle.title==params.destTitle.title); psOldTitle.title.setValidityFlag(psOldTitle.title.value!='' && !titlesMatch ); psNewTitle.title.setValidityFlag(psNewTitle.title.value!='' && !titlesMatch ); psSwap.setLabel(config.swapButton).off('click').on('click', clickSwap 			).setDisabled(psOldTitle.title.value== || psNewTitle.title.value== || titlesMatch ); }	function clickSwap { psConfirm.toggle(false).setType('notice'); psStatus.toggle(false).setType('notice'); psSwap.setDisabled(true); Object.assign(params, params, {			confirmMessages: [],			statusMessages: [],			currTitle: assembleTitle(psOldTitle),			destTitle: assembleTitle(psNewTitle),			moveReason: psReasonOther.value,			moveTalk: psMovetalk.isDisabled ? false : psMovetalk.selected,			moveSubpages: psMoveSubpages.selected,			talkRedirect: psTalkRedirect.selected,			fixSelfRedirect: psFixSelfRedirect.selected,			watch: psWatch.selected ? 'watch' : 'unwatch',		}); if (psReasonList.value != 'other') { params.moveReason = psReasonList.value + (psReasonOther.value ==  ?  : '. ' + psReasonOther.value); } else if (psReasonOther.value == '') { params.moveReason = 'Swap ' + params.currTitle.title + ' and ' +				params.destTitle.title + ' (WP:SWAP)'; }

roundrobin; }

checkTitles;

/**	 * Re-check form on any change */	psOldTitle.namespace.off('change').on( 'change', checkTitles ); psOldTitle.title.setValidation( function(v) {		checkTitles; return (v!='' && params.currTitle.title!=params.destTitle.title);	} ); psNewTitle.namespace.off('change').on( 'change', checkTitles ); psNewTitle.title.setValidation( function(v) {		checkTitles; return (v!='' && params.currTitle.title!=params.destTitle.title);	} ); psReasonList.off('change').on( 'change', checkTitles ); psReasonOther.off('change').on( 'change', checkTitles ); psMovetalk.off('change').on( 'change', checkTitles ); psMoveSubpages.off('change').on( 'change', checkTitles ); psTalkRedirect.off('change').on( 'change', checkTitles ); psFixSelfRedirect.off('change').on( 'change', checkTitles ); psWatch.off('change').on( 'change', checkTitles ); /**	 * Set button and status field actions */	psSwap.off('click').on( 'click', clickSwap ); psStatus.off('close').on( 'close', function {		params.statusMessages = [];		psStatus.setType('notice');		psContribsButton.toggle(false);	} ).off('toggle').on( 'toggle', function {		if (!psStatus.isVisible) {			params.statusMessages = [];			psStatus.setType('notice');			psContribsButton.toggle(false);		}	} ); psConfirm.toggle(false); psStatus.toggle(false); $( '#movepage' ).hide; //hide old form $( '#movepage-loading' ).remove; //remove loading message $( "div.mw-message-box-error" ).hide; //hide error message $( '#psFieldset' ).remove; //remove old form if script started twice $( "div.movepage-wrapper" ).prepend( psFieldset.$element ); //add swap form if( !$( '#psFieldset' ).length ){ //something went wrong mw.notify(config.errorMsg, {type: 'error', title: "Error:" }); document.getElementById("mw-movepage-table").style.display="block"; $( '#movepage' ).show; $( "div.mw-message-box-error" ).show; }

return true; }