//jshint -W083
function pageSwap(prefix, moveReason, debug) {
var config = {
link: "[[" + prefix + ".js|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: '<strong>Round-robin configuration:</strong><ul><li>',
confirmMessageFooter: '<p>Press "Confirm" to proceed.</p>',
statusMessageHeader: '<strong>Performing page swap:</strong><ul>',
indentStyle: ' style="margin-left: 1.6em;"',
introText: '<p style="font: bold 1.2em sans-serif;">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.</p>' +
'<p>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 history to the new ' +
'names. <strong>Links to the old page titles will not be changed' +
'</strong>. ' +
'Be sure to check <strong>[[Special:MyContributions]]</strong> 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.</p>' +
'<p><strong>Note:</strong>' +
'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.</p>',
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:',
errorMsg: 'Error adding swap form to page!'
}, params = {
currTitle: {}, destTitle: {}, confirmMessages: [], statusMessages: [],
defaultMoveTalk: true,
done: false, cleanup: (
typeof pagemoveDoPostMoveCleanup === 'undefined' ?
true :
pagemoveDoPostMoveCleanup
)
};
function wikiLink(message) {
message = message.replace(/\[\[(.*?)\]\]/g, function(s,v) {
v = v.split("|");
var urlSuffix="", aClass="";
if (v[0].search(new RegExp("^"+config.intermediatePrefix)) > -1) {
// Make intermediate page appear as redlink
aClass='new';
} else if (v[0].search(/^\R¬/) > -1) {
// display redirects marked with R¬
v[0] = v[0].replace(/^R¬/, '');
aClass='mw-redirect';
if (v.length == 1) {
urlSuffix = "?redirect=no";
}
}
var url = mw.config.get("wgServer") +
mw.config.get("wgArticlePath").replace("$1", encodeURI(v[0].replace(/ /g,"_")));
return '<a href="'+url+urlSuffix+'" class="'+aClass+'" title="'+v[0].replace(/_/g," ")+'">' +
(v[1] || v[0]) + '</a>';
} );
return message;
}
function showConfirm(message, type='notice', done=false) {
if (message !== '') {
params.confirmMessages.push(wikiLink(message));
}
var types = ['notice', 'success', 'warning', 'error'];
if (types.indexOf(type) > types.indexOf(psConfirm.type)) {
psConfirm.setType(type);
}
var label = new OO.ui.HtmlSnippet(
config.confirmMessageHeader+
params.confirmMessages.join("</li><li>")+
"</li></ul>"+
(done ? config.confirmMessageFooter : '')
);
psConfirm.setLabel(label);
psConfirm.toggle(true);
psConfirm.scrollElementIntoView();
}
function showStatus(message, type='notice', done=false, indent=false) {
if (message !== '') {
params.statusMessages.push("<li"+(indent ? config.indentStyle : '')+
">"+wikiLink(message)+"</li>");
}
if (done) {
params.done = true;
if (params.allSpArr.length) {
params.statusMessages.push(
wikiLink("<li>"+config.doneSubpages+"<ul><li>[[" +
params.allSpArr.join("]]</li><li>[[") + "]]</li></ul></li>")
);
}
psContribsButton.toggle(true);
}
var types = ['notice', 'success', 'warning', 'error'];
if (types.indexOf(type) > types.indexOf(psStatus.type)) {
psStatus.setType(type);
}
var doneMessage = "";
if (params.done) {
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);
}
doneMessage = "<p>"+wikiLink(doneMessage)+".</p>";
}
var label = new OO.ui.HtmlSnippet(
config.statusMessageHeader+
params.statusMessages.join('')+
"</ul>"+doneMessage
);
psStatus.setLabel(label);
psStatus.toggle(true);
psStatus.scrollElementIntoView();
}
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 ) {
showConfirm("<b>Warning: [[" + params.currTitle.title + "]] was last moved " +
Math.round(lastMove) + " minute(s) ago.</b>",
'warning');
}
}
} 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 ? ((titleNs % 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].isRedir ? "(R) " : " ") +
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:<ul><li>[[" +
currSpCannotMove.join("]]</li><li>[[") + "]]</li></ul>",
'warning');
} else {
showConfirm(currSpArr.length + " total subpages of [[" + basepage + "]]" +
(redirCount !== 0 ? (" (" + redirCount + " redirects):") : ":") +
"<ul><li>[[" + currSpArr.join("]]</li><li>[[") + "]]</li></ul>");
}
}
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{{R from move}}",
summary: "Create redirect to [[" + toTalk + "]] using " + config.link,
watchlist: params.watch
};
showStatus("Creating talk page redirect [[R¬"+fromTalk+"]] → [["+toTalk+"]]...");
if (debug) {
console.log(talkRedirect);
showStatus("Redirect [[R¬"+fromTalk+"]] → [["+toTalk+
"]] simulated successfully!.", 'success', true, true);
} else {
new mw.Api().postWithEditToken(talkRedirect).done(function (reslttr) {
showStatus("Redirect [[R¬"+fromTalk+"]] → [[" +toTalk +
"]] created successfully!", 'success', true, true);
}).fail(function (codetr, reslttr) {
showStatus("Failed to create redirect! " +
(reslttr.error.info || (codetr + ".")),
'error', true, true);
});
}
} 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 [[R¬" + vData[thisPage] +
"]] to [[" + vData[otherPage] + "]]...");
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("Redirect at [[R¬"+vData[thisPage]
+"]] simulated retargeted to [["+vData[otherPage] + "]].",
'success', false, true);
} 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 at [[R¬"+
data.query.redirects[0].from +
"]] retargeted to [[" +
data.query.redirects[0].to +
"]].", 'success', false, true);
} 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("Failed to retarget redirect at [[R¬"+vData[thisPage]+
"]] to [["+vData[otherPage]+"]]. "+
(resultrt.error.info || (codert + ".")), 'error', false, true);
});
}
} else {
showStatus("Failed to retarget redirect at [[R¬"+vData[thisPage]+
"]] to [["+vData[otherPage]+"]]: String not found.", 'warning');
}
} else {
showStatus("Failed to check contents of [[R¬"+vData[thisPage]+"]]: " + err + ".",
'error');
}
}
}
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:"[[WP:PMRC#4|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:"[[WP:PMRC#4|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 (vData.destIsRedir) {currTitle = "R¬" + currTitle;}
if (vData.currIsRedir) {destTitle = "R¬" + destTitle;}
if (debug) {
showStatus("Simulating round-robin history swap...");
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.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 + "]] ([[Special:WhatLinksHere/" +
params.currTitle.title + "|links]]) and [[" +
destTitle + "]] ([[Special:WhatLinksHere/" +
params.destTitle.title +
"|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');
}
}
var currTitle = params.currTitle.title;
var destTitle = params.destTitle.title;
if (vData.currIsRedir) {currTitle = "R¬" + currTitle;}
if (vData.destIsRedir) {destTitle = "R¬" + destTitle;}
showConfirm("Swapping [["+currTitle+"]] → [["+destTitle+"]]");
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) {
if (params.allSpArr.length > 0) {
var subpagesToMoveList = params.allSpArr.length+" subpages to move:"+
"<ul><li>[[" + params.allSpArr.join("]]</li><li>[[") + "]]</li></ul>";
//showConfirm(subpagesToMoveList);
} else {
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");
$( '#movepagetext' ).html( wikiLink(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;
}