//jshint -W083
function pageSwap(prefix, moveReason, debug, version) {
const sandbox = "";
const config = {
psTag: 'pageswap',
intermediatePrefix: "Draft:Move/",
portletLink: 'Swap' + sandbox + (debug ? ' (debug)' : ''),
portletAlt: 'Perform a revision history swap / round-robin move',
validateButton: 'Validate page swap' + sandbox + (debug ? " (debug)" : ""),
validatingButton: 'Validating page swap' + sandbox + (debug ? " (debug)" : ""),
introText: "<big>'''Please post bug reports/comments/suggestions for " +
'the Pageswap GUI script at [[User talk:Ahecht]]. Version 2.3 now ' +
'allows you to set redirect categories for new and modified ' +
"redirects.'''</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.',
joinOr: ']] or [[',
confirm: {
button: 'Confirm' + sandbox + (debug ? " (debug)" : ""),
created: (f, t) => `* Redirect from [[${f}]] → [[${t}]] will be created.`,
header: "'''Round-robin configuration:'''\n*",
footer: '\nPress "Confirm" to proceed.',
reason: s => 'Reason: ' + s,
redirMsg: 'The following redirect(s) will be created or modified '+
'({{clickable button|#rcat|choose redirect categories|' +
'style=padding:1px;min-height:0;line-height:normal !important;' +
'vertical-align:top;}}):',
retargeted: s => `* Self-redirect at [[${s}]] will be re-targeted.`,
subpageDisabled: "Moving subpages disabled.",
subpageList: (c, b, r) => `${c.length} total subpages of [[${b}]]` +
(r !== 0 ? ` (${r} redirects):` : `:`) +
`\n**[[${c.join(']]\n**[[')}]]`,
swapping: (c, d) => `Swapping [[${c}]] → [[${d}]]`,
},
selector: {
form: '#movepage',
loading: '#movepage-loading',
messageError: 'div.cdx-message--error',
output: 'div.mw-parser-output',
reasons: '#wpReasonList',
redirect: 'a.mw-redirect',
table: '#mw-movepage-table',
text: '#movepagetext',
watch: '#watch input[name="wpWatch"]',
wrapper: 'div.movepage-wrapper'
},
doneMsg: {
cleanup: 'Please do post-move cleanup as necessary',
redir: 'correct any moved redirects (including on talk pages and ' +
'subpages)',
redlink: 'create new red-linked talk pages/subpages if there are ' +
'incoming links (check your [[Special:MyContributions|' +
'contribs]] for "Talk:" and subpage redlinks)',
subpages: '*The following subpage(s) were moved, and may need new ' +
'or updated redirects:\n',
},
edit: {
create: s => `Create redirect to [[${s}]]`,
default: (c, d) => `Swap [[${c}]] and [[${d}]] ([[WP:SWAP]])`,
move: s => s,
retarget: s => `Retarget redirect to [[${s}]]`,
step: (s, n) => s + ' ([[WP:Page mover#rr|Round-robin swap]] step ' +
`${n})`,
},
error: {
apiParse: s => `Error parsing API data on${s}.`,
apiFetch: (s, i, c) => `Error fetching API data on${s}: ${i||(c+".")}`,
cantSwap: 'User rights insufficient. Swapping pages unavailable.',
checkPerm: 'Cannot check user permissions. Swapping pages unavailable.',
conflict: "'''Error:''' One or more pages involved in the swap has " +
"been edited since the swap was validated. Please check the " +
"pages and validate again.",
createProtect: s => s + ' is create-protected. ',
diffNs: (ct, cn, dt, dn) => `Strange. ${ct} is in ns ${cn} but ` +
`${dt} is in ns ${dn}. Disallowing.`,
form: 'Error adding swap form to page!',
immovable: s => `${s} is immovable. Aborting.`,
lastMoved: (r, s, t, u) => `${r}[[${s}]] was last moved ${t} ${u} ago.`,
moving: (f, t, i, c) => `* Failed when moving ([[${f}]] → [[${t}]]): ` +
(i||(c+'.')),
namespace: (t, n) => `Namespace of ${t} (${n}) is not supported. ` +
'Likely 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' +
'*If the move is legitimate, consider a careful manual swap.',
notDone: 'Titles are null, or move reason given was empty. ' +
'Swap not done',
notExist: s => `Page ${s} does not exist.`,
oneSubpage: "One page is a subpage. Disallowing move-subpages",
oneTalk: "Namespaces don't match: one is a talk page.",
pageData: s => `Unable to get page data for ${s}`,
parseWikitext: (s, i, c) => `Error parsing wikitext:\n\n${s}\n\n` +
(i||(c+".")),
r_info: '',
r_note: 'Note: ',
r_warn: 'Warning: ',
rcatAPI: s => `* API error '${s}' when verifying Rcat templates.`,
redirNotFound: s => 'Note: API data includes a redirect from [[' + s +
']], which is not one of the pages being swapped. This may be ' +
'the result of a [[Wikipedia:Double redirects|double redirect]]. ' +
'Please check redirect targets after the swap is complete',
retargetAPI: s => 'Could not check for self-redirects due to API ' +
`error '${s}' when fetching page contents. `,
retargetFailed: (i, c) => `* Retargeting failed. ${i||(c+".")}`,
retargetFetch: s => 'Could not fetch contents of redirect(s) at [[' +
s + ']].',
retargetParse: 'Error parsing redirects after retargeting:',
retargetString: (t, o) => 'Attempt to retarget redirect at [[' + t +
`]] to [[${o}]] failed: String not found.`,
subpageApiData: t => `API did not return data for subpages of ${t}. ` +
`Subpages may exist.`,
subpageApiErr: (x, s, e, t) => `API error '${x.status||s}' when ` +
`searching for subpages of ${t}. ` +
`${(e||x.responseText).replace('\n','')} Subpages may exist.`,
subpageCannotMove: (c, l, b) => `Disabling move-subpages. The ` +
`following ${c.length} (of ${l}) total subpage(s) of [[${b}]]`+
`CANNOT be moved:\n**[[${c.join(']]\n**[[')}]]`,
subpageLimit: t => `100+ subpages of ${t}. Aborting`,
subpageNsDisabled: 'One namespace does not have subpages enabled. ' +
'Disallowing move subpages.',
t_day: 'day(s)',
t_hour: 'hour(s)',
t_minute: 'minute(s)',
talkImmovable: 'Talk page is immovable. ',
talkMove: 'Disallowing moving talk. ',
templateSwap: '[[Wikipedia:Template editor|Template editor]]s permission is ' +
'required to swap templates or modules with this script.',
title: 'Page Swap Error',
titleInvalid: s => `Title '${s}' is invalid.`,
TPRerror: (i, c) => `* Failed to create redirect! ${i||(c+'.')}`,
validateSwap: 'Failed to validate swap.',
validateTalk: 'Unable to validate talk. ' +
'Disallowing movetalk to be safe.',
},
form: {
contribsButton: 'Open contribs page',
contribs: 'Special:MyContributions',
expiries: [
{data: 'infinite', label: 'Permanent'},
{data: '1 week', label: '1 week'},
{data: '1 month', label: '1 month'},
{data: '3 months', label: '3 months'},
{data: '6 months', label: '6 months'},
{data: '1 year', label: '1 year'}
],
fixSR: 'Fix self-redirects',
fixSRTitle: '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.',
moveSub: 'Move subpages',
moveSubTitle: 'Move up to 100 subpages of the source and/or target pages',
moveTalk: 'Move associated talk page',
new: 'New title:',
old: 'Old title:',
otherReason: 'Other/additional reason:',
reason: 'Reason:',
talkRedir: 'Leave a redirect to new talk page if needed',
talkRedirTitle: '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.',
watch: 'Watch source page and target page',
watchTitle: 'Add both source page and target page to your watchlist',
},
rcat: {
added: '* The following redirect categories will be added where possible: ',
cat: 'Category:Redirect templates',
choose: 'Choose redirect categories for the newly created redirects:',
defaultCat: '{{R from move}}',
dialogTitle: 'Choose Redirect Categories',
regEx: new RegExp("^R (from |to |with )?"),
shell: s => "{{Redirect category shell|\n"+s+"\n}}",
tempNSRegEx: new RegExp("\\|\\s*(\\S*?) category\\s*=", "g"),
},
status: {
doing: 'Doing round-robin history swap...',
TPRcreating: (f, t) => `Creating talk page redirect [[${f}]] → [[${t}]]...`,
TPRcreated: '* Talk page redirect created!',
header: "'''Performing page swap:'''\n",
retargeted: '* Redirect retargeted!',
retargeting: (t, o) => `Retargeting redirect at [[${t}]] to [[${o}]]...`,
step: (n, f, t) => `* Step ${n} ([[${f}]] → [[${t}]])...`,
swapComplete: (c, d) => `* Round-robin history swap of [[${c}]]` +
` ([[Special:WhatLinksHere/${c}|links]]) and [[${d}]]` +
` ([[Special:WhatLinksHere/${d}|links]]) completed ` +
`successfully!`,
},
linkSub: s => s.replace("[[WP:RM/TR]]",
"[[WP:Requested moves/Technical requests|WP:RM/TR]]"),
types: ['notice', 'success', 'warning', 'error'],
}, params = {
apiData: {}, currTitle: {}, destTitle: {},
confirmMessages: [], statusMessages: [],
queryTitles: [], selfRedirs: [], rcats: [],
selectedRcats: { [config.rcat.defaultCat]: ["all"] },
defaultMoveTalk: true, confDone: false, editRedir: false, done: false,
lastrevid: 0, busy: 0, idempotency: {psConfirm: 0, psStatus: 0},
cleanup: (
typeof pagemoveDoPostMoveCleanup === 'undefined' ?
true :
pagemoveDoPostMoveCleanup
),
defaultWatch: $(
config.selector.table + ' ' + config.selector.watch
).prop('checked') ?? false
};
var api = new mw.Api( { userAgent: config.pstag + '/' + version } );
function filterHtml(rawHtml) {
$value=$($.parseHTML(rawHtml));
$value.filter( config.selector.output ).contents().each(function() {
if(this.nodeType === Node.COMMENT_NODE || this.nodeType === Node.TEXT_NODE) {
$(this).remove();
}
}).find( config.selector.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 = config.error.parseWikitext( label, 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 && message !== '') {
params.confirmMessages.push(config.linkSub(message));
}
var label = config.confirm.header +
params.confirmMessages.join("\n*") +
(params.confDone ? config.confirm.footer : '');
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 = "*" + config.linkSub(message) + "\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.doneMsg.subpages + "**[[" +
params.allSpArr.join("]]\n**[[") + "]]\n";
var doneMessages = [config.doneMsg.cleanup];
if (!params.talkRedirect || params.moveSubpages) doneMessages.push(config.doneMsg.redlink);
if (!params.fixSelfRedirect || params.moveSubpages) doneMessages.push(config.doneMsg.redir);
if (doneMessages.length < 3) {
doneMessage = doneMessages.join(" and ") + ".";
} else {
doneMessage = doneMessages.slice(0, -1).join(', ') + ', and ' +
doneMessages.slice(-1) + ".";
}
type = 'success';
}
var label = config.status.header + params.statusMessages.join('') +
doneSubpagesMessage + doneMessage;
api.parse(label).done(
(parsedText) => setLabel(psStatus, filterHtml(parsedText), type, idempotency)
).fail(
(codetr, reslttr) => parseError(psStatus, label, codetr, reslttr, idempotency)
).always( () => {
if (params.done && params.busy == 0) psContribsButton.toggle(true);
} );
}
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 (qp.lastrevid > params.lastrevid ) {
params.lastrevid = qp.lastrevid;
}
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 += config.error.pageData(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("'''"+config.error.lastMoved(
config.error.r_warn, params.currTitle.title,
Math.round(lastMove), config.error.t_minute
)+"'''", 'warning');
} else if ( lastMove < 1440 ) { // 1 day
showConfirm("'''"+config.error.lastMoved(
config.error.r_note, params.currTitle.title,
Math.round(lastMove/60), config.error.t_hour
)+"'''", 'notice');
} else if ( lastMove < 43200 ) { // 30 days
showConfirm(config.error.lastMoved(
config.error.r_info, params.currTitle.title,
Math.round(lastMove/1440), config.error.t_day
), 'notice');
}
}
} else {
ret = {valid: false, invalidReason: config.error.pageData(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 += config.error.validateSwap;
return ret;
}
ret.allowMoveSubpages = true;
ret.checkTalk = true;
for (const k of ["currTitle", "destTitle"]) {
var kns = Number(params[k].ns);
if (k == "-1" || Number.isNaN(kns) || kns < 0) {
ret.valid = false;
ret.invalidReason = config.error.notExist(params[k].title);
return ret;
}
// enable only in ns 0..5,12,13,118,119,828,829 (Main,Talk,U,UT,WP,WT,H,HT,D,DT,M,MT)
if ( (kns >= 6 && kns <= 9) ||
(kns >= 14 && kns <= 117) ||
(kns >= 120 && kns <= 827) ||
(kns >= 830) ) {
ret.valid = false;
ret.invalidReason = config.error.namespace(params[k].title, params[k].ns);
return ret;
} else if (!params.uPerms.allowSwapTemplates && [10, 11, 828, 829].includes(kns)){
ret.valid = false;
ret.invalidReason = config.error.templateSwap;
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 = ( config.error.immovable(ret.currTitle) );
return ret;
}
if (!ret.destCanMove) {
ret.valid = false;
ret.invalidReason = ( config.error.immovable(ret.destTitle) );
return ret;
}
if (ret.currNs % 2 !== ret.destNs % 2) {
ret.valid = false;
ret.invalidReason = config.error.oneTalk;
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 = config.error.diffNs(ret.currTitle,
ret.currNs, ret.destTitle, ret.destNs);
return ret;
}
ret.allowMoveSubpages = ret.currNsAllowSubpages;
if (!ret.allowMoveSubpages)
ret.addlInfo = config.error.oneSubpage;
}
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(config.error.validateTalk, '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,
};
api.get(queryData).done( (subpages) => {
if ( typeof subpages !== 'object' ) {
deferred.reject( config.error.subpageApiData(title) );
} 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( config.error.subpageApiData(title) );
}
} else if (Object.keys(subpages.query.pages).length > 101) {
deferred.reject( config.error.subpageLimit(title) );
} 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 = config.error.subpageApiErr(jqXHR, textStatus,
errorThrown, title);
console.warn(errStr);console.log(queryData);console.log(jqXHR);
deferred.reject(errStr);
} );
}
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( config.error.subpageCannotMove(currSpCannotMove,
currSpArr.length, basepage), 'warning' );
} else if (typeof basepage !== 'undefined') {
showConfirm( config.confirm.subpageList(currSpArr, basepage,
redirCount) );
}
}
}
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.rcat.shell( filterRcats('talk').join('\n') ),
summary: config.edit.create(toTalk), tags: config.psTag,
watchlist: params.watch
};
if (params.watch == 'watch') talkRedirect.watchlistexpiry = psWatchExpiry.value;
showStatus(config.status.TPRcreating(fromTalk, toTalk),
'notice', false, "TPR" + fromTalk);
if (debug) {
params.busy--;
showStatus("* Talk page redirect simulated!.",
'notice', true, "TPR" + fromTalk);
} else {
api.postWithEditToken(talkRedirect).done( () => {
params.busy--;
showStatus( config.status.TPRcreated, 'notice', true,
"TPR" + fromTalk);
} ).fail( (codetr, reslttr) => {
params.busy--;
showStatus(config.error.TPRerror(reslttr.error.info,
codetr), 'error', true, "TPR" + fromTalk);
} );
}
}, 250);
} else { showStatus('', 'notice', true); }
}
function retargetRedirect(thisPage, otherPage, newText) {
params.busy++;
showStatus(config.status.retargeting(thisPage,otherPage), 'notice',
false, "RT"+thisPage);
var retargetData = {
action:'edit',
title: thisPage,
text: newText,
summary: config.edit.retarget(otherPage), tags: config.psTag,
watchlist: params.watch,
};
if (params.watch == 'watch') retargetData.watchlistexpiry = psWatchExpiry.value;
if (debug) {
params.busy--;
showStatus("* Retargeting simulated!",'notice', false, "RT"+thisPage);
} else {
api.postWithEditToken(retargetData).done( (result, jqXHR) => {
params.busy--;
if (typeof result.edit !== 'undefined') {
params.busy++;
api.get( {
action: 'query', prop: '', redirects: '',
titles: result.edit.title
} ).done( (data) => {
params.busy--;
if (data && typeof data?.query?.redirects !== 'undefined') {
showStatus(config.status.retargeted, 'notice',
false, "RT"+thisPage);
} else {
console.warn(config.error.retargetParse);
console.warn(data);
}
} ).fail( (codeart, rsltart) => {
params.busy--;
console.warn(config.error.retargetFetch(result.edit.title));
console.warn(codeart);console.warn(rsltart);
} );
} else {
console.warn(config.error.retargetParse);
console.warn(result);console.warn(jqXHR);
}
} ).fail( (codert, resultrt) => {
params.busy--;
showStatus(config.error.retargetFailed(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;
params.selfRedirs = [];
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(config.error.redirNotFound(redirs[e].from), '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++;
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.rcat.shell(
filterRcats(thisNs).join('\n')
);
} else { // Just change target
newText = oldText.replace(redirRE,
'#REDIRECT [['+otherPage+']]');
}
retargetRedirect(thisPage, otherPage, newText);
} else {
showStatus(config.error.retargetString(thisPage, otherPage),
'warning');
}
} );
} else {
params.busy--;
showStatus( config.error.retargetFetch(srQuery.titles),
'error');
}
} ).fail( (jqXHR, textStatus) => {
params.busy--;
showStatus(config.error.retargetAPI(jqXHR.status||textStatus), '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(config.error.notDone, '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(config.status.step(1,destTitle,intermediateTitle));
new Promise( (r) => setTimeout(r, 1000) ).then( () => {
showStatus(config.status.step(2,currTitle,destTitle));
return new Promise( (r) => setTimeout(r, 1000) );
} ).then( () => {
showStatus(config.status.step(3,intermediateTitle,currTitle));
return new Promise( (r) => setTimeout(r, 1000) );
} ).then( () => {
if (params.fixSelfRedirect || params.talkRedirect) {
showStatus(config.status.swapComplete(currTitle, destTitle));
params.busy--;
if (params.fixSelfRedirect && params.selfRedirs.length > 0) {
checkSelfRedirs(vData, vTData);
} else {
createMissingTalk(vData, vTData);
}
} else {
params.busy--;
showStatus(config.status.swapComplete(currTitle, destTitle), 'notice', true);
}
} );
} else {
showStatus(config.status.doing);
var mQuery = {
action:'move', from:destTitle, to:intermediateTitle,
reason: config.edit.step(params.moveReason, '1'),
tags:config.psTag, watchlist:params.watch, noredirect:1
};
if (params.moveTalk) mQuery.movetalk = 1;
if (params.moveSubpages) mQuery.movesubpages = 1;
if (params.watch == 'watch') mQuery.watchlistexpiry = psWatchExpiry.value;
showStatus(config.status.step(1, mQuery.from, mQuery.to));
api.postWithEditToken(mQuery).then( () => {
Object.assign(mQuery, { from:currTitle, to:destTitle,
reason: config.edit.move(params.moveReason) } );
showStatus(config.status.step(2, mQuery.from, mQuery.to));
return api.postWithEditToken(mQuery);
} ).then( () => {
Object.assign(mQuery, { from:intermediateTitle, to:currTitle,
reason: config.edit.step(params.moveReason, '3') } );
showStatus(config.status.step(3, mQuery.from, mQuery.to));
return api.postWithEditToken(mQuery);
} ).then( () => {
if (params.fixSelfRedirect || params.talkRedirect) {
showStatus(config.status.swapComplete(currTitle, destTitle));
params.busy--;
if (params.fixSelfRedirect && params.selfRedirs.length > 0) {
checkSelfRedirs(vData, vTData);
} else {
createMissingTalk(vData, vTData);
}
} else {
params.busy--;
showStatus(config.status.swapComplete(currTitle, destTitle), 'notice', true);
}
} ).fail( (code, reslt) => {
params.busy--;
showStatus(config.error.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.rcat.defaultCat.replace(/\{\{(.*)\}\}/, "Template:$1")
).text( config.rcat.defaultCat )
);
var content = $( '<span>' ).append( '<p>' + config.rcat.choose + '</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 = config.rcat.dialogTitle;
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) {
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.rcat.tempNSRegEx),
(v) => (v[1])
);
if (nsMatches.length == 0) nsMatches = ['unknown'];
params.selectedRcats[tempCall] = nsMatches;
}
} );
}
if (Object.keys(params.selectedRcats).length > 0) {
showConfirm(config.rcat.added + "<code><nowiki>" +
config.rcat.shell(
Object.keys(params.selectedRcats).join('\n')
) + "</nowiki></code>"
);
}
} ).fail( (jqXHR, textStatus) => {
showConfirm(config.error.rcatAPI(jqXHR.status||textStatus),
'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.rcat.cat, cmsort:'sortkey', cmnamespace:10,
cmtype:'page', cmprop: 'title|sortkeyprefix', cmcontinue:cmcont,
continue:cont
};
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(config.rcat.regEx, '') :
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(config.error.subpageNsDisabled, 'warning');
}
} else {
showConfirm(config.confirm.subpageDisabled);
}
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(config.error.talkMove +
(!vTData.currTCanCreate ? config.error.createProtect(vData.currTalkName)
: (!vTData.destTCanCreate ? config.error.createProtect(vData.destTalkName)
: config.error.talkImmovable)), 'warning');
}
}
showConfirm(config.confirm.swapping(params.currTitle.title, params.destTitle.title));
showConfirm(config.confirm.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.confirm.redirMsg);
if (vTData.redirFromTalk && vTData.redirToTalk) {
showConfirm( config.confirm.created(vTData.redirFromTalk,
vTData.redirToTalk) );
}
for (const t in params.selfRedirs) {
showConfirm( config.confirm.retargeted(params.selfRedirs[t]) );
}
}
psProgress.toggle(false);
showConfirm('', 'notice', true);
psButton.setDisabled(false).setLabel(config.confirm.button).off('click').on('click', () => {
psButton.setDisabled(true).setLabel(config.validateButton);
var confirmQuery = {
action: 'query', prop: 'info', titles: params.queryTitles.join('|')
};
api.get( confirmQuery ).then( (data) => {
if (typeof data === 'object' && typeof data?.query?.pages === 'object') {
var conflict = false;
for (var kp in data.query.pages) {
if (data.query.pages[kp].lastrevid > params.lastrevid ) {
conflict = true;
}
}
if (conflict) {
showStatus(config.error.conflict, type='error');
checkTitles();
} else {
swapPages(vData, vTData);
}
} else {
showStatus(config.error.apiParse(params.titlesString),
'error');
}
} ).fail ( (codetr, reslttr) => {
showStatus(config.error.apiFetch(params.titlesString,
reslttr.error.info, codetr), 'error');
} );
} );
}
/**
* 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.toString(), '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(config.error.checkPerm, { 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:
*/
params.uPerms = checkUserPermissions();
if (!params.uPerms.canSwap) {
mw.loader.using( [ 'mediawiki.notification' ], () => {
mw.notify(config.error.cantSwap, { title: config.error.title,
type: 'error' } );
} );
return;
}
//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;
}
}
$( '#firstHeading' ).text( (i, t) => (t.replace('Move', 'Swap')) );
document.title = document.title.replace("Move", "Swap");
api.parse(config.introText).done( (parsedText) => {
$( config.selector.text ).replaceWith( $($.parseHTML(parsedText)) );
} ).fail( (codetr, reslttr) => {
console.warn( config.error.parseWikitext(config.introText,
reslttr.error.info, codetr) );
$( config.selector.text ).html( config.introText );
} );
var reasonList = [];
if ($( config.selector.reasons )[0]) {
reasonList.push( {
data: $( config.selector.reasons ).children("option").get(0).value,
label: $( config.selector.reasons ).children("option").get(0).text
} );
reasonList.push( {optgroup: $( config.selector.reasons ).children("optgroup").get(0).label} );
$( config.selector.reasons ).children("optgroup").children("option").get().forEach(
option => reasonList.push( {data: option.value, label: option.text} )
);
}
var expiryList = config.form.expiries;
if (typeof pagemoveWatchlistDefaultExpiry !== 'undefined') {
const index = expiryList.findIndex(
item => item.data === pagemoveWatchlistDefaultExpiry
);
if (index !== -1) { // Remove the item if it exists
expiryList.splice(index, 1);
}
expiryList.unshift( { // Add it to the top
data: pagemoveWatchlistDefaultExpiry,
label: pagemoveWatchlistDefaultExpiry
} );
}
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', name: 'wpReason'} ),
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: params.defaultWatch, id: 'psWatch'} ),
psWatchExpiry = new OO.ui.DropdownInputWidget( {
options: config.form.expiries, id: 'psWatchExpiry', $overlay: true,
disabled: !psWatch.isSelected(),
} ),
psWatchWithExpiry = new OO.ui.Widget( {
content: [ new OO.ui.HorizontalLayout( {
items: [
psWatch,
new OO.ui.LabelWidget( {
label: config.form.watch,
title: config.form.watchTitle,
input: psWatch
} ),
psWatchExpiry
]
} ) ]
} ),
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: config.form.contribsButton, title: config.form.contribs,
href: mw.config.get("wgServer") +
mw.config.get("wgArticlePath").replace("$1", config.form.contribs),
framed: true, flags: ['primary', 'progressive'],
id: 'psContribsButton', target: '_blank'
} );
psFieldset.addItems( [
new OO.ui.FieldLayout(psOldTitle, {align: 'top',
label: config.form.old, id: 'psOldTitle'} ),
new OO.ui.FieldLayout(psNewTitle, {align: 'top',
label: config.form.new, id: 'psNewTitle'} ),
new OO.ui.FieldLayout(psReasonList, {align: 'top',
label: config.form.reason} ),
new OO.ui.FieldLayout(psReasonOther, {align: 'top',
label: config.form.otherReason} ),
new OO.ui.FieldLayout(psMovetalk, {align: 'inline',
label: config.form.moveTalk, title: config.form.moveTalk} ),
new OO.ui.FieldLayout(psMoveSubpages, {align: 'inline',
label: config.form.moveSub, title: config.form.moveSubTitle} ),
new OO.ui.FieldLayout(psTalkRedirect, {align: 'inline',
label: config.form.talkRedir, title: config.form.talkRedirTitle} ),
new OO.ui.FieldLayout(psFixSelfRedirect, {align: 'inline',
label: config.form.fixSR, title: config.form.fixSRTitle} ),
new OO.ui.FieldLayout(psWatchWithExpiry, {} ),
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(); psWatchExpiry.setDisabled(!psWatch.isSelected());
} );
/**
* 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);
$( config.selector.table ).hide(); //hide old form
$( config.selector.loading ).remove(); //remove loading message
$( config.selector.messageError ).hide(); //hide error message
$( '#psFieldset' ).remove(); //remove old form if script started twice
// Format watchlist checkbox line
psWatchExpiry.$element.css('width', 'auto');
psWatchExpiry.$element.closest('.oo-ui-fieldLayout').css('margin-top', '7px');
$( config.selector.form ).attr( 'autocomplete', 'on' ).prepend( psFieldset.$element ); //add swap form
if( !$( '#psFieldset' ).length ){ //something went wrong
mw.notify(config.error.form, {type: 'error', title: "Error:" } );
$( config.selector.table ).show()[0].style.display="block";
$( config.selector.messageError ).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 );
params.defaultWatch = psWatch.isSelected();
}
function clickValidate() {
psConfirm.toggle(false).setType('notice');
psStatus.toggle(false).setType('notice');
psButton.setDisabled(true).setLabel(config.validatingButton);
psProgress.toggle(true);
Object.assign(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' : 'nochange',
} );
if (!params.currTitle) {
showConfirm(config.error.titleInvalid(psOldTitle), 'error');
return;
} else if (!params.destTitle) {
showConfirm(config.error.titleInvalid(psNewTitle), 'error');
return;
}
if (psReasonList.value != 'other') {
params.moveReason = psReasonList.value +
(psReasonOther.value == '' ? '' : ': ' + psReasonOther.value);
} else if (psReasonOther.value == '') {
params.moveReason = config.edit.default(params.currTitle.title,
params.destTitle.title);
}
params.queryTitles = [params.currTitle.title, params.destTitle.title];
params.queryTitles.forEach(
(v) => params.queryTitles.push(mw.Title.newFromText( v ).getTalkPage( ).getPrefixedText())
);
params.titlesString = " [[" + params.queryTitles.join(config.joinOr) + "]]";
var queryData = {action:'query', format:'json', titles: params.queryTitles.join('|'),
prop:'info', intestactions:'move|create',
list:'logevents', leprop:'timestamp', letype:'move', letitle: params.currTitle.title, lelimit:'1',
meta:'siteinfo', siprop:'namespaces'
};
api.get( queryData ).then( (data) => {
if (data && data?.query?.namespaces) params.apiData = data.query;
return api.get( {
action:'query', format:'json',
redirects:'', titles: params.queryTitles.join('|')
} );
} ).then( (rData) => {
if (rData && Object.keys(params.apiData).length > 0) {
params.apiData.redirects = rData?.query?.redirects;
gatherSubpageData();
} else {
showConfirm(config.error.apiParse(params.titlesString), 'error');
}
} ).fail ( (codetr, reslttr) => {
showConfirm(config.error.apiFetch(params.titlesString,
reslttr.error.info, codetr), 'error');
} );
}
return true;
}