User:Ahecht/sandbox/Scripts/pageswap-core.js
Appearance
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. |
Documentation for this user script can be added at User:Ahecht/sandbox/Scripts/pageswap-core. |
//jshint -W083
function pageSwap(prefix, moveReason, debug) {
var config = {
link: "using [[" + prefix + "|Pageswap GUI]])",
intermediatePrefix: "Draft:Move/",
portletLink: "Swap (sandbox)" + (debug ? " (debug)" : ""),
portletAlt: "Perform a revision history swap / round-robin move",
validateButton: 'Validate page swap (sandbox)' + (debug ? " (debug)" : ""),
validatingButton: 'Validating page swap (sandbox)' + (debug ? " (debug)" : ""),
confirmButton: 'Confirm (sandbox)' + (debug ? " (debug)" : ""),
confirmMessageHeader: "'''Round-robin configuration:'''\n*",
confirmMessageFooter: '\nPress "Confirm" to proceed.',
statusMessageHeader: "'''Performing page swap:'''\n",
introText: "<big>'''Please post bug reports/comments/suggestions for " +
'the Pageswap GUI script at [[User talk:Ahecht]]. To revert to the ' +
'previous dialogue-based version of this script, use ' +
"[[User:Ahecht/Scripts/pageswap_1.5.2.js]] instead.'''</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 GUI]] script, moving all of their histories to ' +
"the new names. '''Links to the old page titles will not be " +
"changed'''. Be sure to check '''[[Special:MyContributions]]''' " +
'for [[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 subpage(s) were moved, and may need new ' +
'or updated redirects:\n',
errorMsg: 'Error adding swap form to page!',
rrReason: ' ([[WP:Page mover#rr|Round-robin swap]] step 1 ',
newRedirMsg: 'The following redirect(s) will be created or modified '+
'([[#rcat|choose redirect categories]]):',
rcatShell: '{{Redirect category shell|\n$1\n}}',
rcatDefault: '{{R from move}}',
rcatChoose: 'Choose redirect categories for the newly created redirects:',
rcatsAdded: '* The following redirect categories will be added where possible: ',
rcatCat: 'Category:Redirect templates',
rcatTempNSRegEx: new RegExp("\\|\\s*(\\S*?) category\\s*=", "g"),
types: ['notice', 'success', 'warning', 'error'],
}, params = {
apiData: {}, currTitle: {}, destTitle: {},
confirmMessages: [], statusMessages: [], selfRedirs: [], rcats: [],
selectedRcats: { [config.rcatDefault]: ["all"] },
defaultMoveTalk: true, confDone: false, editRedir: false, done: false,
busy: 0, idempotency: {psConfirm: 0, psStatus: 0},
cleanup: (
typeof pagemoveDoPostMoveCleanup === 'undefined' ?
true :
pagemoveDoPostMoveCleanup
)
};
function filterHtml(rawHtml) {
$value=$($.parseHTML(rawHtml));
$value.filter("div.mw-parser-output").contents().each(function() {
if(this.nodeType === Node.COMMENT_NODE || this.nodeType === Node.TEXT_NODE) {
$(this).remove();
}
}).find('a.mw-redirect').each(function() {
$(this).attr('href', $(this).attr('href') + "?redirect=no");
});
return $value.html();
}
function setLabel(container, label, type, idempotency) {
if (config.types.indexOf(type) > config.types.indexOf(container.type)) {
container.setType(type);
}
label = new OO.ui.HtmlSnippet(label);
if (idempotency == params.idempotency[container.elementId]) {
container.setLabel(label).toggle(true).scrollElementIntoView().always( () => {
$( 'a[href="#rcat"]' ).off('click').on('click', (e) => {
e.preventDefault();
mw.loader.load('https://tools-static.wmflabs.org/cdnjs/ajax/libs/select2/4.0.13/css/select2.min.css', 'text/css');
mw.loader.getScript('https://tools-static.wmflabs.org/cdnjs/ajax/libs/select2/4.0.13/js/select2.min.js').then( () => {
if (params.rcats.length == 0) {
getRcats();
} else {
showRcatDialog();
}
} );
return false;
});
if (psContribsButton.isVisible() && !psContribsButton.isDisabled()) {
psContribsButton.scrollElementIntoView();
} else if (psButton.isVisible() && !psButton.isDisabled()) {
psButton.scrollElementIntoView();
}
} );
}
}
function parseError(ps, label, codetr, reslttr, idempotency) {
label = "Error parsing wikitext:\n\n" + label + "\n\n" +
(reslttr.error.info || (codetr + "."));
console.warn(label);
setLabel(ps, label, 'error', idempotency);
}
function showConfirm(message, type='notice', done=false) {
if (done) params.confDone = true;
var idempotency = ++params.idempotency.psConfirm;
if (message !== '') {
params.confirmMessages.push(message.replace("[[WP:RM/TR]]",
"[[WP:Requested moves/Technical requests|WP:RM/TR]]"));
}
var label = config.confirmMessageHeader +
params.confirmMessages.join("\n*") +
(params.confDone ? config.confirmMessageFooter : '');
new mw.Api().parse(label).done( (parsedText) => {
setLabel(psConfirm, filterHtml(parsedText), type, idempotency);
} ).fail( (codetr, reslttr) =>
parseError(psConfirm, label, codetr, reslttr, idempotency)
);
if (type=='error') psProgress.toggle(false);
}
function showStatus(message, type='notice', done=false, topic=false) {
var idempotency = ++params.idempotency.psStatus;
if (done) params.done = true;
if (message !== '') {
var topicFlag = topic ? "<!--"+topic+"-->" : false;
var topicIndex = params.statusMessages.findIndex((str) => str.indexOf(topicFlag) > -1);
message = "*" + 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) {
if (params.allSpArr.length) doneSubpagesMessage = config.doneSubpages + "**[[" +
params.allSpArr.join("]]\n**[[") + "]]\n";
psContribsButton.toggle(true);
var doneMessages = [config.doneMsgCleanup];
if (!params.talkRedirect || params.moveSubpages) doneMessages.push(config.doneMsgRedlink);
if (!params.fixSelfRedirect || params.moveSubpages) doneMessages.push(config.doneMsgRedir);
if (doneMessages.length < 3) {
doneMessage = doneMessages.join(" and ") + ".";
} else {
doneMessage = doneMessages.slice(0, -1).join(', ') + ', and ' +
doneMessages.slice(-1) + ".";
}
type = 'success';
}
var label = config.statusMessageHeader + params.statusMessages.join('') +
doneSubpagesMessage + doneMessage;
new mw.Api().parse(label).done(
(parsedText) => setLabel(psStatus, filterHtml(parsedText), type, idempotency)
).fail(
(codetr, reslttr) => parseError(psStatus, label, codetr, reslttr, idempotency)
);
}
function parsePagesData() {
// get page data, normalize titles
var ret = {valid: true, invalidReason: ''};
var query = params.apiData;
if (typeof query.pages !== 'undefined' && typeof query.logevents !== 'undefined') {
for (var kn in query.normalized) {
var qn = query.normalized[kn];
if (params.currTitle.title == qn.from) {
params.currTitle.title = qn.to;
} else if (params.destTitle.title == qn.from) {
params.destTitle.title = qn.to;
}
}
for (var kp in query.pages) {
var qp = query.pages[kp];
if ([params.currTitle.title,params.destTitle.title].includes(qp.title)) {
if (params.currTitle.title == qp.title) {
params.currTitle = qp;
} else if (params.destTitle.title == qp.title) {
params.destTitle = qp;
}
if (kp < 0) {
ret.valid = false;
if (typeof qp.missing !== 'undefined') {
ret.invalidReason += "Unable to find [["+qp.title+"]]. ";
} else if (typeof qp.invalid !== 'undefined' &&
typeof qp.invalidreason !== 'undefined') {
ret.invalidReason += qp.invalidreason;
} else {
ret.invalidReason += "Unable to get page data for"+params.titlesString;
}
}
}
}
for (var kl in query.logevents) {
var lastMove = (Date.now()-Date.parse(query.logevents[kl].timestamp))/(1000*60);
if ( lastMove < 60 ) { // 1 hour
showConfirm("<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(ret) {
// get page data, normalize titles
if (ret.valid === false || params === null ||
params.currTitle.title === null || params.destTitle.title === null
) {
ret.valid = false;
ret.invalidReason += "Failed to validate swap.";
return ret;
}
ret.allowMoveSubpages = true;
ret.checkTalk = true;
for (const k of ["currTitle", "destTitle"]) {
if (k == "-1" || params[k].ns < 0) {
ret.valid = false;
ret.invalidReason = ("Page " + params[k].title + " does not exist.");
return ret;
}
// enable only in ns 0..5,12,13,118,119 (Main,Talk,U,UT,WP,WT,H,HT,D,DT)
if ((params[k].ns >= 6 && params[k].ns <= 9) ||
(params[k].ns >= 10 && params[k].ns <= 11 && !params.uPerms.allowSwapTemplates) ||
(params[k].ns >= 14 && params[k].ns <= 117) ||
(params[k].ns >= 120)) {
ret.valid = false;
ret.invalidReason = ("Namespace of " + params[k].title + " (" +
params[k].ns + ") not supported.\n\nLikely reasons:\n" +
"- Names of pages in this namespace relies on other pages\n" +
"- Namespace features heavily-transcluded pages\n" +
"- Namespace involves subpages: swaps produce many redlinks\n" +
"\n\nIf the move is legitimate, consider a careful manual swap.");
return ret;
}
ret[k] = params[k].title;
ret[k.slice(0,4)+"Ns"] = params[k].ns;
ret[k.slice(0,4)+"CanMove"] = params[k].actions.move === '';
ret[k.slice(0,4)+"IsRedir"] = params[k].redirect === '';
}
if (!ret.valid) return ret;
if (!ret.currCanMove) {
ret.valid = false;
ret.invalidReason = ('' + ret.currTitle + " is immovable. Aborting");
return ret;
}
if (!ret.destCanMove) {
ret.valid = false;
ret.invalidReason = ('' + ret.destTitle + " is immovable. Aborting");
return ret;
}
if (ret.currNs % 2 !== ret.destNs % 2) {
ret.valid = false;
ret.invalidReason = "Namespaces don't match: one is a talk page.";
return ret;
}
ret.currNsAllowSubpages = params.apiData.namespaces['' + ret.currNs].subpages !== '';
ret.destNsAllowSubpages = params.apiData.namespaces['' + ret.destNs].subpages !== '';
// if same namespace (subpages allowed), if one is subpage of another,
// disallow movesubpages
if (ret.currTitle.startsWith(ret.destTitle + '/') ||
ret.destTitle.startsWith(ret.currTitle + '/')) {
if (ret.currNs !== ret.destNs) {
ret.valid = false;
ret.invalidReason = "Strange.\n" + ret.currTitle + " in ns " +
ret.currNs + "\n" + ret.destTitle + " in ns " + ret.destNs +
". Disallowing.";
return ret;
}
ret.allowMoveSubpages = ret.currNsAllowSubpages;
if (!ret.allowMoveSubpages)
ret.addlInfo = "One page is a subpage. Disallowing move-subpages";
}
if (ret.currNs % 2 === 1) {
ret.checkTalk = false; // no need to check talks, already talk pages
} else { // ret.checkTalk = true;
ret.currTitleWithoutPrefix = mw.Title.newFromText( ret.currTitle ).title;
ret.currTalkName = mw.Title.newFromText( ret.currTitle ).getTalkPage().getPrefixedText();
ret.destTitleWithoutPrefix = mw.Title.newFromText( ret.destTitle ).title;
ret.destTalkName = mw.Title.newFromText( ret.destTitle ).getTalkPage().getPrefixedText();
}
return ret;
}
/**
* Given two talk page titles (may be undefined), retrieves their pages for comparison
* Assumes that talk pages always have subpages enabled.
* Assumes that pages are not identical (subject pages were already verified)
* Assumes namespaces are okay (subject pages already checked)
* (Currently) assumes that the malicious case of subject pages
* not detected as subpages and the talk pages ARE subpages
* (i.e. A and A/B vs. Talk:A and Talk:A/B) does not happen / does not handle
* Returns structure indicating whether move talk should be allowed
*/
function talkValidate(checkTalk, talk1, talk2) {
var ret = {allowMoveTalk: true};
if (!checkTalk) return ret; // currTitle destTitle already talk pages
if (talk1 === undefined || talk2 === undefined) ret.allowMoveTalk = false;
ret.currTDNE = true;
ret.destTDNE = true;
ret.currTCanCreate = true;
ret.destTCanCreate = true;
var talkTitleArr = [talk1, talk2];
if (talkTitleArr.length !== 0 && typeof params.apiData?.pages !== 'undefined') {
var talkData = params.apiData.pages;
for (var id in talkData) {
if (talkData[id].title === talk1) {
ret.currTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
ret.currTTitle = talkData[id].title;
ret.currTCanMove = talkData[id].actions.move === '';
ret.currTCanCreate = talkData[id].actions.create === '';
ret.currTalkIsRedir = talkData[id].redirect === '';
} else if (talkData[id].title === talk2) {
ret.destTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
ret.destTTitle = talkData[id].title;
ret.destTCanMove = talkData[id].actions.move === '';
ret.destTCanCreate = talkData[id].actions.create === '';
ret.destTalkIsRedir = talkData[id].redirect === '';
}
}
} else {
ret.allowMoveTalk = false;
}
if (!ret.allowMoveTalk) {
showStatus("Unable to validate talk. Disallowing movetalk to be safe.", 'warning');
} else {
ret.allowMoveTalk = (ret.currTCanCreate && ret.currTCanMove) &&
(ret.destTCanCreate && ret.destTCanMove);
}
if (params.moveTalk && params.talkRedirect) {
if (ret.currTDNE && !ret.destTDNE) {
ret.redirFromTalk = talk2;
ret.redirToTalk = talk1;
} else if (ret.destTDNE && !ret.currTDNE) {
ret.redirFromTalk = talk1;
ret.redirToTalk = talk2;
}
}
return ret;
}
/**
* Given existing title (not prefixed with "/"), optionally searching for talk,
* finds subpages (incl. those that are redirs) and whether limits are exceeded
*/
function getSubpages(title, isTalk) {
var deferred = $.Deferred();
var titleObj = isTalk ? mw.Title.newFromText( title ).getTalkPage() :
mw.Title.newFromText( title );
var nsSubpages = params.apiData.namespaces['' + titleObj.namespace].subpages;
if ((!titleObj.isTalkPage()) && nsSubpages !== '') {
deferred.resolve( [] );
} else {
var queryData = { format:'json', action:'query',
prop:'info', intestactions:'move|create',
generator:'allpages', gapprefix:titleObj.title + '/',
gapnamespace:titleObj.namespace, gaplimit:101,
};
new mw.Api().get(queryData).done( (subpages) => {
if ( typeof subpages !== 'object' ) {
deferred.reject( "API did not return data for subpages of "+title+". Subpages may exist." );
} else if (typeof subpages?.query?.pages === 'undefined') {
if (subpages.batchcomplete === '') { //no subpages found
deferred.resolve( [] );
} else { //something else went wrong
console.warn("API did not return 'pages' when querying subpage data:");console.log(subpages);
deferred.reject( "API did not return subpage data for "+title+". Subpages may exist." );
}
} else if (Object.keys(subpages.query.pages).length > 101) {
deferred.reject( "100+ subpages of "+title+". Aborting" );
} else {
subpages = subpages.query.pages;
var dataret = [];
for (var k in subpages) {
dataret.push( {
title:subpages[k].title,
isRedir:subpages[k].redirect === '',
canMove:subpages[k].actions.move === ''
} );
}
deferred.resolve( dataret );
}
} ).fail( (jqXHR, textStatus, errorThrown) => {
var errStr = "API error '"+(jqXHR.status||textStatus)+
"' when searching for subpages of "+title+". "+
(errorThrown||jqXHR.responseText).replace("\n","");
console.warn(errStr);console.log(queryData);console.log(jqXHR);
deferred.reject( errStr+" Subpages may exist." );
} );
}
return deferred.promise();
}
/**
* Prints subpage data given retrieved subpage information returned by getSubpages
* Returns a suggestion whether movesubpages should be allowed
*/
function printSubpageInfo(basepage, currSp) {
var ret = {};
var currSpArr = [];
var currSpCannotMove = [];
var redirCount = 0;
for (var kcs in currSp) {
if (!currSp[kcs].canMove) currSpCannotMove.push(currSp[kcs].title);
currSpArr.push(currSp[kcs].title);
if (currSp[kcs].isRedir) redirCount++;
}
if (params.moveSubpages) {
if (currSpArr.length > 0) {
if (currSpCannotMove.length > 0) {
showConfirm("Disabling move-subpages." +
"The following " + currSpCannotMove.length + " (of " +
currSpArr.length + ") total subpage(s) of [[" +
basepage + "]] CANNOT be moved:\n**[[" +
currSpCannotMove.join("]]\n**[[") + "]]",
'warning');
} else if (typeof basepage !== 'undefined') {
showConfirm(currSpArr.length + " total subpages of [[" + basepage + "]]" +
(redirCount !== 0 ? (" (" + redirCount + " redirects):") : ":") +
"\n**[[" + currSpArr.join("]]\n**[[") + "]]");
}
}
}
ret.allowMoveSubpages = currSpCannotMove.length === 0;
ret.noNeed = currSpArr.length === 0;
ret.spArr = currSpArr;
return ret;
}
var filterRcats = (ns) => ( Object.keys(params.selectedRcats).filter(
(e) => ( params.selectedRcats[e].some(
(v) => (v == 'all' || v == 'other' || v == 'unknown' || v == ns)
) )
) );
function createMissingTalk(vData, vTData) {
var fromTalk = vTData.redirFromTalk, toTalk = vTData.redirToTalk;
if (fromTalk && toTalk) {
params.busy++;
setTimeout( () => {
var talkRedirect = {
action:'edit',
title:fromTalk,
createonly: true,
text: "#REDIRECT [[" + toTalk + "]]\n\n" +
config.rcatShell.replace( '$1', filterRcats('talk').join('\n') ),
summary: "Create redirect to [[" + toTalk + "]] (" + config.link,
watchlist: params.watch
};
showStatus("Creating talk page redirect [[" + fromTalk +
"]] → [[" + toTalk + "]]...",'notice', false,
"TPR" + fromTalk);
if (debug) {
params.busy--;
showStatus("* Talk page redirect simulated!.",
'notice', true, "TPR" + fromTalk);
} else {
new mw.Api().postWithEditToken(talkRedirect).done( () => {
params.busy--;
showStatus("* Talk page redirect created!",
'notice', true, "TPR" + fromTalk);
} ).fail( (codetr, reslttr) => {
params.busy--;
showStatus("* Failed to create redirect! " +
(reslttr.error.info || (codetr + ".")),
'error', true, "TPR" + fromTalk);
} );
}
}, 250);
} else { showStatus('', 'notice', true); }
}
function retargetRedirect(thisPage, otherPage, newText) {
params.busy++;
showStatus("Retargeting redirect at [[" + thisPage +
"]] to [[" + otherPage + "]]...",
'notice', false, "RT"+thisPage);
var retargetData = {
action:'edit',
title: thisPage,
text: newText,
summary : "Retarget redirect to [[" + otherPage + "]] (" +
config.link,
watchlist: params.watch
};
if (debug) {
params.busy--;
showStatus("* Retargeting simulated!",'notice', false, "RT"+thisPage);
} else {
new mw.Api().postWithEditToken(retargetData).done( (result, jqXHR) => {
params.busy--;
if (typeof result.edit !== 'undefined') {
params.busy++;
new mw.Api().get( {
action: 'query', prop: '', redirects: '',
titles: result.edit.title
} ).done( (data) => {
params.busy--;
if (data && typeof data?.query?.redirects !== 'undefined') {
showStatus("* Redirect retargeted!", 'notice',
false, "RT"+thisPage);
} else {
console.warn("Error parsing redirects after retargeting:");
console.warn(data);
}
} ).fail( (codeart, rsltart) => {
params.busy--;
console.warn("Error fetching page after retargeting:");
console.warn(codeart);console.warn(rsltart);
} );
} else {
console.warn("Error parsing result of retargeting:");
console.warn(result);console.warn(jqXHR);
}
} ).fail( (codert, resultrt) => {
params.busy--;
showStatus("* Retargeting failed. "+
(resultrt.error.info || (codert + ".")),
'error', false, "RT"+thisPage);
} );
}
}
function preCheckSelfRedirs(vData) {
var pagesArr = [vData.currTitle, vData.destTitle,
vData.currTalkName, vData.destTalkName];
var redirs = params.apiData.redirects;
for (const e in redirs) {
var thisI = pagesArr.indexOf(redirs[e].from);
if (thisI > -1) {
var otherI = (thisI==0)?1:((thisI==1)?0:((thisI==2)?3:2));
var otherPage = pagesArr[otherI];
if(redirs[e].to == otherPage) params.selfRedirs.push(redirs[e].to);
} else {
showConfirm('Page ' + redirs[e].from + ' from redirects table not found in input data.', 'warning');
}
}
}
/**
* After successful page swap, post-move cleanup:
* Make talk page redirect
* TODO more reasonable cleanup/reporting as necessary
* vData.(curr|dest)IsRedir
*/
function checkSelfRedirs(vData, vTData) {
var pagesArr = [vData.currTitle, vData.destTitle,
vData.currTalkName, vData.destTalkName];
var srQuery = {
action: "query", formatversion: "2", prop: "revisions|templates",
titles: pagesArr.filter(
(v) => params.selfRedirs.includes(v)
).join('|'),
rvprop: "content", rvslots: "main", rvsection: "",
tlnamespace: "10", tllimit: "max"
};
params.busy++;
new mw.Api().get( srQuery ).done( (queryData) => {
params.busy--;
if (queryData && queryData?.query?.pages?.[0]?.revisions[0] ) {
queryData.query.pages.forEach( (pageData) => {
var thisPage = pageData.title;
var thisI = pagesArr.indexOf(thisPage);
var otherI = (thisI==0)?1:((thisI==1)?0:((thisI==2)?3:2));
var otherPage = pagesArr[otherI];
var oldText = pageData?.revisions?.[0]?.slots?.main.content;
oldText = oldText ?? '';
var redirRE = new RegExp(
"^\\s*#REDIRECT\\s*\\[\\[ *.* *\\]\\]", "i"
);
if ((thisI > -1) && (oldText.search(redirRE) > -1)) {
var pageRcats = [];
if (pageData?.templates) {
pageData.templates.forEach( (v) => {
v = v.title;
params.rcats.some( (e) => {
if (e.id == v) return pageRcats.push(e.text), true;
} );
} );
}
var oldRcatL = pageRcats.length;
pageRcats = pageRcats.concat( //combine and dedupe
Object.keys(params.selectedRcats)
).filter((v, i, a) => a.indexOf(v) === i);
var thisNs = mw.Title.newFromText(thisPage).getNamespaceId();
thisNs = (thisNs == 0) ? 'main' : ( (thisNs % 2 == 1) ? 'talk' :
mw.config.get('wgFormattedNamespaces')[thisNs].toLowerCase() );
var newText = "";
if ( (pageRcats.length > 0) && (
(oldText.search('{'+'{') == -1) ||
(pageRcats.length != oldRcatL)
) ) { // Completely replace redirect text
newText = '#REDIRECT [['+otherPage+']]\n\n' +
config.rcatShell.replace('$1',
filterRcats(thisNs).join('\n'));
} else { // Just change target
newText = oldText.replace(redirRE,
'#REDIRECT [['+otherPage+']]');
}
retargetRedirect(thisPage, otherPage, newText);
} else {
showStatus("Attempt to retarget " +
"redirect at [[" + thisPage +
"]] to [[" + otherPage + "]] " +
"failed: String not found.",
'warning');
}
} );
} else {
params.busy--;
showStatus("Attempt to retarget redirect " +
"at [[" + thisPage + "]] to [[" +
otherPage + "]] failed: " +
"Could not fetch contents.", 'error');
}
} ).fail( (jqXHR, textStatus) => {
params.busy--;
showStatus("Attempt to retarget redirect at [[" +
pagesArr[i] + "]] failed due to API error '" +
(jqXHR.status||textStatus) + "' when " +
"fetching page contents. ", 'error');
} ).always( () => createMissingTalk(vData, vTData) );
}
/**
* Swaps the two pages (given all prerequisite checks)
* Optionally moves talk pages and subpages
*/
function swapPages(vData, vTData) {
params.busy = 1;
if (params.currTitle.title === null || params.destTitle.title === null ||
params.moveReason === null || params.moveReason === '') {
showStatus("Titles are null, or move reason given was empty. Swap not done", 'error');
return false;
}
var currTitle = params.currTitle.title;
var intermediateTitle = config.intermediatePrefix + currTitle;
var destTitle = params.destTitle.title;
if (debug) {
showStatus("Simulating round-robin history swap...");
showStatus("* Step 1 ([[" + destTitle + "]] → [[" +
intermediateTitle + "]])...");
showStatus("* Step 2 ([[" + currTitle + "]] → [[" +
destTitle + "]])...");
showStatus("* Step 3 ([[" + intermediateTitle + "]] → [[" +
currTitle + "]])...");
var completeMessage = "* Round-robin history swap of [[" +
currTitle + "]] ([[Special:WhatLinksHere/" +
params.currTitle.title + "|links]]) and [[" +
destTitle + "]] ([[Special:WhatLinksHere/" +
params.destTitle.title + "|links]]) simulated successfully!";
if (params.fixSelfRedirect || params.talkRedirect) {
showStatus(completeMessage);
params.busy--;
if (params.fixSelfRedirect && params.selfRedirs.length > 0) {
checkSelfRedirs(vData, vTData);
} else {
createMissingTalk(vData, vTData);
}
} else {
params.busy--;
showStatus(completeMessage, 'notice', true);
}
} else {
showStatus("Doing round-robin history swap...");
var mQuery = { action:'move', from:destTitle, to:intermediateTitle,
reason:params.moveReason + config.rrReason + config.link,
watchlist:params.watch, noredirect:1 };
if (params.moveTalk) mQuery.movetalk = 1;
if (params.moveSubpages) mQuery.movesubpages = 1;
showStatus("* Step 1 ([[" + mQuery.from + "]] → [[" +
mQuery.to + "]])...");
new mw.Api().postWithEditToken(mQuery).then( () => {
Object.assign(mQuery, { from:currTitle, to:destTitle,
reason: params.moveReason + " (" + config.link } );
showStatus("* Step 2 ([[" + mQuery.from + "]] → [[" +
mQuery.to + "]])...");
return new mw.Api().postWithEditToken(mQuery);
} ).then( () => {
Object.assign(mQuery, { from:intermediateTitle, to:currTitle,
reason: params.moveReason + config.rrReason.slice(0,-2) +
"3 " + config.link } );
showStatus("* Step 3 ([[" + mQuery.from + "]] → [[" +
mQuery.to + "]])...");
return new mw.Api().postWithEditToken(mQuery);
} ).then( () => {
var completeMessage = "* Round-robin history swap of [[" +
currTitle + "]] ([[Special:WhatLinksHere/" +
currTitle + "|links]]) and [[" + destTitle +
"]] ([[Special:WhatLinksHere/" + destTitle +
"|links]]) completed successfully!";
if (params.fixSelfRedirect || params.talkRedirect) {
showStatus(completeMessage);
params.busy--;
if (params.fixSelfRedirect && params.selfRedirs.length > 0) {
checkSelfRedirs(vData, vTData);
} else {
createMissingTalk(vData, vTData);
}
} else {
params.busy--;
showStatus(completeMessage, 'notice', true);
}
} ).fail( (code, reslt) => {
params.busy--;
showStatus("* Failed when moving ([[" + mQuery.from + "]] → [[" +
mquery.to + "]])! " + (reslt.error.info || (code + ".")),
'error', true);
} );
}
}
/**
* Prompt for redirect categories for newly created redirects
*/
function showRcatDialog() {
var select = $( '<select>' ).attr( 'id', 'rcat-chooser-form' ).attr('multiple', 'multiple').append(
$( '<option>' ).attr( 'selected', 'selected' ).attr(
'value', config.rcatDefault.replace(/\{\{(.*)\}\}/, "Template:$1")
).text( config.rcatDefault )
);
var content = $( '<span>' ).append( '<p>' + config.rcatChoose + '</p>' ).append( select );
// Subclass ProcessDialog.
function ProcessDialog( config ) {
ProcessDialog.super.call( this, config );
}
OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
ProcessDialog.static.name = 'rcatDialog';
ProcessDialog.static.title = 'Select Redirect Categories';
ProcessDialog.static.actions = [
{
action: 'save',
label: 'Save',
flags: [ 'primary', 'progressive' ]
},
{
label: 'Cancel',
flags: [ 'safe', 'close' ]
}
];
ProcessDialog.prototype.initialize = function () {
ProcessDialog.super.prototype.initialize.apply( this, arguments );
this.content = new OO.ui.PanelLayout( {
padded: true,
expanded: false
} );
this.content.$element.append( content );
params.rcats.forEach( (v, i, a) => {a[i].selected = Object.keys(params.selectedRcats).includes(v.text);} );
select.select2({data: params.rcats, width: '100%'}).on( 'change', () => {rcatDialog.updateSize();} );
this.$body.append( this.content.$element );
};
ProcessDialog.prototype.getActionProcess = function ( action ) {
if ( action ) {
if (action == 'save') params.selectedRcats = {};
if (action == 'save' && select.val().length > 0) {
new mw.Api().get( {
"action": "query", "prop": "revisions", "formatversion": 2,
"titles": select.val().join('|'),
"rvprop": "content", "rvslots": "main"
} ).done( (data) => {
if (data && data?.query?.pages?.[0]) {
data.query.pages.forEach( (page) => {
var pageContent = page?.revisions?.[0]?.slots?.main?.content;
if (typeof pageContent === "string") {
var tempCall = page.title.replace(/Template:(.*)/, '{'+'{$1}}');
var nsMatches = Array.from(
pageContent.matchAll(config.rcatTempNSRegEx),
(v) => (v[1])
);
if (nsMatches.length == 0) nsMatches = ['unknown'];
params.selectedRcats[tempCall] = nsMatches;
}
} );
}
if (Object.keys(params.selectedRcats).length > 0) {
showConfirm(config.rcatsAdded + "<code><nowiki>" +
config.rcatShell.replace(
'$1', Object.keys(params.selectedRcats).join('\n')
) + "</nowiki></code>");
}
} ).fail( (jqXHR, textStatus) => {
showConfirm("* API error '"+(jqXHR.status||textStatus)+
"' when verifying Rcat templates.", 'error');
} );
}
return new OO.ui.Process(
() => this.close( {action: action} )
);
}
return ProcessDialog.super.prototype.getActionProcess.call( this, action );
};
ProcessDialog.prototype.getBodyHeight = function () {
return this.content.$element.outerHeight( true );
};
// Create and append the window manager and rcat dialog
var windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );
var rcatDialog = new ProcessDialog( {size: 'large'} );
windowManager.addWindows( [rcatDialog] );
windowManager.openWindow( rcatDialog );
// Workaround for lack of openOnEnter option in Select2 v4
var select2 = select.data('select2');
var origKeypressCbs = select2.listeners.keypress;
var keypressCb = function (evt) {
if (evt.key === 'Enter' && !select2.isOpen()) {
rcatDialog.executeAction('save');
return;
}
origKeypressCbs.forEach( (cb) => {cb(evt);} );
};
select2.listeners.keypress = [keypressCb];
}
/**
* Retrieve templates from "Category:Redirect templates"
*/
function getRcats(cont='', cmcont='') {
var query = {
action:'query', list:'categorymembers', cmlimit:'max',
cmtitle: config.rcatCat, cmsort:'sortkey', cmnamespace:10,
cmtype:'page', cmprop: 'title|sortkeyprefix', cmcontinue:cmcont,
continue:cont
};
new mw.Api().get( query ).done( (result) => {
if (result?.query?.categorymembers) {
result.query.categorymembers.forEach( (e) => {
var tTitle = mw.Title.newFromText(e.title).getMainText();
if ( tTitle.startsWith('R ') ) {
var sKey = e.sortkeyprefix.trim() == '' ?
tTitle.replace(/^R (from |to |with )?/, '') :
e.sortkeyprefix;
params.rcats.push( {sKey: sKey, id: e.title,
text: '{' + '{' + tTitle + '}}'} );
}
} );
if (result.continue) {
getRcats(result.continue.continue, result.continue.cmcontinue);
} else {
params.rcats.sort( (a, b) => {
return (a.sKey > b.sKey) ? 1 : ((a.sKey == b.sKey) ? 0 : -1 );
} );
showRcatDialog();
}
} else {console.warn('error');console.log(result);}
} ).fail( (e) => {console.warn(e)} );
}
/**
* Given two titles and talk/subpages,
* prompts user to confirm config before swapping the titles
*/
function confirmConfig(vData, currSpFlags, destSpFlags, currTSpFlags, destTSpFlags) {
var vTData = talkValidate(vData.checkTalk, vData.currTalkName, vData.destTalkName);
// future goal: check empty subpage DESTINATIONS on both sides (subj, talk)
// for create protection. disallow move-subpages if any destination is salted
var noSubpages = currSpFlags.noNeed && destSpFlags.noNeed &&
currTSpFlags.noNeed && destTSpFlags.noNeed;
// If one ns disables subpages, other enables subpages, AND HAS subpages,
// consider abort. Assume talk pages always safe (TODO fix)
var subpageCollision = (vData.currNsAllowSubpages && !destSpFlags.noNeed) ||
(vData.destNsAllowSubpages && !currSpFlags.noNeed);
// TODO future: currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages
// needs to be separate check. If talk subpages immovable, should not affect subjspace
if (params.moveSubpages) {
if (!subpageCollision && !noSubpages && vData.allowMoveSubpages &&
(currSpFlags.allowMoveSubpages && destSpFlags.allowMoveSubpages) &&
(currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages))
{
params.allSpArr = currSpFlags.spArr.concat(
destSpFlags.spArr,
currTSpFlags.spArr,
destTSpFlags.spArr
);
} else if (subpageCollision) {
params.moveSubpages = false;
showConfirm("One namespace does not have subpages enabled. Disallowing move subpages.",
'warning');
}
} else {
showConfirm("Moving subpages disabled.");
}
params.allSpArr = params.allSpArr ?? [];
// TODO: count subpages and make restrictions?
if (vData.checkTalk && (!vTData.currTDNE || !vTData.destTDNE || params.moveSubpages)) {
if (!vTData.allowMoveTalk) {
params.moveTalk = false;
showConfirm("Disallowing moving talk. " +
(!vTData.currTCanCreate ? (vData.currTalkName + " is create-protected. ")
: (!vTData.destTCanCreate ? (vData.destTalkName + " is create-protected. ")
: "Talk page is immovable.")), 'warning');
}
}
showConfirm("Swapping [["+params.currTitle.title+"]] → [["+params.destTitle.title+"]]");
showConfirm("Reason: "+params.moveReason);
if (debug) {
showConfirm("Move talk: "+params.moveTalk+", Move subpages: "+params.moveSubpages);
showConfirm("Talk redirect: "+params.talkRedirect+
", Fix self-redirect: "+params.fixSelfRedirect);
}
if (params.moveSubpages && params.allSpArr.length <= 0) showConfirm("No subpages found to move.");
if (params.fixSelfRedirect && params.apiData?.redirects) preCheckSelfRedirs(vData);
if ( (params.selfRedirs.length > 0) ||
(vTData.redirFromTalk && vTData.redirToTalk) ) {
params.editRedir = true;
showConfirm(config.newRedirMsg);
if (vTData.redirFromTalk && vTData.redirToTalk) {
showConfirm("* Redirect from [[" + vTData.redirFromTalk +
"]] → [[" + vTData.redirToTalk + "]] will be created.");
}
for (const t in params.selfRedirs) {
showConfirm("* Self-redirect at [[" + params.selfRedirs[t] +
"]] will be re-targeted.");
}
}
psProgress.toggle(false);
showConfirm('', 'notice', true);
psButton.setDisabled(false).setLabel(config.confirmButton).off('click').on('click', () => {
psButton.setDisabled(true).setLabel(config.validateButton);
swapPages(vData, vTData);
} );
}
/**
* Given two titles, gathers data on talk/subpages,
* then passes that to confirmConfig()
*/
function gatherSubpageData() {
var currSpFlags, destSpFlags, currTSpFlags, destTSpFlags;
// validate namespaces, not identical, can move
var ret = parsePagesData();
const vData = swapValidate(ret);
if (!vData.valid) {
showConfirm(vData.invalidReason, 'error');
return;
}
if (vData.addlInfo !== undefined) showConfirm(vData.addlInfo, 'warning');
// subj subpages
getSubpages(vData.currTitle, false).done( (cData) => {
currSpFlags = printSubpageInfo(vData.currTitle, cData);
return getSubpages(vData.destTitle, false);
} ).then( (dData) => {
destSpFlags = printSubpageInfo(vData.destTitle, dData);
// talk subpages
return getSubpages(vData.currTitle, true);
} ).then( (cTData) => {
currTSpFlags = printSubpageInfo(vData.currTalkName, cTData);
return getSubpages(vData.destTitle, true);
} ).then( (dTData) => {
destTSpFlags = printSubpageInfo(vData.destTalkName, dTData);
confirmConfig(vData, currSpFlags, destSpFlags, currTSpFlags, destTSpFlags);
} ).fail( (error) => showConfirm(error, 'error') );
}
function titleInput(title) {
var nsObj = {value: title.ns || 0, $overlay: true};
var tObj = {value: title.title || '', $overlay: true};
if (typeof title.ns !== 'undefined' && typeof title.title !== 'undefined') {
var re = '^'+mw.config.get("wgFormattedNamespaces")[title.ns]+':';
tObj.value = title.title.replace(new RegExp(re),'');
}
return new mw.widgets.ComplexTitleInputWidget( {namespace: nsObj, title: tObj} );
}
/**
* Determine namespace of title
*/
function psParseTitle(data) {
data = (typeof data === 'object')
? mw.Title.makeTitle(data.namespace.value, data.title.value)
: mw.Title.newFromText(data);
return data ? {ns: data.namespace, title: data.getPrefixedText()} : null;
}
/**
* If user is able to perform swaps
*/
function checkUserPermissions() {
var ret = {};
ret.canSwap = true;
var reslt = $.ajax( {
url: mw.util.wikiScript('api'), async:false,
error: (jsondata) => {
mw.notify("Swapping pages unavailable.", { title: 'Page Swap Error', type: 'error' } );
return ret;
},
data: { action:'query', format:'json', meta:'userinfo', uiprop:'rights' }
} ).responseJSON.query.userinfo;
// check userrights for suppressredirect and move-subpages
var rightslist = reslt.rights;
ret.canSwap =
$.inArray('suppressredirect', rightslist) > -1 &&
$.inArray('move-subpages', rightslist) > -1;
ret.allowSwapTemplates =
$.inArray('templateeditor', rightslist) > -1;
return ret;
}
/**
* Script execution starts here:
*/
//Read the old title from the URL or the relevant pagename
params.currTitle.title = mw.util.getParamValue("wpOldTitle") || mw.config.get("wgRelevantPageName") || '';
if (document.getElementsByName("wpOldTitle")[0] &&
document.getElementsByName("wpOldTitle")[0].value != ''
){
//If the hidden form field element has a value, use that instead
params.currTitle.title = document.getElementsByName("wpOldTitle")[0].value;
}
//Parse out title and namespace
params.currTitle = psParseTitle(params.currTitle.title) || {ns: 0, title: params.currTitle.title};
//Read the new title from the URL or make it blank
params.destTitle.title = mw.util.getParamValue("wpNewTitle") || '';
//Parse out title and namespace
params.destTitle = psParseTitle(params.destTitle.title) || {ns: 0, title: params.destTitle.title};
if (document.getElementsByName("wpNewTitleMain")[0] &&
document.getElementsByName("wpNewTitleMain")[0].value != '' &&
document.getElementsByName("wpNewTitleNs")[0]
){
//If the Move page form exists, use the values from that instead
params.destTitle.title = document.getElementsByName("wpNewTitleMain")[0].value;
params.destTitle.ns = document.getElementsByName("wpNewTitleNs")[0].value;
if (params.destTitle.ns != 0) {
params.destTitle.title = mw.config.get("wgFormattedNamespaces")[params.destTitle.ns] +
":" + params.destTitle.title;
}
}
params.uPerms = checkUserPermissions();
if (!params.uPerms.canSwap) {
mw.loader.using( [ 'mediawiki.notification' ], () => {
mw.notify("User rights insufficient for action.", { title: 'Page Swap Error', type: 'error' } );
return;
} );
}
$( '#firstHeading' ).text( (i, t) => (t.replace('Move', 'Swap')) );
document.title = document.title.replace("Move", "Swap");
new mw.Api().parse(config.introText).done( (parsedText) => {
$( '#movepagetext' ).replaceWith( $($.parseHTML(parsedText)) );
} ).fail( (codetr, reslttr) => {
console.warn( "Error parsing wikitext:\n\n" + config.introText + "\n\n" +
(reslttr.error.info || (codetr + ".")) );
$( '#movepagetext' ).html( config.introText );
} );
var reasonList = [];
if ($( '#wpReasonList' )[0]) {
reasonList.push( {
data: $( '#wpReasonList' ).children("option").get(0).value,
label: $( '#wpReasonList' ).children("option").get(0).text
} );
reasonList.push( {optgroup: $( '#wpReasonList' ).children("optgroup").get(0).label} );
$( '#wpReasonList' ).children("optgroup").children("option").get().forEach(
option => reasonList.push( {data: option.value, label: option.text} )
);
}
var psFieldset = new OO.ui.FieldsetLayout( {
label: 'Swap page', classes: ['container'], id: 'psFieldset'
} ),
psOldTitle = titleInput(params.currTitle),
psNewTitle = titleInput(params.destTitle),
psReasonList = new OO.ui.DropdownInputWidget( {
options: reasonList, id: 'psReasonList', $overlay: true
} ),
psReasonOther = new OO.ui.TextInputWidget( {value: moveReason, id: 'psReasonOther'} ),
psMovetalk = new OO.ui.CheckboxInputWidget( {selected: params.defaultMoveTalk, id: 'psMovetalk'} ),
psMoveSubpages = new OO.ui.CheckboxInputWidget( {selected: true, id: 'psMoveSubpages'} ),
psTalkRedirect = new OO.ui.CheckboxInputWidget( {selected: params.cleanup, id: 'psTalkRedirect'} ),
psFixSelfRedirect = new OO.ui.CheckboxInputWidget( {selected: params.cleanup, id: 'psFixSelfRedirect'} ),
psWatch = new OO.ui.CheckboxInputWidget( {selected: false, id: 'psWatch'} ),
psConfirm = new OO.ui.MessageWidget( {type: 'notice', showClose: false, id: 'psConfirm'} ),
psButton = new OO.ui.ButtonInputWidget( {
label: config.validateButton,
disabled: true, framed: true,
flags: ['primary','progressive'],
id: 'psButton'
} ),
psProgress = new OO.ui.ProgressBarWidget( {progress: false} ),
psStatus = new OO.ui.MessageWidget( {type: 'notice', showClose: true, id: 'psStatus'} ),
psContribsButton = new OO.ui.ButtonWidget( {
label: 'Open contribs page', title: 'Special:MyContributions',
href: mw.config.get("wgServer") +
mw.config.get("wgArticlePath").replace("$1", "Special:MyContributions"),
framed: true, flags: ['primary', 'progressive'],
id: 'psContribsButton', target: '_blank'
} );
psFieldset.addItems( [
new OO.ui.FieldLayout(psOldTitle, {align: 'top', label: 'Old title:', id: 'psOldTitle'} ),
new OO.ui.FieldLayout(psNewTitle, {align: 'top', label: 'New title:', id: 'psNewTitle'} ),
new OO.ui.FieldLayout(psReasonList, {align: 'top', label: 'Reason:'} ),
new OO.ui.FieldLayout(psReasonOther, {align: 'top', label: 'Other/additional reason:'} ),
new OO.ui.FieldLayout(psMovetalk, {align: 'inline',
label: 'Move associated talk page',
title: 'Move associated talk page'
} ),
new OO.ui.FieldLayout(psMoveSubpages, {align: 'inline',
label: 'Move subpages',
title: 'Move up to 100 subpages of the source and/or target pages'
} ),
new OO.ui.FieldLayout(psTalkRedirect, {align: 'inline',
label: 'Leave a redirect to new talk page if needed',
title: 'If one of the pages you\'re swapping has a talk page and ' +
'the other doesn\'t, create a redirect from the missing talk ' +
'page to the new talk page location. This is useful when ' +
'swapping a page with its redirect so that links to the old ' +
'talk page will continue to work.'
} ),
new OO.ui.FieldLayout(psFixSelfRedirect, {align: 'inline',
label: 'Fix self-redirects',
title: 'When swapping a page with its redirect, update the ' +
'redirect to point to the new page name so that it is not ' +
'pointing to itself. This will not update redirects on subpages.'
} ),
new OO.ui.FieldLayout(psWatch, {align: 'inline',
label: 'Watch source page and target page',
title: 'Add both source page and target page to your watchlist'
} ),
new OO.ui.FieldLayout(psConfirm, {} ),
new OO.ui.FieldLayout(psButton, {} ),
new OO.ui.FieldLayout(psProgress, {} ),
new OO.ui.FieldLayout(psStatus, {} ),
new OO.ui.FieldLayout(psContribsButton, {} )
]);
checkTitles();
/**
* Re-check form on any change
*/
psOldTitle.namespace.off('change').on( 'change', checkTitles );
psOldTitle.title.setValidation( (v) => {
checkTitles(); return (v!='' && params.currTitle.title!=params.destTitle.title);
} );
psNewTitle.namespace.off('change').on( 'change', checkTitles );
psNewTitle.title.setValidation( (v) => {
checkTitles(); return (v!='' && params.currTitle.title!=params.destTitle.title);
} );
psReasonList.off('change').on( 'change', checkTitles );
psReasonOther.off('change').on( 'change', checkTitles );
psMovetalk.off('change').on( 'change', checkTitles );
psMoveSubpages.off('change').on( 'change', checkTitles );
psTalkRedirect.off('change').on( 'change', checkTitles );
psFixSelfRedirect.off('change').on( 'change', checkTitles );
psWatch.off('change').on( 'change', checkTitles );
/**
* Set button and status field actions
*/
psButton.off('click').on( 'click', clickValidate );
psStatus.off('close').on( 'close', () => {
params.statusMessages = [];
psStatus.setType('notice');
psContribsButton.toggle(false);
} ).off('toggle').on( 'toggle', () => {
if (!psStatus.isVisible()) {
params.statusMessages = [];
psStatus.setType('notice');
psContribsButton.toggle(false);
}
} );
psConfirm.toggle(false);
psProgress.toggle(false);
psStatus.toggle(false);
$( '#movepage' ).hide(); //hide old form
$( '#movepage-loading' ).remove(); //remove loading message
$( "div.mw-message-box-error" ).hide(); //hide error message
$( '#psFieldset' ).remove(); //remove old form if script started twice
$( "div.movepage-wrapper" ).prepend( psFieldset.$element ); //add swap form
if( !$( '#psFieldset' ).length ){ //something went wrong
mw.notify(config.errorMsg, {type: 'error', title: "Error:" } );
document.getElementById("mw-movepage-table").style.display="block";
$( '#movepage' ).show();
$( "div.mw-message-box-error" ).show();
}
var ulStyle = document.createElement('style'); // Even spacing in lists
ulStyle.innerHTML = '.oo-ui-labelElement-label ul li ul {margin-top: 0.1em;}';
document.head.appendChild(ulStyle);
/**
* Helper functions that rely on above form elements
*/
function checkTitles() {
if (psOldTitle.namespace.value%2==1 || psNewTitle.namespace.value%2==1) {
if (psMovetalk.isDisabled() == false) {
psMovetalk.setDisabled(true);
params.defaultMoveTalk = psMovetalk.isSelected();
psMovetalk.setSelected(false);
}
} else if (psMovetalk.isDisabled()) {
psMovetalk.setDisabled(false);
psMovetalk.setSelected(params.defaultMoveTalk);
}
psConfirm.toggle(false).setType('notice');
params.currTitle = psParseTitle(psOldTitle);
params.destTitle = psParseTitle(psNewTitle);
var titlesMatch = (params.currTitle?.title==params.destTitle?.title);
psOldTitle.title.setValidityFlag(params.currTitle && !titlesMatch );
psNewTitle.title.setValidityFlag(params.destTitle && !titlesMatch );
psButton.setLabel(config.validateButton).off('click').on('click', clickValidate
).setDisabled(psOldTitle.title.value=='' || psNewTitle.title.value=='' || titlesMatch );
}
function clickValidate() {
psConfirm.toggle(false).setType('notice');
psStatus.toggle(false).setType('notice');
psButton.setDisabled(true).setLabel(config.validatingButton);
psProgress.toggle(true);
Object.assign(params, params, {
confirmMessages: [],
statusMessages: [],
currTitle: psParseTitle(psOldTitle),
destTitle: psParseTitle(psNewTitle),
moveReason: psReasonOther.value,
moveTalk: psMovetalk.isDisabled() ? false : psMovetalk.selected,
moveSubpages: psMoveSubpages.selected,
talkRedirect: psTalkRedirect.selected,
fixSelfRedirect: psFixSelfRedirect.selected,
watch: psWatch.selected ? 'watch' : 'unwatch',
} );
if (!params.currTitle) {
showConfirm("Title '" + psOldTitle + "' is invalid.", 'error');
return;
} else if (!params.destTitle) {
showConfirm("Title '" + psNewTitle + "' is invalid.", 'error');
return;
}
if (psReasonList.value != 'other') {
params.moveReason = psReasonList.value +
(psReasonOther.value == '' ? '' : '. ' + psReasonOther.value);
} else if (psReasonOther.value == '') {
params.moveReason = 'Swap [[' + params.currTitle.title + ']] and [[' +
params.destTitle.title + ']] ([[WP:SWAP]])';
}
var queryTitleArr = [params.currTitle.title, params.destTitle.title];
queryTitleArr.forEach(
(v) => queryTitleArr.push(mw.Title.newFromText( v ).getTalkPage( ).getPrefixedText())
);
params.titlesString = " [[" + queryTitleArr.join(']] or [[') + "]]";
var queryData = {action:'query', format:'json', titles: queryTitleArr.join('|'),
prop:'info', intestactions:'move|create',
list:'logevents', leprop:'timestamp', letype:'move', letitle: params.currTitle.title, lelimit:'1',
meta:'siteinfo', siprop:'namespaces'
};
new mw.Api().get( queryData ).then( (data) => {
if (data && data?.query?.namespaces) params.apiData = data.query;
return new mw.Api().get( {
action:'query', format:'json',
redirects:'', titles: queryTitleArr.join('|')
} );
} ).then( (rData) => {
if (rData && Object.keys(params.apiData).length > 0) {
params.apiData.redirects = rData?.query?.redirects;
gatherSubpageData();
} else {
showConfirm("Error parsing API data on" + params.titlesString + ".",
'error');
}
} ).fail ( (codetr, reslttr) => {
showConfirm("Error fetching API data on" + params.titlesString + ": " +
(reslttr.error.info || (codetr + ".")), 'error');
} );
}
return true;
}