User:Ahecht/Scripts/pageswap-core.js
Appearance
< User:Ahecht | Scripts
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
This user script seems to have a documentation page at User:Ahecht/Scripts/pageswap-core. |
//jshint -W083
function pageSwap(prefix, moveReason, debug) {
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',
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 => `Page ${s} from redirects table not found in ` +
'input data.',
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. ',
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',
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
)
};
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 : '');
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 = "*" + 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;
new mw.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"]) {
if (k == "-1" || params[k].ns < 0) {
ret.valid = false;
ret.invalidReason = config.error.notExist(params[k].title);
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 = config.error.namespace(params[k].title, params[k].ns);
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,
};
new mw.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
};
showStatus(config.status.TPRcreating(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( 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 (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(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++;
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.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;
showStatus(config.status.step(1, mQuery.from, mQuery.to));
new mw.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 new mw.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 new mw.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) {
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.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
};
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(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('|')
};
new mw.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:
*/
//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(config.error.cantSwap, { title: config.error.title,
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) => {
$( 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 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: 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(psWatch, {align: 'inline',
label: config.form.watch, title: config.form.watchTitle} ),
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);
$( config.selector.form ).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
$( config.selector.wrapper ).prepend( psFieldset.$element ); //add swap form
if( !$( '#psFieldset' ).length ){ //something went wrong
mw.notify(config.error.form, {type: 'error', title: "Error:" } );
$( config.selector.table )[0].style.display="block";
$( config.selector.form ).show();
$( 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 );
}
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(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'
};
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: 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;
}