User:Ahecht/Scripts/massmove-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. |
Documentation for this user script can be added at User:Ahecht/Scripts/massmove-core. |
//jshint maxerr:512
//Forked version of [[User:Plastikspork/massmove.js]] that adds a link to the left column and allows adding and removing both prefixes and suffixes
//Loaded from [[User:Ahecht/Scripts/massmove.js]]
var massMoveTitle = "Mass-move tool";
// Adapted from [[User:Animum/massdelete.js]]
function massMoveGetValues() {
return {reason: document.getElementById("wpMassMoveReason").value,
oldPrefix: document.getElementById("wpMassMovePrefix1").value,
newPrefix: document.getElementById("wpMassMovePrefix2").value,
oldSuffix: document.getElementById("wpMassMoveSuffix1").value,
newSuffix: document.getElementById("wpMassMoveSuffix2").value,
watch: document.getElementById("wpMassMoveWatch").value,
find: document.getElementById("wpMassMoveFind").value,
replace: document.getElementById("wpMassMoveReplace").value,
flags: document.getElementById("wpMassMoveFlags").value,
pipeTrick: document.getElementById("wpMassMovePipeTrick").checked,
leaveRedirect: document.getElementById("wpMassMoveLeaveRedirect").checked,
noRatelimit: document.getElementById("wpMassMoveNoRatelimit").checked,
moveTalk: document.getElementById("wpMassMoveMoveTalk").checked,
moveSubPages: document.getElementById("wpMassMoveMoveSubPages").checked,
regexp: document.getElementById("wpMassMoveRegExp").checked,
debug: new URLSearchParams(window.location.search).get('debug') !== null
};
}
function massMoveReplace(s, values) {
s = s.trim();
if (values.pipeTrick) {
s = s.replace(/^(?:\:)?(?:.*\:)?(.*?)(?:, .*)?$/,"$1").replace(/(.*?)(?: ?\(.*\))?$/, "$1");
}
if (s.substring(0,values.oldPrefix.length) == values.oldPrefix) {
s = s.substring(values.oldPrefix.length);
}
if (s.substring(s.length - values.oldSuffix.length) == values.oldSuffix) {
s = s.substring(0, s.length - values.oldSuffix.length);
}
s = values.newPrefix + s + values.newSuffix;
if (values.find.length > 0) {
if (values.regexp) values.find = new RegExp(values.find, values.flags);
s = s.replace(values.find, values.replace);
}
return s.trim();
}
function massMoveGetArticles() {
var articles = document.getElementById("wpMassMovePages").value.split("\n");
var ret = [];
var i, len;
for (i = 0, len = articles.length; i < len; i++) {
var s = articles[i];
s = s.trim();
if (s) {
ret.push(s);
}
}
return ret;
}
function massMoveUpdatePreview() {
var articles = massMoveGetArticles();
var values = massMoveGetValues();
$( ".regexp" ).toggle(values.regexp);
$( "#wpMassMoveReplace" ).attr('disabled', values.find.length == 0);
if (articles.length > 0) {
var arrow = values.debug ? " ↛ " : " → ";
var preview = [articles[0] + arrow + massMoveReplace(articles[0], values)];
for (var i = 1, len = articles.length; i < len; i++) {
preview.push(articles[i] + arrow + massMoveReplace(articles[i], values));
}
document.getElementById("wpMassMovePreview").value = preview.join("\n");
document.getElementById("wpMassMoveSubmit").disabled = false;
} else {
document.getElementById("wpMassMovePreview").value = '';
document.getElementById("wpMassMoveSubmit").disabled = true;
}
}
function massMove() {
console.log(massMoveTitle + " loaded.");
var config = mw.config.get(['wgNamespaceNumber', 'wgTitle', 'wgUserGroups', 'skin']);
function now() {
return new Date().getTime();
}
function doMassMove(values) {
var articles = massMoveGetArticles();
if (!articles.length) {
return;
}
var
api = new mw.Api(),
moved = 0,
failed = [],
error = [],
deferreds = [],
lastMoved = 0;
function delay(len) {
return function() {
return $.Deferred(function (deferred) {
var interval = lastMoved + config.wait - now();
if ( (len <= config.hits) || ((lastMoved + config.wait - now()) < 0) || values.noRatelimit ) {
interval = 0;
}
console.log(now() + ': Waiting ' + interval + 'ms...');
setTimeout(function () {
console.log(now() + ': Done waiting.');
deferred.resolve();
}, interval);
});
};
}
function makeMoveFunc(article) {
return function () {
return $.Deferred(function (deferred) {
var options = {
format: 'json',
action: 'move',
watchlist: values.watch,
from: article,
to: massMoveReplace(article, values),
reason: values.reason,
tags: 'massmove'
};
if (!values.leaveRedirect) {
options.noredirect = '';
}
if (values.moveTalk) {
options.movetalk = '';
}
if (values.moveSubPages) {
options.movesubpages = '';
}
var movingText = values.debug ? "Simulating " : "Moving ";
console.log(now() + ": "+ movingText + options.from + "→" + options.to);
mw.notify(movingText + options.from + " → " + options.to, {type: 'info', tag: 'status'});
lastMoved = now();
if (values.debug) {
moved++;
console.log(now() + ": Simulated " + moved);
mw.notify("Success! " + moved + " pages simulated.", {type: 'success', tag: 'status'});
deferred.resolve();
} else {
api.postWithEditToken(options).done(function () {
moved++;
console.log(now() + ": Moved " + moved);
mw.notify("Success! " + moved + " pages moved.", {type: 'success', tag: 'status'});
}).fail(function (code, obj) {
failed.push(article);
error.push(obj.error.info);
console.warn(now() + ": Move failed (" + obj.error.info + ").");
mw.notify("Move failed: " + obj.error.info, {type: 'error'});
}).always(function () {
deferred.resolve();
});
}
});
};
}
// Make a chain of deferred objects. We chain them rather than execute them in
// parallel so that we don't make 1000 simultaneous move requests and bring the
// site down. We use deferred objects rather than the promise objects returned
// from the API request so that the chain continues even if some articles gave
// errors.
var deferred = makeMoveFunc(articles[0])();
for (var i = 1, len = articles.length; i < len; i++) {
deferred = deferred.then(delay(len));
deferred = deferred.then(makeMoveFunc(articles[i]));
}
// Show the output and do cleanup once all the requests are done.
$.when(deferred).then(function () {
console.log(now() + ": Done! Moved " + moved);
if (failed.length) {
mw.notify("Done. " + moved + " pages moved, " + failed.length + " errors.", {type: 'warn', tag: 'doneNotify', autoHide: false});
var $failedList = $('<ul>');
for(var x = 0; x < failed.length; x++) {
// Link the titles in the "failed" array
var failedTitle = mw.Title.newFromText(failed[x]);
var $failedItem = $('<li>');
if (failedTitle) {
$failedItem.append( $('<a>')
.attr('href', failedTitle.getUrl())
.text(failed[x])
);
} else {
$failedItem.text(failed[x]);
}
$failedItem.append(document.createTextNode(': ' + error[x]));
$failedList.append($failedItem);
}
$('#wpMassMoveFailedContainer')
.append($('<br />'))
.append($('<b>')
.text('Failed moves:')
)
.append($failedList);
} else {
mw.notify("Done. " + moved + " pages moved.", {type: 'success', tag: 'doneNotify', autoHide: false});
}
document.getElementById("wpMassMoveSubmit").value = "Move";
$("*", "#wpMassMove").not("#wpMassMovePreview").prop('disabled',false);
});
}
function getWait() {
var values = massMoveGetValues();
config.hits=8; // default rate limit is 8/minute
config.seconds=60; // default rate limit is 8/minute
config.wait = Math.ceil(config.seconds/config.hits) * 1000;
new mw.Api().get({
meta: 'userinfo',
uiprop: 'ratelimits|rights'
}).fail(function(code, error) {
console.warn(error);
mw.notify("Unable to determine rate limit, using default: " + config.hits + " moves every " + config.seconds + " seconds.", {type: 'warn', tag: 'rlNotify'});
doMassMove(values);
}).done( function(d) {
var ratelimits = d.query.userinfo?.ratelimits;
if (values.noRatelimit == true || (d.query.userinfo.rights
&& d.query.userinfo.rights.includes("noratelimit")) ) {
document.getElementById("wpMassMoveNoRatelimit").checked = true;
values.noRatelimit == true;
console.log("User is not ratelimited or noRatelimit checked");
mw.notify("Rate limit: none", {type: 'info', tag: 'rlNotify'});
doMassMove(values);
} else {
if (ratelimits && ratelimits.move) {
for (const property in d.query.userinfo.ratelimits.move) {
var rlm = d.query.userinfo.ratelimits.move[property];
if (rlm && rlm.hits && rlm.seconds) {
console.log(property + " rate limit: " + rlm.hits + " moves every " + rlm.seconds + " seconds.");
var thisWait = Math.ceil((rlm.seconds/rlm.hits) * 1000);
if (thisWait < config.wait) {
config.hits = rlm.hits;
config.seconds = rlm.seconds;
config.wait = thisWait;
console.log("Calculated " + config.wait + "-millisecond wait between queries");
}
}
}
} else {
console.warn('Unable to find rate limit.');
}
mw.notify("Rate limit: " + config.hits + " moves every " + config.seconds + " seconds.", {type: 'info', tag: 'rlNotify'});
doMassMove(values);
}
});
}
function massMoveForm() {
wpMassMoveStyle = document.createElement('style');
wpMassMoveStyle.type = 'text/css';
document.getElementsByTagName('head')[0].appendChild(wpMassMoveStyle);
wpMassMoveStyle = document.styleSheets[document.styleSheets.length-1];
wpMassMoveStyle.insertRule('td.maxcol {width:50%; white-space:nowrap;}', 0);
wpMassMoveStyle.insertRule('td.mincol {min-width:0; width:auto; white-space:nowrap;}', 0);
wpMassMoveStyle.insertRule('input[type="text" i] {width:100%; box-sizing:border-box; padding:2px 0.5em;}', 0);
wpMassMoveStyle.insertRule('.regexp-container {position:relative;}', 0);
wpMassMoveStyle.insertRule('.regexp {display:none;}', 0);
wpMassMoveStyle.insertRule('.slash.left {left:0.3em;}', 0);
wpMassMoveStyle.insertRule('.slash.right {right:0.3em;}', 0);
wpMassMoveStyle.insertRule('.slash {position:absolute; color:gray; pointer-events:none; user-select:none; z-index:1;}', 0);
wpMassMoveStyle.insertRule('input[type="checkbox" i] {vertical-align: middle;}', 0);
document.getElementById(config.bodyContent).innerHTML = config.wpMassMoveIntro +
'<form id="wpMassMove" name="wpMassMove">' +
'<b>If you abuse this tool, it\'s <i>your</i> fault, not mine.</b>' +
'<div id="wpMassMoveFailedContainer"></div>' +
'<br /><br />' +
'Pages to move (one on each line, please):<br />' +
'<textarea tabindex="1" accesskey="," name="wpMassMovePages" id="wpMassMovePages" rows="10" cols="80" oninput="massMoveUpdatePreview()" style="padding: 2px 0.5em"></textarea>' +
'<br /><br />' +
'<table style="background-color:transparent"><thead>' +
'<th colspan="2" scope="col">Old name</th>' +
'<th colspan="1" scope="col"></th>' +
'<th colspan="2" scope="col">New name</th>' +
'</thead><tr>' +
'<td>Apply <a target="_blank" href="' + mw.config.get('wgArticlePath').replace('$1','Help:Pipe_trick') + '">"Pipe Trick"</a> to old name:</td>' +
'<td colspan="4"><input type="checkbox" id="wpMassMovePipeTrick" name="wpMassMovePipeTrick"/ oninput="massMoveUpdatePreview()"></td>' +
'</tr><tr>' +
'<td class="mincol">Prefix to remove (e.g., <code>Template:</code> or <code>List of </code>):</td>' +
'<td class="maxcol"><input type="text" id="wpMassMovePrefix1" name="wpMassMovePrefix1" maxlength="255" oninput="massMoveUpdatePreview()"/></td>' +
'<td class="mincol" style="min-width:2em;"> </td>' +
'<td class="mincol">Prefix to add (e.g., <code>User:Plastikspork/</code>):</td>' +
'<td class="maxcol"><input type="text" id="wpMassMovePrefix2" name="wpMassMovePrefix2" maxlength="255" oninput="massMoveUpdatePreview()"/></td>' +
'</tr><tr>' +
'<td class="mincol">Suffix to remove (e.g., <code>/sandbox</code>):</td>' +
'<td class="maxcol"><input type="text" id="wpMassMoveSuffix1" name="wpMassMoveSuffix1" maxlength="255" oninput="massMoveUpdatePreview()"/></td>' +
'<td class="mincol" style="min-width:2em;"> </td>' +
'<td class="mincol">Suffix to add (e.g., <code> (disambiguation)</code>):</td>' +
'<td class="maxcol"><input type="text" id="wpMassMoveSuffix2" name="wpMassMoveSuffix2" maxlength="255" oninput="massMoveUpdatePreview()"/></td>' +
'</tr><tr>' +
'<td class="mincol">Find (<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#writing_a_regular_expression_pattern">regular expression</a>: <input type="checkbox" id="wpMassMoveRegExp" name="wpMassMoveRegExp" oninput="massMoveUpdatePreview()"/><span class="regexp"> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#advanced_searching_with_flags">flags</a>: <input type="text" style="width:3em; padding:0 0.5em" id="wpMassMoveFlags" name="wpMassMoveFlags" maxlength="8" oninput="massMoveUpdatePreview()"/></span>):</td>' +
'<td class="maxcol regexp-container"><span class="regexp slash left">/</span><input type="text" id="wpMassMoveFind" name="wpMassMoveFind" maxlength="255" oninput="massMoveUpdatePreview()"/><span class="regexp slash right">/</span></td>' +
'<td class="mincol" style="min-width:2em;"> </td>' +
'<td class="mincol">Replace (<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement">special replacement patterns</a>):</td>' +
'<td class="maxcol"><input type="text" id="wpMassMoveReplace" name="wpMassMoveReplace" maxlength="255" oninput="massMoveUpdatePreview()" disabled="disabled" /></td>' +
'</tr></table><p></p><table style="background-color:transparent"><thead>' +
'<th colspan="5" scope="col">Options</th>' +
'</thead><tr>' +
'<td class="mincol">Move associated talk page:</td>' +
'<td class="maxcol"><input type="checkbox" id="wpMassMoveMoveTalk" name="wpMassMoveMoveTalk" checked/></td>' +
'<td class="mincol" style="min-width:2em;"> </td>' +
'<td class="mincol">Leave a redirect behind:</td>' +
'<td class="maxcol"><input type="checkbox" id="wpMassMoveLeaveRedirect" name="wpMassMoveLeaveRedirect" checked/></td>' +
'</tr><tr>' +
'<td class="mincol">Move subpages (up to <span id="mmMaxMove">100</span>):</td>' +
'<td class="maxcol"><input type="checkbox" id="wpMassMoveMoveSubPages" name="wpMassMoveMoveSubPages" checked/></td>' +
'<td class="mincol" style="min-width:2em;"> </td>' +
'<td class="mincol">Ignore ratelimit'+(/sysop|bot/.test(mw.config.get("wgUserGroups")) ? "" : " (may cause errors)")+':</td>' +
'<td class="maxcol"><input type="checkbox" id="wpMassMoveNoRatelimit" name="wpMassMoveNoRatelimit"/'+(/sysop|bot/.test(mw.config.get("wgUserGroups")) ? " checked" : "")+'></td>' +
'</tr><tr>' +
'<td class="mincol">Watch source page and target page:</td>' +
'<td colspan="4"><select id="wpMassMoveWatch">' +
'<option value="nochange">No change</option>' +
'<option value="preferences">User preferences</option>' +
'<option value="watch">Add to watch list</option>' +
'<option value="unwatch">Remove from watch list</option>' +
'</select></td>' +
'</tr><tr>' +
'<td class="mincol">Edit summary:</td>' +
'<td colspan="4"><input type="text" id="wpMassMoveReason" name="wpMassMoveReason" maxlength="500" /></td>' +
'</tr></table>' +
'<br /><br />Preview:<br />' +
'<textarea disabled name="wpMassMovePreview" id="wpMassMovePreview" rows="10" cols="80"></textarea>' +
'<br /><br /><input disabled type="button" id="wpMassMoveSubmit" name="wpMassMoveSubmit" value="Move" />' +
'</form>';
$( "#wpMassMoveFlags" ).on("keypress", function (e) {
var key = String.fromCharCode(e.which);
if(!"dgimsuvy".includes(key) || $(this).val().indexOf(key) !== -1) {
e.preventDefault();
var oldOC = $(this).css("outline-color");
$(this).css("outline-color","var(--border-color-error,#f54739)");
setTimeout(() => {$(this).css("outline-color", oldOC)}, 125);
}
});
$( "#wpMassMoveSubmit" ).on("click", function (e) {
$('#wpMassMoveFailedContainer').empty();
$("*", "#wpMassMove").prop('disabled',true);
document.getElementById("wpMassMoveSubmit").value = "Moving...";
window.scrollTo(0,0);
getWait();
});
}
var authorLink = mw.config.get('wgArticlePath').replace('$1','User talk:Ahecht');
function massMoveError() {
document.getElementById(config.bodyContent).innerHTML = config.wpMassMoveIntro +
'For more information, please feel free to contact the <a href="' + authorLink + '">script author</a>!';
}
document.getElementsByTagName("h1")[0].textContent = massMoveTitle;
document.title = massMoveTitle + " - Wikipedia, the free encyclopedia";
config.wpMassMoveIntro = '<div id="siteSub">From Wikipedia, the free encyclopedia</div>' +
'<p>Adapted from Plastikspork\'s mass-move tool, which is in turn adapted ' +
'from Animum\'s mass-delete tool and Timotheus Canens\'s mass-edit tool. ' +
'Please post bug reports/comments/suggestions at <a href="' + authorLink + '">User talk:Ahecht</a>.</p>' +
'<br />' + 'This tool is restricted to editors in the <code>sysop</code>, ' +
'<code>extendedmover</code>, or <code>bot</code> groups.' +
'<br />' + 'Your user groups are: <code>' + mw.config.get('wgUserGroups') + '</code><br /><br />';
if (config.skin == 'modern') {
config.bodyContent = 'mw_contentholder';
} else if (config.skin == 'cologneblue') {
config.bodyContent = 'article';
} else {
config.bodyContent = 'bodyContent';
}
if (/sysop|extendedmover|bot/.test(mw.config.get("wgUserGroups"))) {
massMoveForm();
} else {
massMoveError();
}
}