//jshint -W083
function pageSwap(prefix, moveReason, debug) {
var config = {
link: "[[" + prefix + "|pageswap]]",
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: "<big>'''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.'''</big>\n\n" +
'Using the form below will [[Wikipedia:Moving a page#Swapping ' +
'two pages|swap]] two pages using the [[User:Ahecht/Scripts/' +
'pageswap|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 [[Special:DoubleRedirects|double]] or [[Special:' +
'BrokenRedirects|broken redirects]] and [[Wikipedia:Red link|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 [[User:Ahecht/Scripts/pageswap' +
'#Out of scope|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 [[Wikipedia: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 [[Special:MyContributions|' +
'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!',
rrReason: '[[WP:Page mover#rr|Round-robin history swap]] step 1 using ',
redirTempls: "{{Redirect category shell|\n{{R from move}}\n}}",
types: ['notice', 'success', 'warning', 'error']
}, params = {
apiData: {}, currTitle: {}, destTitle: {},
confirmMessages: [], statusMessages: [],
defaultMoveTalk: true, 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();
}
}
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) {
var idempotency = ++params.idempotency.psConfirm;
if (message !== '') {
params.confirmMessages.push(message.replace("[[WP:RM/TR]]",
"[[WP:Requested moves/Technical requests|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), type, idempotency)
).fail(
(codetr, reslttr) => parseError(psConfirm, label, codetr, reslttr, idempotency)
);
if (type=='error') {psProgress.toggle(false);}
}
function showStatus(message, type='notice', done=false, indent=false, topic=false) {
var idempotency = ++params.idempotency.psStatus;
if (done) {
params.done = true;
console.log("Done. Caller:"+showStatus.caller.name+";message:"+message+"; type:"+type+"; done:"+done+"; indent:"+indent+"; topic:"+topic);//debug
}
if (message !== '') {
var topicFlag = topic ? "<!--"+topic+"-->" : false;
var topicIndex = params.statusMessages.findIndex((str) => str.indexOf(topicFlag) > -1);
message = "*"+(indent ? "*" : "") + message.replace("[[WP:RM/TR]]",
"[[WP:Requested moves/Technical requests|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) {
console.log("Not busy. Caller:"+showStatus.caller.name+";message:"+message+"; type:"+type+"; done:"+done+"; indent:"+indent+"; topic:"+topic);//debug
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 getPagesData() {
// 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) {
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,params.destTitle.title].includes(query.pages[kp].title)) {
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"+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("<b>Warning: [[" + params.currTitle.title + "]] was moved " +
Math.round(lastMove) + " minute(s) ago.</b>",
'warning');
} else if ( lastMove < 1440 ) { // 1 day
showConfirm("<b>Note: [[" + params.currTitle.title + "]] was moved " +
Math.round(lastMove/60) + " hour(s) ago.</b>",
'notice');
} else if ( lastMove < 43200 ) { // 30 days
showConfirm("[[" + params.currTitle.title + "]] was last moved " +
Math.round(lastMove/1440) + " day(s) ago.</b>",
'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() {
// get page data, normalize titles
var ret = getPagesData();
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;
}
if (params.currTitle.title == params[k].title) {
ret.currTitle = params[k].title;
ret.currNs = params[k].ns;
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.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.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);
}
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 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) {
params.busy++;console.log(arguments.callee.name);console.log("+");console.log(params.busy);//debug
setTimeout( () => {
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) {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("Talk page redirect simulated!.",
'notice', true, true, "TPR" + fromTalk);
} else {
new mw.Api().postWithEditToken(talkRedirect).done( () => {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("Talk page redirect created!",
'notice', true, true, "TPR" + fromTalk);
} ).fail( (codetr, reslttr) => {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("Failed to create redirect! " +
(reslttr.error.info || (codetr + ".")),
'error', true, true, "TPR" + fromTalk);
} );
}
}, 250);
} else { showStatus('', 'notice', true); }
} else { showStatus('', 'notice', true); }
}
function retargetRedirect(thisPage, otherPage, newText) {
params.busy++;console.log(arguments.callee.name);console.log("+");console.log(params.busy);//debug
showStatus("Retargeting redirect at [[" + thisPage +
"]] to [[" + otherPage + "]]...",
'notice', false, false, "RT"+thisPage);
var retargetData = {
action:'edit',
title: thisPage,
text: newText,
summary : "Retarget redirect to [[" +
otherPage + "]] using " +
config.link,
watchlist: params.watch
};
if (debug) {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("Retargeting simulated!",'notice', false, true, "RT"+thisPage);
} else {
new mw.Api().postWithEditToken(retargetData).done( (result, jqXHR) => {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
if (typeof result.edit !== 'undefined') {
params.busy++;console.log(arguments.callee.name);console.log("+");console.log(params.busy);//debug
new mw.Api().get( {
action: 'query', prop: '', redirects: 1,
titles: result.edit.title
} ).done( (data) => {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
if (data && typeof data?.query?.redirects !== 'undefined') {
showStatus("Redirect retargeted!", 'notice',
false, true, "RT"+thisPage);
} else {
console.warn("Error parsing redirects after retargeting:");
console.warn(data);
}
} ).fail( (codeart, rsltart) => {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
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, true, "RT"+thisPage);
} );
}
}
/**
* 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) {
params.busy++;console.log(arguments.callee.name);console.log("+");console.log(params.busy);//debug
var pagesArr = [vData.currTitle, vData.destTitle,
vData.currTalkName, vData.destTalkName];
new mw.Api().get( {
action:'query', format:'json',
redirects:'true', titles: pagesArr.join("|")
} ).done( rData => {
var redirs = rData?.query?.redirects;
var selfRedirs = [];
for (const e in redirs) {
if(redirs[e].from == redirs[e].to) {
selfRedirs.push(redirs[e].from);
}
}
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
if (selfRedirs.length > 0) {
for (let i = 0; i < 4; i++) {
if (selfRedirs.includes(pagesArr[i])) {
params.busy++;console.log(arguments.callee.name);console.log("+");console.log(params.busy);//debug
new mw.Api().get( {
action:'parse', format:'json', prop:'wikitext',
page: pagesArr[i]
} ).done( parseData => {
var thisPage = parseData.parse.title;
var thisI = pagesArr.indexOf(thisPage);
var otherI = (thisI==0)?1:((thisI==1)?0:((thisI==2)?3:2));
var otherPage = pagesArr[otherI];
if (parseData && parseData?.parse?.wikitext) {
var oldText = parseData.parse.wikitext['*'];
var redirRE = new RegExp(
"^\\s*#REDIRECT +\\[\\[ *.* *\\]\\]", "i"
);
if ((thisI > -1) && (oldText.search(redirRE) > -1)) {
var newText = oldText.replace(redirRE,
'#REDIRECT [['+otherPage+']]');
if (oldText.search("{{") == -1) {
newText += "\n\n" + config.redirTempls;
}
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
retargetRedirect(thisPage, otherPage, newText);
} else {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("Attempt to retarget " +
"redirect at [[" + thisPage +
"]] to [[" + otherPage + "]] " +
"failed: String not found.",
'warning');
}
} else {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("Attempt to retarget redirect " +
"at [[" + thisPage + "]] to [[" +
otherPage + "]] failed: " +
"Could not fetch contents.", 'error');
}
} ).fail( (jqXHR, textStatus) => {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("Attempt to retarget redirect at [[" +
pagesArr[i] + "]] failed due to API error '" +
(jqXHR.status||textStatus) + "' when " +
"fetching page contents. ", 'error');
} ).always( () => {
if (i == 3) {createMissingTalk(vData, vTData);}
} );
} else if (i == 3) { createMissingTalk(vData, vTData); }
}
} else {
createMissingTalk(vData, vTData);
}
} ).fail( (jqXHR, textStatus) => {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("API error '" + (jqXHR.status||textStatus) +
"' when fetching redirect targets. ", 'error');
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 + "]])...", '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 + "]] ([[Special:WhatLinksHere/" +
params.currTitle.title + "|links]]) and [[" +
destTitle + "]] ([[Special:WhatLinksHere/" +
params.destTitle.title + "|links]]) simulated successfully!";
if (params.fixSelfRedirect || params.talkRedirect) {
showStatus(completeMessage, 'notice', false, true);
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
if (params.fixSelfRedirect) {
checkSelfRedirs(vData, vTData);
} else {
createMissingTalk(vData, vTData);
}
} else {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus(completeMessage, 'notice', true, true);
}
} else {
showStatus("Doing round-robin history swap...");
var mQuery = { action:'move', from:destTitle, to:intermediateTitle,
reason: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 + "]])...", 'notice', false, true);
new mw.Api().postWithEditToken(mQuery).then( () => {
Object.assign(mQuery, { from:currTitle, to:destTitle,
reason: mQuery.reason.replace("step 1", "step 2") } );
showStatus("Step 2 ([[" + mQuery.from + "]] → [[" +
mQuery.to + "]])...", 'notice', false, true);
return new mw.Api().postWithEditToken(mQuery);
} ).then( () => {
Object.assign(mQuery, { from:intermediateTitle, to:currTitle,
reason: mQuery.reason.replace("step 2", "step 3") } );
showStatus("Step 3 ([[" + mQuery.from + "]] → [[" +
mQuery.to + "]])...", 'notice', false, true);
return new mw.Api().postWithEditToken(mQuery);
} ).then( () => {
var completeMessage = "Round-robin history swap of [[" +
currTitle + "]] ([[Special:WhatLinksHere/" +
currTitle + "|links]]) and [[" + destTitle +
"]] ([[Special:WhatLinksHere/" + destTitle +
"|links]]) completed successfully!";
if (params.fixSelfRedirect || params.talkRedirect) {
showStatus(completeMessage, 'notice', false, true);
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
if (params.fixSelfRedirect) {
checkSelfRedirs(vData, vTData);
} else {
createMissingTalk(vData, vTData);
}
} else {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus(completeMessage, 'notice', true, true);
}
} ).fail( (code, reslt) => {
params.busy--;console.log(arguments.callee.name);console.log("-");console.log(params.busy);//debug
showStatus("Failed when moving ([[" + mQuery.from + "]] → [[" +
mquery.to + "]])! " + (reslt.error.info || (code + ".")),
'error', true, true);
} );
}
}
/**
* 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.");
}
psProgress.toggle(false);
showConfirm('', 'notice', true);
psButton.setDisabled(false).setLabel(config.confirmButton).off('click').on('click', function() {
psButton.setDisabled(true).setLabel(config.validateButton);
swapPages(vData, vTData);
} );
}
/**
* Given two titles, gathers data on talk/subpages,
* then passes that to confirmConfig()
*/
function getApiData () {
var currSpFlags, destSpFlags, currTSpFlags, destTSpFlags;
// validate namespaces, not identical, can move
const vData = swapValidate();
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);
getSubpages(vData.destTitle, false).done( (dData) => {
destSpFlags = printSubpageInfo(vData.destTitle, dData);
// talk subpages
getSubpages(vData.currTitle, true).done( (cTData) => {
currTSpFlags = printSubpageInfo(vData.currTalkName, cTData);
getSubpages(vData.destTitle, true).done( (dTData) => {
destTSpFlags = printSubpageInfo(vData.destTalkName, dTData);
confirmConfig(vData, currSpFlags, destSpFlags, currTSpFlags, destTSpFlags);
} ).fail( (error) => showConfirm(error, 'error') );
} ).fail( (error) => showConfirm(error, 'error') );
} ).fail( (error) => showConfirm(error, 'error') );
} ).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) {
if (typeof data === 'object') {
data = mw.Title.makeTitle(data.namespace.value, data.title.value);
} else {
data = mw.Title.newFromText(data);
}
if (data) {
return {ns: data.namespace, title: data.getPrefixedText()};
} else {
return 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: 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) || {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' ], 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( (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( 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
*/
psButton.off('click').on( 'click', clickValidate );
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);
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();
}
/**
* 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 ).done ( (data) => {
if (data && data?.query?.namespaces) {
params.apiData = data.query;
getApiData();
} 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;
}