User:Ahecht/sandbox/Scripts/pageswap-core.js

//jshint -W083

function pageSwap(prefix, moveReason, debug) { var config = { link: "using Pageswap GUI)",		intermediatePrefix: "Draft:Move/",		portletLink: "Swap (sandbox)" + (debug ? " (debug)" : ""),		portletAlt: "Perform a revision history swap / round-robin move",		validateButton: 'Validate page swap (sandbox)' + (debug ? " (debug)" : ""),		validatingButton: 'Validating page swap (sandbox)' + (debug ? " (debug)" : ""),		confirmButton: 'Confirm (sandbox)' + (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 GUI 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 GUI 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 subpage(s) were moved, and may need new ' + 'or updated redirects:\n', errorMsg: 'Error adding swap form to page!', rrReason: ' (Round-robin swap step 1 ',		newRedirMsg: 'The following redirect(s) will be created or modified '+			'(choose redirect categories):',		rcatShell: ,		rcatDefault: ,		rcatChoose: 'Choose redirect categories for the newly created redirects:',		rcatsAdded: '* The following redirect categories will be added where possible: ',		rcatCat: 'Category:Redirect templates',		rcatTempNSRegEx: new RegExp("\\|\\s*(\\S*?) category\\s*=", "g"),		types: ['notice', 'success', 'warning', 'error'],	}, params = {		apiData: {}, currTitle: {}, destTitle: {},		confirmMessages: [], statusMessages: [], selfRedirs: [], rcats: [],		selectedRcats: { [config.rcatDefault]: ["all"] },		defaultMoveTalk: true, confDone: false, editRedir: false, done: false,		busy: 0, 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, type, idempotency) {		if (config.types.indexOf(type) > config.types.indexOf(container.type)) {			container.setType(type);		}		label = new OO.ui.HtmlSnippet(label);		if (idempotency == params.idempotency[container.elementId]) {			container.setLabel(label).toggle(true).scrollElementIntoView.always( => { $( 'a[href="#rcat"]' ).off('click').on('click', (e) => {					e.preventDefault;					mw.loader.load('https://tools-static.wmflabs.org/cdnjs/ajax/libs/select2/4.0.13/css/select2.min.css', 'text/css');					mw.loader.getScript('https://tools-static.wmflabs.org/cdnjs/ajax/libs/select2/4.0.13/js/select2.min.js').then( => { if (params.rcats.length == 0) { getRcats; } else { showRcatDialog; }					} );					return false;				}); if (psContribsButton.isVisible && !psContribsButton.isDisabled) { psContribsButton.scrollElementIntoView; } else if (psButton.isVisible && !psButton.isDisabled) { psButton.scrollElementIntoView; }			} );		}	}	function parseError(ps, label, codetr, reslttr, idempotency) {			label = "Error parsing wikitext:\n\n" + label + "\n\n" +				(reslttr.error.info || (codetr + "."));			console.warn(label);			setLabel(ps, label, 'error', idempotency);		}	function showConfirm(message, type='notice', done=false) {		if (done) params.confDone = true;		var idempotency = ++params.idempotency.psConfirm;

if (message !== '') { params.confirmMessages.push(message.replace("WP:RM/TR", "WP:RM/TR")); }		var label = config.confirmMessageHeader + params.confirmMessages.join("\n*") + (params.confDone ? config.confirmMessageFooter : ''); new mw.Api.parse(label).done(	(parsedText) => {			setLabel(psConfirm, filterHtml(parsedText), type, idempotency);		} ).fail( (codetr, reslttr) => 			parseError(psConfirm, label, codetr, reslttr, idempotency)		); if (type=='error') psProgress.toggle(false); }	function showStatus(message, type='notice', done=false, topic=false) { var idempotency = ++params.idempotency.psStatus;

if (done) params.done = true; if (message !== '') { var topicFlag = topic ? "" : false; var topicIndex = params.statusMessages.findIndex((str) => str.indexOf(topicFlag) > -1); message = "*" + 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 && params.busy == 0) { 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) + "."; }			type = 'success'; }		var label = config.statusMessageHeader + params.statusMessages.join('') + doneSubpagesMessage + doneMessage; new mw.Api.parse(label).done(			(parsedText) => setLabel(psStatus, filterHtml(parsedText), type, idempotency)		).fail(			(codetr, reslttr) => parseError(psStatus, label, codetr, reslttr, idempotency)		); }	function parsePagesData { // get page data, normalize titles var ret = {valid: true, invalidReason: ''}; var query = params.apiData; if (typeof query.pages !== 'undefined' && typeof query.logevents !== 'undefined') { for (var kn in query.normalized) { var qn = query.normalized[kn]; if (params.currTitle.title == qn.from) { params.currTitle.title = qn.to; } else if (params.destTitle.title == qn.from) { params.destTitle.title = qn.to; }			}			for (var kp in query.pages) { var qp = query.pages[kp]; if ([params.currTitle.title,params.destTitle.title].includes(qp.title)) { if (params.currTitle.title == qp.title) { params.currTitle = qp; } else if (params.destTitle.title == qp.title) { params.destTitle = qp; }					if (kp < 0) { ret.valid = false; if (typeof qp.missing !== 'undefined') { ret.invalidReason += "Unable to find "+qp.title+". "; } else if (typeof qp.invalid !== 'undefined' &&							typeof qp.invalidreason !== 'undefined') { ret.invalidReason += qp.invalidreason; } else { ret.invalidReason += "Unable to get page data for"+params.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) + " hour(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"+params.titlesString}; }

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(ret) { // get page data, normalize titles if (ret.valid === false || params === null ||			params.currTitle.title === null || params.destTitle.title === 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; }			ret[k]                     = params[k].title; ret[k.slice(0,4)+"Ns"]     = params[k].ns; ret[k.slice(0,4)+"CanMove"] = params[k].actions.move === ''; ret[k.slice(0,4)+"IsRedir"] = 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.apiData.namespaces[ + ret.currNs].subpages !== ; ret.destNsAllowSubpages = params.apiData.namespaces[ + 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; ret.currTitleWithoutPrefix = mw.Title.newFromText( ret.currTitle ).title; ret.currTalkName = mw.Title.newFromText( ret.currTitle ).getTalkPage.getPrefixedText; ret.destTitleWithoutPrefix = mw.Title.newFromText( ret.destTitle ).title; ret.destTalkName = mw.Title.newFromText( ret.destTitle ).getTalkPage.getPrefixedText; }		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 = {allowMoveTalk: true}; if (!checkTalk) return ret; // currTitle destTitle already talk pages if (talk1 === undefined || talk2 === undefined) ret.allowMoveTalk = false; ret.currTDNE = true; ret.destTDNE = true; ret.currTCanCreate = true; ret.destTCanCreate = true; var talkTitleArr = [talk1, talk2]; if (talkTitleArr.length !== 0 && typeof params.apiData?.pages !== 'undefined') { var talkData = params.apiData.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 { ret.allowMoveTalk = false; }		if (!ret.allowMoveTalk) { showStatus("Unable to validate talk. Disallowing movetalk to be safe.", 'warning'); } else { ret.allowMoveTalk = (ret.currTCanCreate && ret.currTCanMove) && (ret.destTCanCreate && ret.destTCanMove); }		if (params.moveTalk && params.talkRedirect) { if (ret.currTDNE && !ret.destTDNE) { ret.redirFromTalk = talk2; ret.redirToTalk = talk1; } else if (ret.destTDNE && !ret.currTDNE) { ret.redirFromTalk = talk1; ret.redirToTalk = talk2; }		}		return ret; }

/**	 * Given existing title (not prefixed with "/"), optionally searching for talk, *  finds subpages (incl. those that are redirs) and whether limits are exceeded */	function getSubpages(title, isTalk) { var deferred = $.Deferred; var titleObj = isTalk ? mw.Title.newFromText( title ).getTalkPage : mw.Title.newFromText( title ); var nsSubpages = params.apiData.namespaces['' + titleObj.namespace].subpages; if ((!titleObj.isTalkPage) && nsSubpages !== '') { deferred.resolve( [] ); } else { var queryData = { format:'json', action:'query', prop:'info', intestactions:'move|create', generator:'allpages', gapprefix:titleObj.title + '/', gapnamespace:titleObj.namespace, gaplimit:101, };			new mw.Api.get(queryData).done( (subpages) => {				if ( typeof subpages !== 'object' ) {					deferred.reject( "API did not return data for subpages of "+title+". Subpages may exist." );				} else if (typeof subpages?.query?.pages === 'undefined') {					if (subpages.batchcomplete === '') { //no subpages found						deferred.resolve( [] );					} else { //something else went wrong						console.warn("API did not return 'pages' when querying subpage data:");console.log(subpages);						deferred.reject( "API did not return subpage data for "+title+". Subpages may exist." );					}				} else if (Object.keys(subpages.query.pages).length > 101) {					deferred.reject( "100+ subpages of "+title+". Aborting" );				} else {					subpages = subpages.query.pages;					var dataret = [];					for (var k in subpages) {						dataret.push( { title:subpages[k].title, isRedir:subpages[k].redirect === '', canMove:subpages[k].actions.move === '' } );					}					deferred.resolve( dataret );				}			} ).fail( (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);				deferred.reject( errStr+" Subpages may exist." );			} );		}		return deferred.promise; }

/**	 * 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) { if (!currSp[kcs].canMove) currSpCannotMove.push(currSp[kcs].title); currSpArr.push(currSp[kcs].title); if (currSp[kcs].isRedir) redirCount++; }

if (params.moveSubpages) { if (currSpArr.length > 0) { if (currSpCannotMove.length > 0) { showConfirm("Disabling move-subpages." +						"The following " + currSpCannotMove.length + " (of " + currSpArr.length + ") total subpage(s) 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; }	var filterRcats = (ns) => ( Object.keys(params.selectedRcats).filter( (e) => ( params.selectedRcats[e].some( (v) => (v == 'all' || v == 'other' || v == 'unknown' || v == ns) ) )	) );	function createMissingTalk(vData, vTData) { var fromTalk = vTData.redirFromTalk, toTalk = vTData.redirToTalk; if (fromTalk && toTalk) { params.busy++; setTimeout( => {				var talkRedirect = {					action:'edit',					title:fromTalk,					createonly: true,					text: "#REDIRECT " + toTalk + "\n\n" +						config.rcatShell.replace( '$1', filterRcats('talk').join('\n') ),					summary: "Create redirect to " + toTalk + " (" + config.link, watchlist: params.watch };				showStatus("Creating talk page redirect " + fromTalk +					" → " + toTalk + "...",'notice', false,					"TPR" + fromTalk); if (debug) { params.busy--; showStatus("* Talk page redirect simulated!.",						'notice', true, "TPR" + fromTalk); } else { new mw.Api.postWithEditToken(talkRedirect).done( => {						params.busy--;						showStatus("* Talk page redirect created!", 'notice', true, "TPR" + fromTalk);					} ).fail( (codetr, reslttr) => {						params.busy--;						showStatus("* Failed to create redirect! " + (reslttr.error.info || (codetr + ".")), 'error', true, "TPR" + fromTalk);					} ); }			}, 250);		} else { showStatus('', 'notice', true); }	}

function retargetRedirect(thisPage, otherPage, newText) { params.busy++; showStatus("Retargeting redirect at " + thisPage +			" to "	+ otherPage + "...",			'notice', false, "RT"+thisPage); var retargetData = { action:'edit', title: thisPage, text: newText, summary : "Retarget redirect to " + otherPage + " (" +					config.link,				watchlist: params.watch			};		if (debug) {			params.busy--;			showStatus("* Retargeting simulated!",'notice', false, "RT"+thisPage);		} else {			new mw.Api.postWithEditToken(retargetData).done( (result, jqXHR) => { params.busy--; if (typeof result.edit !== 'undefined') { params.busy++; new mw.Api.get( {						action: 'query', prop: , redirects: ,						titles: result.edit.title					} ).done( (data) => {						params.busy--;						if (data && typeof data?.query?.redirects !== 'undefined') {							showStatus("* Redirect retargeted!", 'notice', false, "RT"+thisPage);						} else {							console.warn("Error parsing redirects after retargeting:");							console.warn(data);						}					} ).fail( (codeart, rsltart) => {						params.busy--;						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( (codert, resultrt) => { params.busy--; showStatus("* Retargeting failed. "+				(resultrt.error.info || (codert + ".")),				'error', false, "RT"+thisPage); } );		}	}

function preCheckSelfRedirs(vData) { var pagesArr = [vData.currTitle, vData.destTitle, vData.currTalkName,	vData.destTalkName]; var redirs = params.apiData.redirects; for (const e in redirs) { var thisI = pagesArr.indexOf(redirs[e].from); if (thisI > -1) { var otherI = (thisI==0)?1:((thisI==1)?0:((thisI==2)?3:2)); var otherPage = pagesArr[otherI]; if(redirs[e].to == otherPage) params.selfRedirs.push(redirs[e].to); } else { showConfirm('Page ' + redirs[e].from + ' from redirects table not found in input data.', 'warning'); }		}	}

/**	 * After successful page swap, post-move cleanup: * Make talk page redirect * TODO more reasonable cleanup/reporting as necessary * vData.(curr|dest)IsRedir */	function checkSelfRedirs(vData, vTData) { var pagesArr = [vData.currTitle, vData.destTitle, vData.currTalkName,	vData.destTalkName]; var srQuery = { action: "query", formatversion: "2", prop: "revisions|templates", titles: pagesArr.filter(				(v) => params.selfRedirs.includes(v)			).join('|'), rvprop: "content", rvslots: "main", rvsection: "", tlnamespace: "10", tllimit: "max" };		params.busy++; new mw.Api.get( srQuery ).done( (queryData) => {			params.busy--;			if (queryData && queryData?.query?.pages?.[0]?.revisions[0] ) {				queryData.query.pages.forEach( (pageData) => { var thisPage = pageData.title; var thisI = pagesArr.indexOf(thisPage); var otherI = (thisI==0)?1:((thisI==1)?0:((thisI==2)?3:2)); var otherPage = pagesArr[otherI]; var oldText = pageData?.revisions?.[0]?.slots?.main.content; oldText = oldText ?? '';					var redirRE = new RegExp(						"^\\s*#REDIRECT\\s*\\[\\[ *.* *\\]\\]", "i"					); if ((thisI > -1) && (oldText.search(redirRE) > -1)) { var pageRcats = []; if (pageData?.templates) { pageData.templates.forEach( (v) => {								v = v.title;								params.rcats.some( (e) => { if (e.id == v) return pageRcats.push(e.text), true; } );							} );						}						var oldRcatL = pageRcats.length; pageRcats = pageRcats.concat( //combine and dedupe							Object.keys(params.selectedRcats)						).filter((v, i, a) => a.indexOf(v) === i); var thisNs = mw.Title.newFromText(thisPage).getNamespaceId; thisNs = (thisNs == 0) ? 'main' : ( (thisNs % 2 == 1) ? 'talk' :							mw.config.get('wgFormattedNamespaces')[thisNs].toLowerCase ); var newText = ""; if ( (pageRcats.length > 0) && ( (oldText.search('{'+'{') == -1) || (pageRcats.length != oldRcatL) ) ) { // Completely replace redirect text newText = '#REDIRECT '+otherPage+'\n\n' + config.rcatShell.replace('$1',								filterRcats(thisNs).join('\n')); } else { // Just change target newText = oldText.replace(redirRE,								'#REDIRECT '+otherPage+''); }						retargetRedirect(thisPage, otherPage, newText); } else { showStatus("Attempt to retarget " +							"redirect at " + thisPage + 							" to " + otherPage + " " +							"failed: String not found.",							'warning'); }				} );			} else {				params.busy--;				showStatus("Attempt to retarget redirect " + "at " + thisPage + " to " +					otherPage + " failed: " + "Could not fetch contents.", 'error');			}		} ).fail( (jqXHR, textStatus) => {			params.busy--;			showStatus("Attempt to retarget redirect at " +				pagesArr[i] + " failed due to API error '" + (jqXHR.status||textStatus) + "' when " + "fetching page contents. ", 'error');		} ).always( => createMissingTalk(vData, vTData) ); }

/**	 * Swaps the two pages (given all prerequisite checks) * Optionally moves talk pages and subpages */	function swapPages(vData, vTData) { params.busy = 1; 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 currTitle = params.currTitle.title; var intermediateTitle = config.intermediatePrefix + currTitle; var destTitle = params.destTitle.title; if (debug) { showStatus("Simulating round-robin history swap..."); showStatus("* Step 1 (" + destTitle + " → " + 				intermediateTitle + ")..."); showStatus("* Step 2 (" + currTitle + " → " + 				destTitle + ")..."); showStatus("* Step 3 (" + intermediateTitle + " → " + 				currTitle + ")..."); var completeMessage = "* Round-robin history swap of " +				currTitle + " (links) and " +				destTitle + " (links) simulated successfully!"; if (params.fixSelfRedirect || params.talkRedirect) { showStatus(completeMessage); params.busy--; if (params.fixSelfRedirect && params.selfRedirs.length > 0) { checkSelfRedirs(vData, vTData); } else { createMissingTalk(vData, vTData); }			} else { params.busy--; showStatus(completeMessage, 'notice', true); }		} else { showStatus("Doing round-robin history swap..."); var mQuery = { action:'move', from:destTitle, to:intermediateTitle, reason:params.moveReason + config.rrReason + config.link, watchlist:params.watch, noredirect:1 }; if (params.moveTalk) mQuery.movetalk = 1; if (params.moveSubpages) mQuery.movesubpages = 1; showStatus("* Step 1 (" + mQuery.from + " → " + 				mQuery.to + ")..."); new mw.Api.postWithEditToken(mQuery).then( => {				Object.assign(mQuery, { from:currTitle, to:destTitle, reason: params.moveReason + " (" + config.link } ); showStatus("* Step 2 (" + mQuery.from + " → " + 					mQuery.to + ")..."); return new mw.Api.postWithEditToken(mQuery); } ).then( => { Object.assign(mQuery, { from:intermediateTitle, to:currTitle,					reason: params.moveReason + config.rrReason.slice(0,-2) +						"3 " + config.link } ); showStatus("* Step 3 (" + mQuery.from + " → " + 					mQuery.to + ")..."); return new mw.Api.postWithEditToken(mQuery); } ).then( => { var completeMessage = "* Round-robin history swap of " +					currTitle + " (links) and " + destTitle + 					" (links) completed successfully!"; if (params.fixSelfRedirect || params.talkRedirect) { showStatus(completeMessage); params.busy--; if (params.fixSelfRedirect && params.selfRedirs.length > 0) { checkSelfRedirs(vData, vTData); } else { createMissingTalk(vData, vTData); }				} else { params.busy--; showStatus(completeMessage, 'notice', true); }			} ).fail( (code, reslt) => { params.busy--; showStatus("* Failed when moving (" + mQuery.from + " → " +					mquery.to + ")! " + (reslt.error.info || (code + ".")),					'error', true); } );		}	}	/**	 * Prompt for redirect categories for newly created redirects 	*/	function showRcatDialog {		var select = $( ' ' ).attr( 'id', 'rcat-chooser-form' ).attr('multiple', 'multiple').append( $( ' ' ).attr( 'selected', 'selected' ).attr(				'value', config.rcatDefault.replace(/\{\{(.*)\}\}/, "Template:$1")			).text( config.rcatDefault ) );		var content = $( ' ' ).append( ' ' + config.rcatChoose + ' ' ).append( select );

// Subclass ProcessDialog. function ProcessDialog( config ) { ProcessDialog.super.call( this, config ); }		OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog ); ProcessDialog.static.name = 'rcatDialog'; ProcessDialog.static.title = 'Select Redirect Categories'; ProcessDialog.static.actions = [ {				action: 'save', label: 'Save', flags: [ 'primary', 'progressive' ] },			{				label: 'Cancel', flags: [ 'safe', 'close' ] }		];		ProcessDialog.prototype.initialize = function { ProcessDialog.super.prototype.initialize.apply( this, arguments ); this.content = new OO.ui.PanelLayout( {				padded: true,				expanded: false			} ); this.content.$element.append( content ); params.rcats.forEach( (v, i, a) => {a[i].selected = Object.keys(params.selectedRcats).includes(v.text);} ); select.select2({data: params.rcats, width: '100%'}).on( 'change', => {rcatDialog.updateSize;} ); this.$body.append( this.content.$element ); };		ProcessDialog.prototype.getActionProcess = function ( action ) { if ( action ) { if (action == 'save') params.selectedRcats = {}; if (action == 'save' && select.val.length > 0) { new mw.Api.get( {						"action": "query", "prop": "revisions", "formatversion": 2,						"titles": select.val.join('|'),						"rvprop": "content", "rvslots": "main"					} ).done( (data) => {						if (data && data?.query?.pages?.[0]) {							data.query.pages.forEach( (page) => { var pageContent = page?.revisions?.[0]?.slots?.main?.content; if (typeof pageContent === "string") { var tempCall = page.title.replace(/Template:(.*)/, '{'+'{$1}}'); var nsMatches = Array.from(										pageContent.matchAll(config.rcatTempNSRegEx),										(v) => (v[1])									); if (nsMatches.length == 0) nsMatches = ['unknown']; params.selectedRcats[tempCall] = nsMatches; }							} );						}						if (Object.keys(params.selectedRcats).length > 0) {							showConfirm(config.rcatsAdded + " ");						}					} ).fail( (jqXHR, textStatus) => {						showConfirm("* API error '"+(jqXHR.status||textStatus)+ "' when verifying Rcat templates.", 'error');					} ); }				return new OO.ui.Process(					 => this.close( {action: action} )				); }			return ProcessDialog.super.prototype.getActionProcess.call( this, action ); };		ProcessDialog.prototype.getBodyHeight = function { return this.content.$element.outerHeight( true ); };		// Create and append the window manager and rcat dialog var windowManager = new OO.ui.WindowManager; $( document.body ).append( windowManager.$element ); var rcatDialog = new ProcessDialog( {size: 'large'} ); windowManager.addWindows( [rcatDialog] ); windowManager.openWindow( rcatDialog ); // Workaround for lack of openOnEnter option in Select2 v4		var select2 = select.data('select2'); var origKeypressCbs = select2.listeners.keypress; var keypressCb = function (evt) { if (evt.key === 'Enter' && !select2.isOpen) { rcatDialog.executeAction('save'); return; }			origKeypressCbs.forEach( (cb) => {cb(evt);} ); };		select2.listeners.keypress = [keypressCb]; }

/**	 * Retrieve templates from "Category:Redirect templates" */	function getRcats(cont=, cmcont=) { var query = { action:'query', list:'categorymembers', cmlimit:'max', cmtitle: config.rcatCat, cmsort:'sortkey', cmnamespace:10, cmtype:'page', cmprop: 'title|sortkeyprefix', cmcontinue:cmcont, continue:cont };		new mw.Api.get( query ).done( (result) => {			if (result?.query?.categorymembers) {				result.query.categorymembers.forEach( (e) => { var tTitle = mw.Title.newFromText(e.title).getMainText; if ( tTitle.startsWith('R ') ) { var sKey = e.sortkeyprefix.trim == '' ? tTitle.replace(/^R (from |to |with )?/, '') : e.sortkeyprefix; params.rcats.push( {sKey: sKey, id: e.title,							text: '{' + '{' + tTitle + '}}'} ); }				} );				if (result.continue) {					getRcats(result.continue.continue, result.continue.cmcontinue);				} else {					params.rcats.sort( (a, b) => { return (a.sKey > b.sKey) ? 1 : ((a.sKey == b.sKey) ? 0 : -1 ); } );					showRcatDialog;				}			} else {console.warn('error');console.log(result);}		} ).fail( (e) => {console.warn(e)} ); }

/**	 * Given two titles and talk/subpages, * prompts user to confirm config before swapping the titles */	function confirmConfig(vData, currSpFlags, destSpFlags, currTSpFlags, destTSpFlags) { 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 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 (params.moveSubpages) { if (!subpageCollision && !noSubpages && vData.allowMoveSubpages &&				(currSpFlags.allowMoveSubpages && destSpFlags.allowMoveSubpages) &&				(currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages)) {				params.allSpArr = currSpFlags.spArr.concat(					destSpFlags.spArr,					currTSpFlags.spArr,					destTSpFlags.spArr				); } else if (subpageCollision) { params.moveSubpages = false; showConfirm("One namespace does not have subpages enabled. Disallowing move subpages.",					'warning'); }		} else { showConfirm("Moving subpages disabled."); }		params.allSpArr = params.allSpArr ?? [];		// TODO: count subpages and make restrictions? if (vData.checkTalk && (!vTData.currTDNE || !vTData.destTDNE || params.moveSubpages)) { if (!vTData.allowMoveTalk) { 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."); if (params.fixSelfRedirect && params.apiData?.redirects) preCheckSelfRedirs(vData); if ( (params.selfRedirs.length > 0) || 			(vTData.redirFromTalk && vTData.redirToTalk) ) { params.editRedir = true; showConfirm(config.newRedirMsg); if (vTData.redirFromTalk && vTData.redirToTalk) { showConfirm("* Redirect from " + vTData.redirFromTalk +					" → " +	vTData.redirToTalk + " will be created."); }			for (const t in params.selfRedirs) { showConfirm("* Self-redirect at " + params.selfRedirs[t] + 				" will be re-targeted."); }		}		psProgress.toggle(false); showConfirm('', 'notice', true); psButton.setDisabled(false).setLabel(config.confirmButton).off('click').on('click', => {				psButton.setDisabled(true).setLabel(config.validateButton);				swapPages(vData, vTData);			} ); }

/**	 * Given two titles, gathers data on talk/subpages, * then passes that to confirmConfig */	function gatherSubpageData { var currSpFlags, destSpFlags, currTSpFlags, destTSpFlags; // validate namespaces, not identical, can move var ret = parsePagesData; const vData = swapValidate(ret); if (!vData.valid) { showConfirm(vData.invalidReason, 'error'); return; }		if (vData.addlInfo !== undefined) showConfirm(vData.addlInfo, 'warning'); // subj subpages getSubpages(vData.currTitle, false).done( (cData) => {			currSpFlags = printSubpageInfo(vData.currTitle, cData);			return getSubpages(vData.destTitle, false);		} ).then( (dData) => {			destSpFlags = printSubpageInfo(vData.destTitle, dData);			// talk subpages			return getSubpages(vData.currTitle, true);		} ).then( (cTData) => {			currTSpFlags = printSubpageInfo(vData.currTalkName, cTData);			return getSubpages(vData.destTitle, true);		} ).then( (dTData) => {			destTSpFlags = printSubpageInfo(vData.destTalkName, dTData);			confirmConfig(vData, currSpFlags, destSpFlags, currTSpFlags, destTSpFlags);		} ).fail( (error) => showConfirm(error, 'error') ); }	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} ); }	/**	 * Determine namespace of title */	function psParseTitle(data) { data = (typeof data === 'object') ? mw.Title.makeTitle(data.namespace.value, data.title.value) : mw.Title.newFromText(data); return data ? {ns: data.namespace, title: data.getPrefixedText} : null; }	/**	 * 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: (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) || {ns: 0, title: 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) || {ns: 0, title: 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' ], => {			mw.notify("User rights insufficient for action.", { title: 'Page Swap Error', type: 'error' } );			return;		} ); }	$( '#firstHeading' ).text( (i, t) => (t.replace('Move', 'Swap')) ); document.title = document.title.replace("Move", "Swap"); new mw.Api.parse(config.introText).done( (parsedText) => {		$( '#movepagetext' ).replaceWith( $($.parseHTML(parsedText)) );	} ).fail( (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'} ), psButton = new OO.ui.ButtonInputWidget( {			label: config.validateButton,			disabled: true, framed: true,			flags: ['primary','progressive'],			id: 'psButton'		} ), psProgress = new OO.ui.ProgressBarWidget( {progress: false} ), 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(psButton, {} ),		new OO.ui.FieldLayout(psProgress, {} ),		new OO.ui.FieldLayout(psStatus, {} ),		new OO.ui.FieldLayout(psContribsButton, {} )		]); checkTitles;

/**	 * Re-check form on any change */	psOldTitle.namespace.off('change').on( 'change', checkTitles ); psOldTitle.title.setValidation( (v) => {		checkTitles; return (v!='' && params.currTitle.title!=params.destTitle.title);	} ); psNewTitle.namespace.off('change').on( 'change', checkTitles ); psNewTitle.title.setValidation( (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 */	psButton.off('click').on( 'click', clickValidate ); psStatus.off('close').on( 'close', => {		params.statusMessages = [];		psStatus.setType('notice');		psContribsButton.toggle(false);	} ).off('toggle').on( 'toggle',  => {		if (!psStatus.isVisible) {			params.statusMessages = [];			psStatus.setType('notice');			psContribsButton.toggle(false);		}	} ); psConfirm.toggle(false); psProgress.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; }	var ulStyle = document.createElement('style'); // Even spacing in lists ulStyle.innerHTML = '.oo-ui-labelElement-label ul li ul {margin-top: 0.1em;}'; document.head.appendChild(ulStyle);

/**	 * Helper functions that rely on above form elements */	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 = psParseTitle(psOldTitle); params.destTitle = psParseTitle(psNewTitle); var titlesMatch = (params.currTitle?.title==params.destTitle?.title); psOldTitle.title.setValidityFlag(params.currTitle && !titlesMatch ); psNewTitle.title.setValidityFlag(params.destTitle && !titlesMatch ); psButton.setLabel(config.validateButton).off('click').on('click', clickValidate			).setDisabled(psOldTitle.title.value== || psNewTitle.title.value== || titlesMatch ); }	function clickValidate { psConfirm.toggle(false).setType('notice'); psStatus.toggle(false).setType('notice'); psButton.setDisabled(true).setLabel(config.validatingButton); psProgress.toggle(true); Object.assign(params, params, {			confirmMessages: [],			statusMessages: [],			currTitle: psParseTitle(psOldTitle),			destTitle: psParseTitle(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 (!params.currTitle) { showConfirm("Title '" + psOldTitle + "' is invalid.", 'error'); return; } else if (!params.destTitle) { showConfirm("Title '" + psNewTitle + "' is invalid.", 'error'); return; }		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)'; }		var queryTitleArr = [params.currTitle.title, params.destTitle.title]; queryTitleArr.forEach(				(v) => queryTitleArr.push(mw.Title.newFromText( v ).getTalkPage.getPrefixedText)			); params.titlesString = " " + queryTitleArr.join(' or ') + ""; var queryData = {action:'query', format:'json', titles: queryTitleArr.join('|'), prop:'info', intestactions:'move|create', list:'logevents', leprop:'timestamp', letype:'move', letitle: params.currTitle.title, lelimit:'1', meta:'siteinfo', siprop:'namespaces' };		new mw.Api.get( queryData	).then( (data) => {			if (data && data?.query?.namespaces) params.apiData = data.query;			return new mw.Api.get( { action:'query', format:'json', redirects:'', titles: queryTitleArr.join('|') } );		} ).then( (rData) => {			if (rData && Object.keys(params.apiData).length > 0) {				params.apiData.redirects = rData?.query?.redirects;				gatherSubpageData;			} else {				showConfirm("Error parsing API data on" + params.titlesString + ".", 'error');			}		} ).fail ( (codetr, reslttr) => {			showConfirm("Error fetching API data on" + params.titlesString + ": " + (reslttr.error.info || (codetr + ".")), 'error');		} ); }	return true; }