User:Ahecht/Scripts/pageswap-core.js
< User:Ahecht | Scripts
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
This user script seems to have a documentation page at User:Ahecht/Scripts/pageswap-core. |
//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: "'''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!',
redirTempls: "{{Redirect category shell|\n{{R from move}}\n}}",
types: ['notice', 'success', 'warning', 'error']
}, params = {
currTitle: {}, destTitle: {}, confirmMessages: [], statusMessages: [],
defaultMoveTalk: true, done: false,
idempotency: {psConfirm: 0, psStatus: 0},
cleanup: (
typeof pagemoveDoPostMoveCleanup === 'undefined' ?
true :
pagemoveDoPostMoveCleanup
)
};
function filterHtml(rawHtml) {
$value=$($.parseHTML(rawHtml));
$value.filter("div.mw-parser-output").contents().each(function() {
if(this.nodeType === Node.COMMENT_NODE || this.nodeType === Node.TEXT_NODE) {
$(this).remove();
}
}).find('a.mw-redirect').each(function() {
$(this).attr('href', $(this).attr('href') + "?redirect=no");
});
return $value.html();
}
function setLabel(container, label, idempotency) {
label = new OO.ui.HtmlSnippet(label);
if (idempotency == params.idempotency[container.elementId]) {
container.setLabel(label);
container.toggle(true);
container.scrollElementIntoView();
}
}
function parseError(ps, label, codetr, reslttr, idempotency) {
label = "Error parsing wikitext:\n\n" + label + "\n\n" +
(reslttr.error.info || (codetr + "."));
console.warn(label);
ps.setType('error');
setLabel(ps, label, idempotency);
}
function showConfirm(message, type='notice', done=false) {
var idempotency = ++params.idempotency.psConfirm;
if (config.types.indexOf(type) > config.types.indexOf(psConfirm.type)) {
psConfirm.setType(type);
}
if (message !== '') {
params.confirmMessages.push(message.replace("[[WP:RM/TR]]",
"[[WP: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), idempotency)
).fail(
(codetr, reslttr) => parseError(psConfirm, label, codetr, reslttr, idempotency)
);
}
function showStatus(message, type='notice', done=false, indent=false, topic=false) {
var idempotency = ++params.idempotency.psStatus;
if (config.types.indexOf(type) > config.types.indexOf(psStatus.type)) {
psStatus.setType(type);
}
if (!params.done && done) {
params.done = true;
}
if (message !== '') {
var topicFlag = topic ? "<!--"+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) {
if (params.allSpArr.length) {
doneSubpagesMessage = config.doneSubpages + "**[[" +
params.allSpArr.join("]]\n**[[") + "]]\n";
}
psContribsButton.toggle(true);
var doneMessages = [config.doneMsgCleanup];
if (!params.talkRedirect || params.moveSubpages) {doneMessages.push(config.doneMsgRedlink)}
if (!params.fixSelfRedirect || params.moveSubpages) {doneMessages.push(config.doneMsgRedir)}
if (doneMessages.length < 3) {
doneMessage = doneMessages.join(" and ") + ".";
} else {
doneMessage = doneMessages.slice(0, -1).join(', ') + ', and ' +
doneMessages.slice(-1) + ".";
}
}
var label = config.statusMessageHeader + params.statusMessages.join('') +
doneSubpagesMessage + doneMessage;
new mw.Api().parse(label).done(
(parsedText) => setLabel(psStatus, filterHtml(parsedText), idempotency)
).fail(
(codetr, reslttr) => parseError(psStatus, label, codetr, reslttr, idempotency)
);
}
function getPagesData() {
// get page data, normalize titles
var ret = {valid: true, invalidReason: ''};
var titlesString = " [["+params.currTitle.title+"]] or [["+params.destTitle.title+"]]. ";
var queryData = {action:'query', format:'json', prop:'info', inprop:'talkid',
intestactions:'move|create', titles: (params.currTitle.title + "|" + params.destTitle.title),
list:'logevents', leprop:'timestamp', letype:'move', letitle: params.currTitle.title, lelimit:'1'
};
var query = $.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jqXHR, textStatus, errorThrown) {
var errStr = "Error '"+(jqXHR.status||textStatus)+
"' fetching API data on "+titlesString+". "+
(errorThrown||jqXHR.responseText).replace("\n","");
console.warn(errStr);console.log(queryData);console.log(jqXHR);
ret = {valid: false, invalidReason: errStr};
},
data: queryData
}).responseJSON;
if (typeof query === 'undefined' || typeof query.query === 'undefined') {
return {valid: false, invalidReason: ret.invalidReason+
"Error parsing API data on"+titlesString};
}
if (!ret.valid) {return ret;}
query = query.query;
if (typeof query.pages !== 'undefined' && typeof query.logevents !== 'undefined') {
for (var kn in query.normalized) {
if (params.currTitle.title == query.normalized[kn].from) {
params.currTitle.title = query.normalized[kn].to;
}
if (params.destTitle.title == query.normalized[kn].from) {
params.destTitle.title = query.normalized[kn].to;
}
}
for (var kp in query.pages) {
if (params.currTitle.title == query.pages[kp].title) {
params.currTitle = query.pages[kp];
}
if (params.destTitle.title == query.pages[kp].title) {
params.destTitle = query.pages[kp];
}
if (kp < 0) {
ret.valid = false;
if (typeof query.pages[kp].missing !== 'undefined') {
ret.invalidReason += "Unable to find [["+query.pages[kp].title+"]]. ";
} else if (typeof query.pages[kp].invalid !== 'undefined' &&
typeof query.pages[kp].invalidreason !== 'undefined') {
ret.invalidReason += query.pages[kp].invalidreason;
} else {
ret.invalidReason += "Unable to get page data for"+titlesString;
}
}
}
for (var kl in query.logevents) {
var lastMove = (Date.now()-Date.parse(query.logevents[kl].timestamp))/(1000*60);
if ( lastMove < 60 ) { // 1 hour
showConfirm("<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) + " hours(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"+titlesString};
}
return ret;
}
/**
* Given namespace data, title, title namespace, returns expected title of page
* Along with title without prefix
* Precondition, title, titleNs is a subject page!
*/
function getTalkPageName(title, titleNs) {
var ret = {};
var nsData = mw.config.get("wgFormattedNamespaces");
var prefixLength = nsData['' + titleNs].length === 0 ?
0 : nsData['' + titleNs].length + 1;
ret.titleWithoutPrefix = title.substring(prefixLength, title.length);
ret.talkTitle = nsData['' + ((Math.floor(titleNs / 2)*2) + 1)] + ':' +
ret.titleWithoutPrefix;
return ret;
}
/**
* Given two (normalized) titles, find their namespaces, if they are redirects,
* if have a talk page, whether the current user can move the pages, suggests
* whether movesubpages should be allowed, whether talk pages need to be checked
*/
function swapValidate() {
// get page data, normalize titles
var ret = getPagesData();
if (ret.valid === false || params.currTitle.title === null ||
params.destTitle.title === null || params === null
) {
ret.valid = false;
ret.invalidReason += "Failed to validate swap.";
return ret;
}
ret.allowMoveSubpages = true;
ret.checkTalk = true;
for (const k of ["currTitle", "destTitle"]) {
if (k == "-1" || params[k].ns < 0) {
ret.valid = false;
ret.invalidReason = ("Page " + params[k].title + " does not exist.");
return ret;
}
// enable only in ns 0..5,12,13,118,119 (Main,Talk,U,UT,WP,WT,H,HT,D,DT)
if ((params[k].ns >= 6 && params[k].ns <= 9) ||
(params[k].ns >= 10 && params[k].ns <= 11 && !params.uPerms.allowSwapTemplates) ||
(params[k].ns >= 14 && params[k].ns <= 117) ||
(params[k].ns >= 120)) {
ret.valid = false;
ret.invalidReason = ("Namespace of " + params[k].title + " (" +
params[k].ns + ") not supported.\n\nLikely reasons:\n" +
"- Names of pages in this namespace relies on other pages\n" +
"- Namespace features heavily-transcluded pages\n" +
"- Namespace involves subpages: swaps produce many redlinks\n" +
"\n\nIf the move is legitimate, consider a careful manual swap.");
return ret;
}
if (params.currTitle.title == params[k].title) {
ret.currTitle = params[k].title;
ret.currNs = params[k].ns;
ret.currTalkId = params[k].talkid; // could be undefined
ret.currCanMove = params[k].actions.move === '';
ret.currIsRedir = params[k].redirect === '';
}
if (params.destTitle.title == params[k].title) {
ret.destTitle = params[k].title;
ret.destNs = params[k].ns;
ret.destTalkId = params[k].talkid; // could be undefined
ret.destCanMove = params[k].actions.move === '';
ret.destIsRedir = params[k].redirect === '';
}
}
if (!ret.valid) return ret;
if (!ret.currCanMove) {
ret.valid = false;
ret.invalidReason = ('' + ret.currTitle + " is immovable. Aborting");
return ret;
}
if (!ret.destCanMove) {
ret.valid = false;
ret.invalidReason = ('' + ret.destTitle + " is immovable. Aborting");
return ret;
}
if (ret.currNs % 2 !== ret.destNs % 2) {
ret.valid = false;
ret.invalidReason = "Namespaces don't match: one is a talk page.";
return ret;
}
ret.currNsAllowSubpages = params.nsData['' + ret.currNs].subpages !== '';
ret.destNsAllowSubpages = params.nsData['' + ret.destNs].subpages !== '';
// if same namespace (subpages allowed), if one is subpage of another,
// disallow movesubpages
if (ret.currTitle.startsWith(ret.destTitle + '/') ||
ret.destTitle.startsWith(ret.currTitle + '/')) {
if (ret.currNs !== ret.destNs) {
ret.valid = false;
ret.invalidReason = "Strange.\n" + ret.currTitle + " in ns " +
ret.currNs + "\n" + ret.destTitle + " in ns " + ret.destNs +
". Disallowing.";
return ret;
}
ret.allowMoveSubpages = ret.currNsAllowSubpages;
if (!ret.allowMoveSubpages)
ret.addlInfo = "One page is a subpage. Disallowing move-subpages";
}
if (ret.currNs % 2 === 1) {
ret.checkTalk = false; // no need to check talks, already talk pages
} else { // ret.checkTalk = true;
var currTPData = getTalkPageName(ret.currTitle, ret.currNs);
ret.currTitleWithoutPrefix = currTPData.titleWithoutPrefix;
ret.currTalkName = currTPData.talkTitle;
var destTPData = getTalkPageName(ret.destTitle, ret.destNs);
ret.destTitleWithoutPrefix = destTPData.titleWithoutPrefix;
ret.destTalkName = destTPData.talkTitle;
// possible: ret.currTalkId undefined, but subject page has talk subpages
}
return ret;
}
/**
* Given two talk page titles (may be undefined), retrieves their pages for comparison
* Assumes that talk pages always have subpages enabled.
* Assumes that pages are not identical (subject pages were already verified)
* Assumes namespaces are okay (subject pages already checked)
* (Currently) assumes that the malicious case of subject pages
* not detected as subpages and the talk pages ARE subpages
* (i.e. A and A/B vs. Talk:A and Talk:A/B) does not happen / does not handle
* Returns structure indicating whether move talk should be allowed
*/
function talkValidate(checkTalk, talk1, talk2) {
var ret = {};
ret.allowMoveTalk = true;
if (!checkTalk) { return ret; } // currTitle destTitle already talk pages
if (talk1 === undefined || talk2 === undefined) {
showStatus("Unable to validate talk. Disallowing movetalk to be safe.", 'warning');
ret.allowMoveTalk = false;
return ret;
}
ret.currTDNE = true;
ret.destTDNE = true;
ret.currTCanCreate = true;
ret.destTCanCreate = true;
var talkTitleArr = [talk1, talk2];
if (talkTitleArr.length !== 0) {
var talkData = JSON.parse($.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
showStatus("Unable to get info on talk pages.", 'warning');
return ret;
},
data: { action:'query', format:'json', prop:'info',
intestactions:'move|create', titles:talkTitleArr.join('|') }
}).responseText).query.pages;
for (var id in talkData) {
if (talkData[id].title === talk1) {
ret.currTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
ret.currTTitle = talkData[id].title;
ret.currTCanMove = talkData[id].actions.move === '';
ret.currTCanCreate = talkData[id].actions.create === '';
ret.currTalkIsRedir = talkData[id].redirect === '';
} else if (talkData[id].title === talk2) {
ret.destTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
ret.destTTitle = talkData[id].title;
ret.destTCanMove = talkData[id].actions.move === '';
ret.destTCanCreate = talkData[id].actions.create === '';
ret.destTalkIsRedir = talkData[id].redirect === '';
} else {
showStatus("Found pageid ("+talkData[id].title+") not matching given ids ("+
talk1+" and "+talk2+").", 'error');
return {};
}
}
}
ret.allowMoveTalk = (ret.currTCanCreate && ret.currTCanMove) &&
(ret.destTCanCreate && ret.destTCanMove);
return ret;
}
/**
* Given existing title (not prefixed with "/"), optionally searching for talk,
* finds subpages (incl. those that are redirs) and whether limits are exceeded
* As of 2016-08, uses 2 api get calls to get needed details:
* whether the page can be moved, whether the page is a redirect
*/
function getSubpages(nsData, title, titleNs, isTalk) {
if ((!isTalk) && nsData['' + titleNs].subpages !== '') { return { data:[] }; }
var titlePageData = getTalkPageName(title, titleNs);
var queryData = { action:'query', format:'json', list:'allpages',
apnamespace:(isTalk ? ((Math.floor(titleNs / 2)*2) + 1) : titleNs),
apfrom:(titlePageData.titleWithoutPrefix + '/'),
apto:(titlePageData.titleWithoutPrefix + '0'),
aplimit:101 };
var subpages = JSON.parse($.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jqXHR, textStatus, errorThrown) {
var errStr = "API error '"+(jqXHR.status||textStatus)+
"' when searching for subpages of "+title+". "+
(errorThrown||jqXHR.responseText).replace("\n","");
console.warn(errStr);console.log(queryData);console.log(jqXHR);
return { error:errStr+" Subpages may exist." };
},
data: queryData
}).responseText).query;
if (typeof subpages === 'object' && typeof subpages.allpages !== 'undefined') {
subpages = subpages.allpages;
} else {
console.warn("API did not return 'allpages' when querying subpage data:");console.log(subpages);
return { error:"API did not return subpage data. Subpages may exist." };
}
// put first 50 in first arr (need 2 queries due to api limits)
var subpageids = [[],[]];
for (var idx in subpages) {
subpageids[idx < 50 ? 0 : 1].push( subpages[idx].pageid );
}
if (subpageids[0].length === 0) { return { data:[] }; }
if (subpageids[1].length === 51) { return { error:"100+ subpages. Aborting" }; }
var dataret = [];
var subpageData0 = $.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
return { error:"Unable to fetch subpage data." }; },
data: { action:'query', format:'json', prop:'info', intestactions:'move|create',
pageids:subpageids[0].join('|') }
}).responseJSON.query.pages;
for (var k0 in subpageData0) {
dataret.push({
title:subpageData0[k0].title,
isRedir:subpageData0[k0].redirect === '',
canMove:subpageData0[k0].actions.move === ''
});
}
if (subpageids[1].length === 0) {
return { data:dataret };
}
var subpageData1 = $.ajax({
url: mw.util.wikiScript('api'), async: false,
error: function (jsondata) {
return { error:"Unable to fetch subpage data." }; },
data: { action:'query', format:'json', prop:'info', intestactions:'move|create',
pageids:subpageids[1].join('|') }
}).responseJSON.query.pages;
for (var k1 in subpageData1) {
dataret.push({
title:subpageData1[k1].title,
isRedir:subpageData1[k1].redirect === '',
canMove:subpageData1[k1].actions.move === ''
});
}
return { data:dataret };
}
/**
* Prints subpage data given retrieved subpage information returned by getSubpages
* Returns a suggestion whether movesubpages should be allowed
*/
function printSubpageInfo(basepage, currSp) {
var ret = {};
var currSpArr = [];
var currSpCannotMove = [];
var redirCount = 0;
for (var kcs in currSp.data) {
if (!currSp.data[kcs].canMove) {
currSpCannotMove.push(currSp.data[kcs].title);
}
currSpArr.push(currSp.data[kcs].title);
if (currSp.data[kcs].isRedir)
redirCount++;
}
if (currSpArr.length > 0) {
if (currSpCannotMove.length > 0) {
showConfirm("Disabling move-subpages." +
"The following " + currSpCannotMove.length + " (of " +
currSpArr.length + ") total subpages of [[" +
basepage + "]] CANNOT be moved:\n**[[" +
currSpCannotMove.join("]]\n**[[") + "]]",
'warning');
} else if (typeof basepage !== 'undefined') {
showConfirm(currSpArr.length + " total subpages of [[" + basepage + "]]" +
(redirCount !== 0 ? (" (" + redirCount + " redirects):") : ":") +
"\n**[[" + currSpArr.join("]]\n**[[") + "]]");
}
}
ret.allowMoveSubpages = currSpCannotMove.length === 0;
ret.noNeed = currSpArr.length === 0;
ret.spArr = currSpArr;
return ret;
}
function createMissingTalk(vData, vTData) {
if (params.moveTalk && params.talkRedirect) {
var fromTalk, toTalk;
if (vTData.currTDNE && !vTData.destTDNE) {
fromTalk = vData.destTalkName;
toTalk = vData.currTalkName;
} else if (vTData.destTDNE && !vTData.currTDNE) {
fromTalk = vData.currTalkName;
toTalk = vData.destTalkName;
}
if (fromTalk && toTalk) {
setTimeout(() => {
if (params.talkRedirect) {
var talkRedirect = {
action:'edit',
title:fromTalk,
createonly: true,
text: "#REDIRECT [[" + toTalk + "]]\n\n" + config.redirTempls,
summary: "Create redirect to [[" + toTalk + "]] using " + config.link,
watchlist: params.watch
};
showStatus("Creating talk page redirect [["+fromTalk+"]] → [["+toTalk+"]]...",
'notice', false, false, "TPR"+fromTalk);
if (debug) {
console.log(talkRedirect);
showStatus("Talk page redirect simulated!.", 'success', true,
true, "TPR"+fromTalk);
} else {
new mw.Api().postWithEditToken(talkRedirect).done(function (reslttr) {
showStatus("Talk page redirect created!", 'success', true,
true, "TPR"+fromTalk);
}).fail(function (codetr, reslttr) {
showStatus("Failed to create redirect! " +
(reslttr.error.info || (codetr + ".")),
'error', true, true, "TPR"+fromTalk);
});
}
} else { showStatus('', 'notice', true); }
}, 250);
} else { showStatus('', 'notice', true); }
} else { showStatus('', 'notice', true); }
}
/**
* After successful page swap, post-move cleanup:
* Make talk page redirect
* TODO more reasonable cleanup/reporting as necessary
* vData.(curr|dest)IsRedir
*/
/** TO DO:
*Check if talk is self redirect
*/
function doPostMoveCleanup(vData, vTData, current = "currTitle", destination = "destTitle") {
if (params.fixSelfRedirect) {// Check for self redirect
for (const thisPage of [current, destination]){
var otherPage = (thisPage == current) ? destination : current;
var rData = $.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
showStatus("Unable to get info about " + vData[thisPage] + ".", 'error');
},
data: { action:'query', format:'json', redirects:'true', titles: vData[thisPage] }
}).responseJSON.query;
if (rData && rData.redirects &&
(rData.redirects[0].from == rData.redirects[0].to ||
(debug && rData.redirects[0].to == vData[otherPage])
)
) {
var parseData = $.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
showStatus("Unable to fetch contents of " + vData[thisPage] + ".",
'error');
},
data: {
action:'parse', format:'json', prop:'wikitext', page: vData[thisPage]
}
}).responseJSON.parse;
if (parseData) {
var redirRE = new RegExp("^\\s*#REDIRECT +\\[\\[ *.* *\\]\\]", "i");
if (parseData.wikitext['*'].search(redirRE) > -1) {
showStatus("Retargeting redirect at [[" + vData[thisPage] +
"]] to [[" + vData[otherPage] + "]]...",
'notice', false, false, "RT"+vData[thisPage]);
var retargetRedirect = {
action:'edit',
title: vData[thisPage],
text: parseData.wikitext['*'].replace(redirRE,
'#REDIRECT [['+vData[otherPage]+']]'),
summary : "Retarget redirect to [[" +
vData[otherPage] + "]] using " +
config.link,
watchlist: params.watch
};
if (debug) {
showStatus("Retargeting simulated!",'success', false, true, "RT"+vData[thisPage]);
} else {
new mw.Api().postWithEditToken(retargetRedirect).done(function (result, jqXHR) {
if (typeof result.edit !== 'undefined') {
new mw.Api().get( {
action: 'query', prop: '', redirects: 1,
titles: result.edit.title
}).done( function (data) {
if (typeof data.query.redirects !== 'undefined') {
showStatus("Redirect retargeted!", 'success',
false, true, "RT"+vData[thisPage]);
} else {
console.warn("Error parsing redirects after retargeting:");
console.warn(data);
}
}).fail( function (codeart, rsltart) {
console.warn("Error fetching page after retargeting:");
console.warn(codeart);console.warn(rsltart);
});
} else {
console.warn("Error parsing result of retargeting:");
console.warn(result);console.warn(jqXHR);
}
}).fail(function (codert, resultrt) {
showStatus("Retargeting failed. "+
(resultrt.error.info || (codert + ".")),
'error', false, true, "RT"+vData[thisPage]);
});
}
} else {
showStatus("Retargeting failed: String not found.", 'warning', false,
true, "RT"+vData[thisPage]);
}
} else {
showStatus("Failed to check contents of [[" +
vData[thisPage] + "]]: " + err + ".", 'error',
false, true, "RT"+vData[thisPage]);
}
}
}
if (current == "currTitle") {
doPostMoveCleanup(vData, vTData,"currTalkName", "destTalkName");
} else {
createMissingTalk(vData, vTData);
}
} else { //Option to fix self-redirects not selected, skipping
createMissingTalk(vData, vTData);
}
}
/**
* Swaps the two pages (given all prerequisite checks)
* Optionally moves talk pages and subpages
*/
function swapPages(vData, vTData) {
if (params.currTitle.title === null || params.destTitle.title === null ||
params.moveReason === null || params.moveReason === '') {
showStatus("Titles are null, or move reason given was empty. Swap not done", 'error');
return false;
}
var intermediateTitle = config.intermediatePrefix + params.currTitle.title;
var pOne = { action:'move', from:params.destTitle.title, to:intermediateTitle,
reason:"[[WP:Page mover#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:Page mover#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 (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.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');
}
}
showConfirm("Swapping [["+params.currTitle.title+"]] → [["+params.destTitle.title+"]]");
showConfirm("Reason: "+params.moveReason);
if (debug) {
showConfirm("Move talk: "+params.moveTalk+", Move subpages: "+params.moveSubpages);
showConfirm("Talk redirect: "+params.talkRedirect+
", Fix self-redirect: "+params.fixSelfRedirect);
}
if (params.moveSubpages && params.allSpArr.length <= 0) {
showConfirm("No subpages found to move.");
}
showConfirm('', 'notice', true);
psSwap.setDisabled(false).setLabel(config.confirmButton).off('click').on('click', function() {
psSwap.setDisabled(true).setLabel(config.swapButton);
swapPages(vData, vTData);
});
}
function titleInput(title) {
var nsObj = {value: title.ns || 0, $overlay: true};
var tObj = {value: title.title || '', $overlay: true};
if (typeof title.ns !== 'undefined' && typeof title.title !== 'undefined') {
var re = '^'+mw.config.get("wgFormattedNamespaces")[title.ns]+':';
tObj.value = title.title.replace(new RegExp(re),'');
}
return new mw.widgets.ComplexTitleInputWidget({namespace: nsObj, title: tObj});
}
function assembleTitle(field) {
if (field.namespace.value == 0) { return {ns: 0, title: field.title.value}; }
return {
ns: field.namespace.value,
title: mw.config.get("wgFormattedNamespaces")[field.namespace.value]+":"+field.title.value
};
}
/**
* Determine namespace of title
*/
function psParseTitle(title) {
var ns, titleMain;
title = title.replace("_"," ");
for (var k in mw.config.get("wgFormattedNamespaces")) {
var nsName = mw.config.get("wgFormattedNamespaces")[k],
match = title.match(new RegExp("^"+nsName+":(.*)$","i"));
if (match) {
ns = k;
titleMain = match[1];
break;
}
}
var ret = {ns: (ns || 0), title: title, titleMain: (titleMain || title)};
return ret;
}
/**
* If user is able to perform swaps
*/
function checkUserPermissions() {
var ret = {};
ret.canSwap = true;
var reslt = $.ajax({
url: mw.util.wikiScript('api'), async:false,
error: function (jsondata) {
mw.notify("Swapping pages unavailable.", { title: 'Page Swap Error', type: 'error' });
return ret;
},
data: { action:'query', format:'json', meta:'userinfo', uiprop:'rights' }
}).responseJSON.query.userinfo;
// check userrights for suppressredirect and move-subpages
var rightslist = reslt.rights;
ret.canSwap =
$.inArray('suppressredirect', rightslist) > -1 &&
$.inArray('move-subpages', rightslist) > -1;
ret.allowSwapTemplates =
$.inArray('templateeditor', rightslist) > -1;
return ret;
}
/**
* Script execution starts here:
*/
//Read the old title from the URL or the relevant pagename
params.currTitle.title = mw.util.getParamValue("wpOldTitle") || mw.config.get("wgRelevantPageName");
if (document.getElementsByName("wpOldTitle")[0] &&
document.getElementsByName("wpOldTitle")[0].value != ''
){
//If the hidden form field element has a value, use that instead
params.currTitle.title = document.getElementsByName("wpOldTitle")[0].value;
}
//Parse out title and namespace
params.currTitle = psParseTitle(params.currTitle.title);
//Read the new title from the URL or make it blank
params.destTitle.title = mw.util.getParamValue("wpNewTitle") || '';
//Parse out title and namespace
params.destTitle = psParseTitle(params.destTitle.title);
if (document.getElementsByName("wpNewTitleMain")[0] &&
document.getElementsByName("wpNewTitleMain")[0].value != '' &&
document.getElementsByName("wpNewTitleNs")[0]
){
//If the Move page form exists, use the values from that instead
params.destTitle.title = document.getElementsByName("wpNewTitleMain")[0].value;
params.destTitle.ns = document.getElementsByName("wpNewTitleNs")[0].value;
if (params.destTitle.ns != 0) {
params.destTitle.title = mw.config.get("wgFormattedNamespaces")[params.destTitle.ns] +
":" + params.destTitle.title;
}
}
params.uPerms = checkUserPermissions();
if (!params.uPerms.canSwap) {
mw.loader.using( [ 'mediawiki.notification' ], function () {
mw.notify("User rights insufficient for action.", { title: 'Page Swap Error', type: 'error' });
return;
} );
}
$( '#firstHeading' ).text(function(i, t) {return t.replace('Move', 'Swap');});
document.title = document.title.replace("Move", "Swap");
new mw.Api().parse(config.introText).done(function (parsedText) {
$( '#movepagetext' ).replaceWith( $($.parseHTML(parsedText)) );
}).fail(function (codetr, reslttr) {
console.warn( "Error parsing wikitext:\n\n" + config.introText + "\n\n" +
(reslttr.error.info || (codetr + ".")) );
$( '#movepagetext' ).html( config.introText );
});
var reasonList = [];
if ($( '#wpReasonList' )[0]) {
reasonList.push({
data: $( '#wpReasonList' ).children("option").get(0).value,
label: $( '#wpReasonList' ).children("option").get(0).text
});
reasonList.push({optgroup: $( '#wpReasonList' ).children("optgroup").get(0).label});
$( '#wpReasonList' ).children("optgroup").children("option").get().forEach(
option => reasonList.push({data: option.value, label: option.text})
);
}
var psFieldset = new OO.ui.FieldsetLayout({
label: 'Swap page', classes: ['container'], id: 'psFieldset'
}),
psOldTitle = titleInput(params.currTitle),
psNewTitle = titleInput(params.destTitle),
psReasonList = new OO.ui.DropdownInputWidget({
options: reasonList, id: 'psReasonList', $overlay: true
}),
psReasonOther = new OO.ui.TextInputWidget({value: moveReason, id: 'psReasonOther'}),
psMovetalk = new OO.ui.CheckboxInputWidget({selected: params.defaultMoveTalk, id: 'psMovetalk'}),
psMoveSubpages = new OO.ui.CheckboxInputWidget({selected: true, id: 'psMoveSubpages'}),
psTalkRedirect = new OO.ui.CheckboxInputWidget({selected: params.cleanup, id: 'psTalkRedirect'}),
psFixSelfRedirect = new OO.ui.CheckboxInputWidget({selected: params.cleanup, id: 'psFixSelfRedirect'}),
psWatch = new OO.ui.CheckboxInputWidget({selected: false, id: 'psWatch'}),
psConfirm = new OO.ui.MessageWidget({type: 'notice', showClose: false, id: 'psConfirm'}),
psSwap = new OO.ui.ButtonInputWidget({
label: config.swapButton,
disabled: true, framed: true,
flags: ['primary','progressive'],
id: 'psSwap'
}),
psStatus = new OO.ui.MessageWidget({type: 'notice', showClose: true, id: 'psStatus'}),
psContribsButton = new OO.ui.ButtonWidget({
label: 'Open contribs page', title: 'Special:MyContributions',
href: mw.config.get("wgServer") +
mw.config.get("wgArticlePath").replace("$1", "Special:MyContributions"),
framed: true, flags: ['primary', 'progressive'],
id: 'psContribsButton', target: '_blank'
});
psFieldset.addItems( [
new OO.ui.FieldLayout(psOldTitle, {align: 'top', label: 'Old title:', id: 'psOldTitle'}),
new OO.ui.FieldLayout(psNewTitle, {align: 'top', label: 'New title:', id: 'psNewTitle'}),
new OO.ui.FieldLayout(psReasonList, {align: 'top', label: 'Reason:'}),
new OO.ui.FieldLayout(psReasonOther, {align: 'top', label: 'Other/additional reason:'}),
new OO.ui.FieldLayout(psMovetalk, {align: 'inline',
label: 'Move associated talk page',
title: 'Move associated talk page'
}),
new OO.ui.FieldLayout(psMoveSubpages, {align: 'inline',
label: 'Move subpages',
title: 'Move up to 100 subpages of the source and/or target pages'
}),
new OO.ui.FieldLayout(psTalkRedirect, {align: 'inline',
label: 'Leave a redirect to new talk page if needed',
title: 'If one of the pages you\'re swapping has a talk page and ' +
'the other doesn\'t, create a redirect from the missing talk ' +
'page to the new talk page location. This is useful when ' +
'swapping a page with its redirect so that links to the old ' +
'talk page will continue to work.'
}),
new OO.ui.FieldLayout(psFixSelfRedirect, {align: 'inline',
label: 'Fix self-redirects',
title: 'When swapping a page with its redirect, update the ' +
'redirect to point to the new page name so that it is not ' +
'pointing to itself. This will not update redirects on subpages.'
}),
new OO.ui.FieldLayout(psWatch, {align: 'inline',
label: 'Watch source page and target page',
title: 'Add both source page and target page to your watchlist'
}),
new OO.ui.FieldLayout(psConfirm, {}),
new OO.ui.FieldLayout(psSwap, {}),
new OO.ui.FieldLayout(psStatus, {}),
new OO.ui.FieldLayout(psContribsButton, {})
]);
function checkTitles() {
if (psOldTitle.namespace.value%2==1 || psNewTitle.namespace.value%2==1) {
if (psMovetalk.isDisabled() == false) {
psMovetalk.setDisabled(true);
params.defaultMoveTalk = psMovetalk.isSelected();
psMovetalk.setSelected(false);
}
} else if (psMovetalk.isDisabled()) {
psMovetalk.setDisabled(false);
psMovetalk.setSelected(params.defaultMoveTalk);
}
psConfirm.toggle(false).setType('notice');
params.currTitle = assembleTitle(psOldTitle);
params.destTitle = assembleTitle(psNewTitle);
var titlesMatch = (params.currTitle.title==params.destTitle.title);
psOldTitle.title.setValidityFlag(psOldTitle.title.value!='' && !titlesMatch );
psNewTitle.title.setValidityFlag(psNewTitle.title.value!='' && !titlesMatch );
psSwap.setLabel(config.swapButton).off('click').on('click', clickSwap
).setDisabled(psOldTitle.title.value=='' || psNewTitle.title.value=='' || titlesMatch );
}
function clickSwap() {
psConfirm.toggle(false).setType('notice');
psStatus.toggle(false).setType('notice');
psSwap.setDisabled(true);
Object.assign(params, params, {
confirmMessages: [],
statusMessages: [],
currTitle: assembleTitle(psOldTitle),
destTitle: assembleTitle(psNewTitle),
moveReason: psReasonOther.value,
moveTalk: psMovetalk.isDisabled() ? false : psMovetalk.selected,
moveSubpages: psMoveSubpages.selected,
talkRedirect: psTalkRedirect.selected,
fixSelfRedirect: psFixSelfRedirect.selected,
watch: psWatch.selected ? 'watch' : 'unwatch',
});
if (psReasonList.value != 'other') {
params.moveReason = psReasonList.value +
(psReasonOther.value == '' ? '' : '. ' + psReasonOther.value);
} else if (psReasonOther.value == '') {
params.moveReason = 'Swap [[' + params.currTitle.title + ']] and [[' +
params.destTitle.title + ']] ([[WP:SWAP]])';
}
roundrobin();
}
checkTitles();
/**
* Re-check form on any change
*/
psOldTitle.namespace.off('change').on( 'change', checkTitles );
psOldTitle.title.setValidation( function(v) {
checkTitles(); return (v!='' && params.currTitle.title!=params.destTitle.title);
} );
psNewTitle.namespace.off('change').on( 'change', checkTitles );
psNewTitle.title.setValidation( function(v) {
checkTitles(); return (v!='' && params.currTitle.title!=params.destTitle.title);
} );
psReasonList.off('change').on( 'change', checkTitles );
psReasonOther.off('change').on( 'change', checkTitles );
psMovetalk.off('change').on( 'change', checkTitles );
psMoveSubpages.off('change').on( 'change', checkTitles );
psTalkRedirect.off('change').on( 'change', checkTitles );
psFixSelfRedirect.off('change').on( 'change', checkTitles );
psWatch.off('change').on( 'change', checkTitles );
/**
* Set button and status field actions
*/
psSwap.off('click').on( 'click', clickSwap );
psStatus.off('close').on( 'close', function() {
params.statusMessages = [];
psStatus.setType('notice');
psContribsButton.toggle(false);
} ).off('toggle').on( 'toggle', function() {
if (!psStatus.isVisible()) {
params.statusMessages = [];
psStatus.setType('notice');
psContribsButton.toggle(false);
}
} );
psConfirm.toggle(false);
psStatus.toggle(false);
$( '#movepage' ).hide(); //hide old form
$( '#movepage-loading' ).remove(); //remove loading message
$( "div.mw-message-box-error" ).hide(); //hide error message
$( '#psFieldset' ).remove(); //remove old form if script started twice
$( "div.movepage-wrapper" ).prepend( psFieldset.$element ); //add swap form
if( !$( '#psFieldset' ).length ){ //something went wrong
mw.notify(config.errorMsg, {type: 'error', title: "Error:" });
document.getElementById("mw-movepage-table").style.display="block";
$( '#movepage' ).show();
$( "div.mw-message-box-error" ).show();
}
return true;
}