User:Synoman Barris/common.js: Difference between revisions

From TestWiki
Content added Content deleted
(Test)
(For run)
Line 1: Line 1:
// <nowiki>
/** <nowiki>
* Install this script by pasting the following in your personal JavaScript file:


mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.js/load.js&action=raw&ctype=text/javascript');


* Or for users on en.wikipedia.org:
(function($) {


{{subst:lusc|User:Joeytje50/JWB.js/load.js}}
var api = new mw.Api(), relevantUserName, blockedUserName;
var menuFormattedNamespaces = $.extend({}, mw.config.get('wgFormattedNamespaces'));
menuFormattedNamespaces[0] = '(Article)';


* Note that this script will only run on the 'Project:AutoWikiBrowser/Script' page.
/*
* This script is based on the downloadable AutoWikiBrowser.
****************************************
*
*** twinkleblock.js: Block module
* @licence
****************************************
* This program is free software; you can redistribute it and/or modify
* Mode of invocation: Tab ("Block")
* it under the terms of the GNU General Public License as published by
* Active on: Any page with relevant user name (userspace, contribs, etc.)
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
* @version 4.3.1
* @author Joeytje50
* </nowiki>
*/
*/


window.JWBdeadman = false; // ADMINS: in case of fire, set this variable to true to disable this entire tool for all users
Twinkle.block = function twinkleblock() {
relevantUserName = mw.config.get('wgRelevantUserName');
// should show on Contributions or Block pages, anywhere there's a relevant user
// Ignore ranges wider than the CIDR limit
if (Morebits.userIsSysop && relevantUserName && (!Morebits.ip.isRange(relevantUserName) || Morebits.ip.validCIDR(relevantUserName))) {
Twinkle.addPortletLink(Twinkle.block.callback, 'Block', 'tw-block', 'Block relevant user');
}
};


//TODO: more advanced pagelist-generating options
Twinkle.block.callback = function twinkleblockCallback() {
//TODO: generate page list based on images on a page
if (relevantUserName === mw.config.get('wgUserName') &&
//TODO: Add feature to perform general cleanup (<table> to {|, fullurl-links to wikilinks, removing underscores from wikilinks)
!confirm('You are about to block yourself! Are you sure you want to proceed?')) {
//TODO: Add report button to AJAX error alert box
return;
//TODO: Re-add errored pages to the page list
}


//Cleanup / modernize:
Twinkle.block.currentBlockInfo = undefined;
// .indexOf('') != -1 -> .includes()
Twinkle.block.field_block_options = {};
// mw for requests, instead of ajax
Twinkle.block.field_template_options = {};
// Optional?.chaining?.properties


/***** Global object/variables *****/
var Window = new Morebits.simpleWindow(650, 530);
window.JWB = {}; //The main global object for the script.
// need to be verbose about who we're blocking
Window.setTitle('Block or issue block template to ' + relevantUserName);
Window.setScriptName('Twinkle');
Window.addFooterLink('Block templates', 'Template:Uw-block/doc/Block_templates');
Window.addFooterLink('Block policy', 'WP:BLOCK');
Window.addFooterLink('Block prefs', 'WP:TW/PREF#block');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#block');
Window.addFooterLink('Give feedback', 'WT:TW');


(function() {
// Always added, hidden later if actual user not blocked
// Easier way to change import location for local debugging etc.
Window.addFooterLink('Unblock this user', 'Special:Unblock/' + relevantUserName, true);
JWB.imports = {
'JWB.css': '//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.css&action=raw&ctype=text/css',
'i18n.js': '//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.js/i18n.js&action=raw&ctype=text/javascript',
'i18n': {},
'RETF.js': '//en.wikipedia.org/w/index.php?title=User:Joeytje50/RETF.js&action=raw&ctype=text/javascript',
'worker.js':'//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.js/worker.js&action=raw&ctype=text/javascript',
};
let objs = ['page', 'api', 'worker', 'fn', 'pl', 'messages', 'setup', 'settings', 'ns'];
for (let i=0;i<objs.length;i++) {
JWB[objs[i]] = {};
}
JWB.summarySuffix = ' (via JWB)';
if (document.location.hostname == 'en.wikipedia.org') JWB.summarySuffix = ' (via [[WP:JWB]])';
JWB.lang = mw.config.get('wgUserLanguage').replace('-', '_');
JWB.contentLang = mw.config.get('wgContentLanguage').replace('-', '_');
JWB.index_php = mw.config.get('wgScript');
JWB.isStopped = true;
JWB.tooltip = window.tooltipAccessKeyPrefix || '';
let configext = 'js';
if (document.location.hostname.split('.').slice(-2).join('.') == 'wikia.com' || document.location.hostname.split('.').slice(-2).join('.') == 'fandom.com') {
//LEGACY: fallback to settings on css for Wikia; uses JSON now.
configext = 'css';
}
JWB.settingspage = 'JWB-settings.'+configext;
if (window.hasOwnProperty('JWBSETTINGS')) {
JWB.settingspage = JWBSETTINGS+'-settings.'+configext;
delete window.JWBSETTINGS; //clean up the global variable
}
JWB.hasJSON = false; // whether or not the wiki supports JSON userpages (mw 1.31+).
JWB.hasSMW = false; // whether or not the wiki has SMW installed.
})();


/***** User verification *****/
var form = new Morebits.quickForm(Twinkle.block.callback.evaluate);

var actionfield = form.append({
(function() {
type: 'field',
if (mw.config.get('wgCanonicalNamespace')+':'+mw.config.get('wgTitle') !== 'Project:AutoWikiBrowser/Script' || JWB.allowed === false || mw.config.get('wgUserName') === null) {
label: 'Type of action'
JWB.allowed = false;
return;
}
mw.loader.load(JWB.imports['JWB.css'], 'text/css');
mw.loader.load('mediawiki.diff.styles');
$.getScript(JWB.imports['i18n.js'], function() {
if (JWB.allowed === false) {
alert(JWB.msg('not-on-list'));
return;
}
let langs = [];
if (JWB.lang !== 'en' && JWB.imports.i18n.hasOwnProperty(JWB.lang)) {
langs.push(JWB.imports.i18n[JWB.lang]);
JWB.messages[JWB.lang] = JWB.messages[JWB.lang] || null;
} else if (JWB.lang !== 'en' && JWB.lang !== 'qqx') {
// this only happens if the language file does not exist.
JWB.lang = 'en';
}
if (JWB.contentLang !== 'en' && JWB.contentLang !== JWB.lang && JWB.imports.i18n.hasOwnProperty(JWB.contentLang)) {
langs.push(JWB.imports.i18n[JWB.contentLang]);
JWB.messages[JWB.contentLang] = JWB.messages[JWB.contentLang] || null;
}
if (langs.length) {
$.when.apply($, langs.map(url => $.getScript(url))).done(function(r) {
if (JWB.allowed === true && JWB.messages.length == langs + 1) { // if there are two languages to load, wait for them both.
console.log('langs loaded');
JWB.init(); //init if verification has already returned true
} else if (JWB.allowed === false) {
alert(JWB.msg('not-on-list'));
}
});
} else if (JWB.allowed === true) { // no more languages to load.
console.log('no langs loaded');
JWB.init();
}
});
});
actionfield.append({
//RegEx Typo Fixing
type: 'checkbox',
$.getScript(JWB.imports['RETF.js'], function() {
name: 'actiontype',
$('#refreshRETF').click(RETF.load);
event: Twinkle.block.callback.change_action,
list: [
{
label: 'Block user',
value: 'block',
tooltip: 'Block the relevant user with the given options. If partial block is unchecked, this will be a sitewide block.',
checked: true
},
{
label: 'Partial block',
value: 'partial',
tooltip: 'Enable partial blocks and partial block templates.',
checked: Twinkle.getPref('defaultToPartialBlocks') // Overridden if already blocked
},
{
label: 'Add block template to user talk page',
value: 'template',
tooltip: 'If the blocking admin forgot to issue a block template, or you have just blocked the user without templating them, you can use this to issue the appropriate template. Check the partial block box for partial block templates.',
// Disallow when viewing the block dialog on an IP range
checked: !Morebits.ip.isRange(relevantUserName),
disabled: Morebits.ip.isRange(relevantUserName)
}
]
});
});


if (window.JWBdeadman === true) {
/*
window.JWB = false; // disable all access
Add option for IPv6 ranges smaller than /64 to upgrade to the 64
alert("This tool has been temporarily been disabled by Wikipedia admins due to issues it would otherwise cause. Please check back soon to see if it is working again.");
CIDR ([[WP:/64]]). This is one of the few places where we want
return false;
wgRelevantUserName since this depends entirely on the original user.
} else if (!window.Worker) {
In theory, we shouldn't use Morebits.ip.get64 here since since we want
// https://caniuse.com/webworkers - this should not happen for any sensible human being. Either you're on IE<10, or you're just testing my patience.
to exclude functionally-equivalent /64s. That'd be:
alert("Web Workers are not supported in this browser. Please use a more modern browser to use JWB. Most matching and replacing features are not supported in this browser.");
// if (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName'), true) &&
// (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName')) || parseInt(mw.config.get('wgRelevantUserName').replace(/^(.+?)\/?(\d{1,3})?$/, '$2'), 10) > 64)) {
In practice, though, since functionally-equivalent ranges are
(mis)treated as separate by MediaWiki's logging ([[phab:T146628]]),
using Morebits.ip.get64 provides a modicum of relief in thise case.
*/
var sixtyFour = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
if (sixtyFour && sixtyFour !== mw.config.get('wgRelevantUserName')) {
var block64field = form.append({
type: 'field',
label: 'Convert to /64 rangeblock',
name: 'field_64'
});
block64field.append({
type: 'div',
style: 'margin-bottom: 0.5em',
label: ['It\'s usually fine, if not better, to ', $.parseHTML('<a target="_blank" href="' + mw.util.getUrl('WP:/64') + '">just block the /64</a>')[0], ' range (',
$.parseHTML('<a target="_blank" href="' + mw.util.getUrl('Special:Contributions/' + sixtyFour) + '">' + sixtyFour + '</a>)')[0], ').']
});
block64field.append({
type: 'checkbox',
name: 'block64',
event: Twinkle.block.callback.change_block64,
list: [{
checked: Twinkle.getPref('defaultToBlock64'),
label: 'Block the /64 instead',
value: 'block64',
tooltip: Morebits.ip.isRange(mw.config.get('wgRelevantUserName')) ? 'Will eschew leaving a template.' : 'Any template issued will go to the original IP: ' + mw.config.get('wgRelevantUserName')
}]
});
}
}


(new mw.Api()).get({
form.append({ type: 'field', label: 'Preset', name: 'field_preset' });
action: 'query',
form.append({ type: 'field', label: 'Template options', name: 'field_template_options' });
titles: 'Project:AutoWikiBrowser/CheckPageJSON',
form.append({ type: 'field', label: 'Block options', name: 'field_block_options' });
prop: 'info|revisions',
meta: 'userinfo|siteinfo',
rvprop: 'content',
rvlimit: 1,
uiprop: 'groups',
siprop: 'namespaces|usergroups|extensions',
indexpageids: true,
format: 'json',
}).done(function(response) {
if (response.error) {
alert('API error: ' + response.error.info);
JWB = false; //preventing further access. No verification => no access.
return;
}
JWB.ns = response.query.namespaces; //saving for later
// This will execute before JWB.init() and therefore before JWB.setup.load() loading the user's settings.
let wikigroups = response.query.usergroups;
for (var u of wikigroups) {
if (u.rights.indexOf('edituserjson') !== -1) {
JWB.hasJSON = true;
break;
}
}
// Check if we've got SMW on this wiki
let extensions = response.query.extensions;
for (var e of extensions) {
if (e.name == "SemanticMediaWiki") {
JWB.hasSMW = true;
break;
}
}
JWB.username = response.query.userinfo.name; //preventing any "hacks" that change wgUserName or mw.config.wgUserName
var groups = response.query.userinfo.groups;
var page = response.query.pages[response.query.pageids[0]];
var users = [];
var bots = [];
JWB.sysop = groups.indexOf('sysop') !== -1;
if (response.query.pageids[0] !== '-1') {
var checkPageData = JSON.parse(page.revisions[0]['*']);
users = checkPageData.enabledusers;
if ("enabledbots" in checkPageData) {
bots = checkPageData.enabledbots;
}
} else {
users = false; //fallback when page doesn't exist
if (JWB.sysop) { // Check and inform admins if their checkpage is the unsupported format.
(new mw.Api()).get({
action: 'query',
titles: 'Project:AutoWikiBrowser/CheckPage',
prop: 'info',
indexpageids: true,
}).done(function(oldpage){
var q = oldpage.query;
if (q.pageids[0] != '-1' && !q.pages[q.pageids[0]].hasOwnProperty('redirect')) {
// CheckPageJSON does not exist, and CheckPage does exist, and is not a redirect.
// This indicates the checkpage needs to be ported to JSON. Notify admins.
prompt('Warning: The AWB checkpage found at Project:AutoWikiBrowser/CheckPage is no longer supported.\n'+
'Please convert this checkpage to a JSON checkpage. See the URL below for more information.\n'+
'After creating the JSON checkpage, you can use "Special:ChangeContentModel" to change the content model to JSON.',
'https://en.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/CheckPage_format');
}
});
}
}
JWB.bot = groups.indexOf('bot') !== -1 && (users === false || bots.includes(JWB.username));
// Temporary global debugging variables
JWB.debug = [groups.indexOf('bot'), users === false, bots && bots.indexOf(JWB.username)];
if (JWB.username === "Joeytje50" && response.query.userinfo.id === 13299994) {//TEMP: Dev full access to entire interface.
JWB.bot = true;
users.push("Joeytje50");
}
var allLoaded = true;
for (var m in JWB.messages) if (JWB.messages[m] === null) allLoaded = false;
if (JWB.sysop || response.query.pageids[0] === '-1' || users === false || users.includes(JWB.username) || bots.includes(JWB.username)) {
JWB.allowed = true;
if (allLoaded) JWB.init(); //init if messages have already loaded
} else {
if (allLoaded) {
//run this after messages have loaded, so the message that shows is in the user's language
alert(JWB.msg('not-on-list'));
}
JWB = false; //prevent further access
}
}).fail(function(xhr, error) {
alert(JWB.msg('verify-error') + '\n' + error);
JWB = false; //preventing further access. No verification => no access.
});
})();


/***** API functions *****/
form.append({ type: 'submit' });


//Main template for API calls
var result = form.render();
JWB.api.call = function(data, callback, onerror) {
Window.setContent(result);
data.format = 'json';
Window.display();
if (data.action !== 'query' && data.action !== 'compare' && data.action !== 'ask') {
result.root = result;
data.bot = true; // mark edits as bot

}
Twinkle.block.fetchUserInfo(function() {
$.ajax({
// Toggle initial partial state depending on prior block type,
data: data,
// will override the defaultToPartialBlocks pref
dataType: 'json',
if (blockedUserName === relevantUserName) {
url: mw.config.get('wgScriptPath') + '/api.php',
$(result).find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
type: 'POST',
success: function(response) {
if (response.error) {
if (onerror && onerror(response, 'API') === false) return;
alert('API error: ' + response.error.info);
JWB.stop();
} else {
callback(response);
}
},
// onerror: if it exists and returns false, do not show error alert. Otherwise, do show alert.
error: function(xhr, error) {
if (onerror && onerror(error, 'AJAX') === false) return;
alert('AJAX error: ' + error);
JWB.stop();
}
}
});
};


//Get page diff, and process it for more interactivity
// clean up preset data (defaults, etc.), done exactly once, must be before Twinkle.block.callback.change_action is called
JWB.api.diff = function(callback) {
Twinkle.block.transformBlockPresets();
if (JWB.isStopped) return; // prevent new API calls when stopped

JWB.status('diff');
// init the controls after user and block info have been fetched
var editBoxInput = $('#editBoxArea').val();
var evt = document.createEvent('Event');
var redirect = $('input.redirects:checked').val();
evt.initEvent('change', true, true);
var data = {

action: 'compare',
if (result.block64 && result.block64.checked) {
indexpageids: true,
// Calls the same change_action event once finished
fromtitle: JWB.page.name,
result.block64.dispatchEvent(evt);
//toslots: 'main', // TODO: Once this gets supported more widely, convert to the non-deprecated toslots system.
//'totext-main': editBoxInput,
totext: editBoxInput,
topst: true,
};
if (redirect=='follow') data.redirects = true;
JWB.api.call(data, function(response) {
var diff;
diff = response.compare['*'];
if (diff === '') {
diff = '<h2>'+JWB.msg('no-changes-made')+'</h2>';
} else {
} else {
diff = '<table class="diff">'+
result.actiontype[0].dispatchEvent(evt);
'<colgroup>'+
'<col class="diff-marker">'+
'<col class="diff-content">'+
'<col class="diff-marker">'+
'<col class="diff-content">'+
'</colgroup>'+
'<tbody>'+diff+'</tbody></table>';
}
$('#resultWindow').html(diff);
$('.diff-lineno').each(function() {
var lineNumMatch = $(this).html().match(/\d+/);
if (lineNumMatch) {
$(this).parent().attr('data-line',parseInt(lineNumMatch[0])-1).addClass('lineheader');
}
});
$('table.diff tr').each(function() { //add data-line attribute to every line, relative to the previous one. Used for click event.
if (!$(this).next().is('[data-line]') && !$(this).next().has('td.diff-deletedline + td.diff-empty')) {
$(this).next().attr('data-line',parseInt($(this).data('line'))+1);
} else if ($(this).next().has('td.diff-deletedline + td.diff-empty')) {
$(this).next().attr('data-line',$(this).data('line')); //copy over current data-line for deleted lines to prevent them from messing up counting.
}
});
JWB.status('done', true);
if (typeof(callback) === 'function') {
callback();
}
}, function(err, type) {
if (type == 'API' && err.error.code == 'missingtitle') {
// missingtitle is to be expected when editing a page that doesn't exist; just show a message and move on.
$('#resultWindow').html('<span style="font-weight:bold;color:red;">'+JWB.msg('page-not-exists')+'</span>');
JWB.status('done', true);
if (typeof(callback) === 'function') {
callback();
}
return false; // stop propagation of error; do not show alerts.
}
}
});
});
};
};


//Retrieve page contents/info, process them, and store information in JWB.page object.
// Store fetched user data, only relevant if switching IPv6 to a /64
JWB.api.get = function(pagename) {
Twinkle.block.fetchedData = {};
if (JWB.isStopped) return; // prevent new API calls when stopped
// Processes the data from a a query response, separated from
JWB.pageCount();
// Twinkle.block.fetchUserInfo to allow reprocessing of already-fetched data
if (!JWB.list[0] || JWB.isStopped) {
Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) {
return JWB.stop();
var blockinfo = data.query.blocks[0],
userinfo = data.query.users[0];
// If an IP is blocked *and* rangeblocked, the above finds
// whichever block is more recent, not necessarily correct.
// Three seems... unlikely
if (data.query.blocks.length > 1 && blockinfo.user !== relevantUserName) {
blockinfo = data.query.blocks[1];
}
}
if (pagename === '#PRE-PARSE-STOP') {
// Cache response, used when toggling /64 blocks
var curval = $('#articleList').val();
Twinkle.block.fetchedData[userinfo.name] = data;
$('#articleList').val(curval.substr(curval.indexOf('\n') + 1));

$('#preparse').prop('checked', false);
Twinkle.block.isRegistered = !!userinfo.userid;
JWB.stop();
if (Twinkle.block.isRegistered) {
return;
Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map(function(e) {
return e.group;
}).indexOf('bot') !== -1;
} else {
Twinkle.block.userIsBot = false;
}
}
let cgns = JWB.ns[14]['*'];

let skipcg = $('#skipCategories').val();
if (blockinfo) {
// prepend Category: before all categories and turn CSV(,) into CSV(|).
// handle frustrating system of inverted boolean values
skipcg = skipcg.replace(new RegExp('(^|,|\\|)('+cgns+':)?', 'gi'), '|'+cgns+':').substr(1);
blockinfo.disabletalk = blockinfo.allowusertalk === undefined;
var redirect = $('input.redirects:checked').val();
blockinfo.hardblock = blockinfo.anononly === undefined;
var data = {
action: 'query',
prop: 'info|revisions|categories',
inprop: 'watched|protection',
type: 'csrf|watch',
titles: pagename,
rvprop: 'content|timestamp|ids',
rvlimit: '1',
cllimit: 'max',
clcategories: skipcg,
indexpageids: true,
meta: 'userinfo|tokens',
uiprop: 'hasmsg'
};
if (redirect=='follow'||redirect=='skip') data.redirects = true;
if (JWB.sysop) {
data.list = 'deletedrevs';
}
}
JWB.status('load-page');
// will undefine if no blocks present
JWB.api.call(data, function(response) {
Twinkle.block.currentBlockInfo = blockinfo;
if (response.query.userinfo.hasOwnProperty('messages')) {
blockedUserName = Twinkle.block.currentBlockInfo && Twinkle.block.currentBlockInfo.user;
var view = mw.config.get('wgScriptPath') + '?title=Special:MyTalk';
var viewNew = view + '&diff=cur';
JWB.status(
'<span style="color:red;font-weight:bold;">'+
JWB.msg('status-newmsg',
'<a href="'+view+'" target="_blank">'+JWB.msg('status-talklink')+'</a>',
'<a href="'+viewNew+'" target="_blank">'+JWB.msg('status-difflink')+'</a>')+
'</span>', true);
alert(JWB.msg('new-message'));
JWB.stop();
return;
}
JWB.page = response.query.pages[response.query.pageids[0]];
JWB.page.token = response.query.tokens.csrftoken;
JWB.page.watchtoken = response.query.tokens.watchtoken;
JWB.page.name = JWB.list[0].split('|')[0];
var varOffset = JWB.list[0].indexOf('|') !== -1 ? JWB.list[0].indexOf('|') + 1 : 0;
JWB.page.pagevar = JWB.list[0].substr(varOffset);
JWB.page.content = JWB.page.revisions ? JWB.page.revisions[0]['*'] : '';
JWB.page.exists = !response.query.pages["-1"];
JWB.page.deletedrevs = response.query.deletedrevs;
JWB.page.watched = JWB.page.hasOwnProperty('watched');
JWB.page.protections = JWB.page.restrictiontypes;


if (response.query.redirects) {
// Toggle unblock link if not the user in question; always first
JWB.page.name = response.query.redirects[0].to;
var unblockLink = document.querySelector('.morebits-dialog-footerlinks a');
}
if (blockedUserName !== relevantUserName) {
// check for skips that can be determined before replacing
unblockLink.hidden = true, unblockLink.nextSibling.hidden = true; // link+trailing bullet
if (!JWB.fn.allowBots(JWB.page.content, JWB.username) || !JWB.fn.allowBots(JWB.page.content)) {
} else {
// skip if {{bots}} template forbids editing on this page by user OR by JWB in general
unblockLink.hidden = false, unblockLink.nextSibling.hidden = false; // link+trailing bullet
JWB.log('nobots', JWB.page.name);
}
return JWB.next();
} else if (JWB.page.categories !== undefined || // skip because of a matching category as passed via clcategories.
($('#exists-no').prop('checked') && !JWB.page.exists) ||
($('#exists-yes').prop('checked') && JWB.page.exists) ||
(redirect==='skip' && response.query.redirects) // variable redirect is defined outside this callback function.
) {
// simple skip rules
JWB.log('skip', JWB.page.name);
return JWB.next();
}
// Check skip contains rules.
var containRegex = $('#containRegex').prop('checked'),
containFlags = $('#containFlags').val();
var skipContains, skipNotContains;
if (containRegex) {
JWB.status('check-skips');
var skipping = false; // for tracking if match is found in synchronous calls.
if ($('#skipContains').val().length) {
JWB.worker.match(JWB.page.content, $('#skipContains').val(), containFlags, function(result, err) {
console.log('Contains', result, err);
if (result !== null && err === undefined) {
JWB.log('skip', JWB.page.name);
JWB.next(); // next() also cancels the skipNotContains.
skipping = true;
return;
} // else continue with the queued worker job that checks skipNotContains
});
}
if (skipping) {
console.log('skipped page before replaces')
return;
}
if ($('#skipNotContains').val().length) {
JWB.worker.match(JWB.page.content, $('#skipNotContains').val(), containFlags, function(result, err) {
console.log('Not contains', result, err);
if (result === null && err === undefined) {
JWB.log('skip', JWB.page.name);
JWB.next(); // also cancels the replace
skipping = true;
return;
} // else move on to replacing
});
}
if (skipping) {
console.log('skipped page before replaces')
return;
}
} else {
skipContains = $('#skipContains').val();
skipNotContains = $('#skipNotContains').val();
if ((skipContains && JWB.page.content.includes(skipContains)) ||
(skipNotContains && !JWB.page.content.includes(skipNotContains))) {
console.log('skipped page before replaces')
return JWB.next();
}
JWB.status('done', true);
}
JWB.replace(JWB.page.content, function(newContent) {
if (JWB.isStopped === true) return;
if ($('#skipNoChange').prop('checked') && JWB.page.content === newContent) { //skip if no changes are made
JWB.log('skip', JWB.page.name);
return JWB.next();
} else {
JWB.editPage(newContent);
}
JWB.updateButtons();
});
});
};


//Some functions with self-explanatory names:
// Semi-busted on ranges, see [[phab:T270737]] and [[phab:T146628]].
JWB.api.submit = function(page) {
// Basically, logevents doesn't treat functionally-equivalent ranges
if (JWB.isStopped) return; // prevent new API calls when stopped
// as equivalent, meaning any functionally-equivalent IP range is
JWB.status('submit');
// misinterpreted by the log throughout. Without logevents
var summary = $('#summary').val();
// redirecting (like Special:Block does) we would need a function to
if ($('#summary').parent('label').hasClass('viaJWB')) summary += JWB.summarySuffix;
// parse ranges, which is a pain. IPUtils has the code, but it'd be a
if ((typeof page === 'text' && page !== JWB.page.name) || $('#currentpage a').html().replace(/&amp;/g, '&') !== JWB.page.name) {
// lot of cruft for one purpose.
console.log(page, JWB.page.name, $('#currentpage a').html())
Twinkle.block.hasBlockLog = !!data.query.logevents.length;
JWB.stop();
Twinkle.block.blockLog = Twinkle.block.hasBlockLog && data.query.logevents;
alert(JWB.msg('autosave-error', JWB.msg('tab-log')));
// Used later to check if block status changed while filling out the form
$('#currentpage').html(JWB.msg('editbox-currentpage', ' ', ' '));
Twinkle.block.blockLogId = Twinkle.block.hasBlockLog ? data.query.logevents[0].logid : false;
return;

if (typeof fn === 'function') {
return fn();
}
}
var newval = $('#editBoxArea').val();
var diffsize = newval.length - JWB.page.content.length;
if ($('#sizelimit').val() != 0 && Math.abs(diffsize) > parseInt($('#sizelimit').val())){
alert(JWB.msg('size-limit-exceeded', diffsize > 0 ? '+'+diffsize : diffsize));
JWB.status('done', true);
return;
}
var data = {
title: JWB.page.name,
summary: summary,
action: 'edit',
//tags: 'JWB',
basetimestamp: JWB.page.revisions ? JWB.page.revisions[0].timestamp : '',
token: JWB.page.token,
text: newval,
watchlist: $('#watchPage').val()
};
if ($('#minorEdit').prop('checked')) data.minor = true;
JWB.api.call(data, function(response) {
JWB.log('edit', response.edit.title, response.edit.newrevid);
}, function(error, errtype) {
var cont = false;
if (errtype == 'API') {
cont = confirm("API error: " + error.error.info + "\n" + JWB.msg('confirm-continue'));
} else {
cont = confirm("AJAX error: " + error + "\n" + JWB.msg('confirm-continue'));
}
if (!cont) {
JWB.stop();
}
return false; // do not fall back on default error handling
});
// While the edit is submitting, continue to the next page to edit.
JWB.status('done', true);
JWB.next();
};
};
JWB.api.preview = function() {

if (JWB.isStopped) return; // prevent new API calls when stopped
Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) {
JWB.status('preview');
var query = {
JWB.api.call({
format: 'json',
action: 'query',
title: JWB.page.name,
action: 'parse',
list: 'blocks|users|logevents',
letype: 'block',
pst: true,
text: $('#editBoxArea').val()
lelimit: 1,
}, function(response) {
letitle: 'User:' + relevantUserName,
$('#resultWindow').html(response.parse.text['*']);
bkprop: 'expiry|reason|flags|restrictions|range|user',
$('#resultWindow div.previewnote').remove();
ususers: relevantUserName
JWB.status('done', true);
});
};
JWB.api.move = function() {
if (JWB.isStopped) return; // prevent new API calls when stopped
JWB.status('move');
var topage = $('#moveTo').val().replace(/\$x/gi, JWB.page.pagevar);
var summary = $('#summary').val();
if ($('#summary').parent('label').hasClass('viaJWB')) summary += JWB.summarySuffix;
var data = {
action: 'move',
from: JWB.page.name,
to: topage,
token: JWB.page.token,
reason: summary,
ignorewarnings: 'yes'
};
};
if ($('#moveTalk').prop('checked')) data.movetalk = true;

if ($('#moveSubpage').prop('checked')) data.movesubpages = true;
// bkusers doesn't catch single IPs blocked as part of a range block
if ($('#suppressRedir').prop('checked')) data.noredirect = true;
if (mw.util.isIPAddress(relevantUserName, true)) {
JWB.api.call(data, function(response) {
query.bkip = relevantUserName;
JWB.log('move', response.move.from, response.move.to);
} else {
JWB.status('done', true);
query.bkusers = relevantUserName;
if (!$('#moveTo').val().match(/\$x/i)) $('#moveTo').val('')[0].focus(); //clear entered move-to pagename if it's not based on the pagevar
// groupmemberships only relevant for registered users
JWB.next(topage);
query.usprop = 'groupmemberships';
}

api.get(query).then(function(data) {
Twinkle.block.processUserInfo(data, fn);
}, function(msg) {
Morebits.status.init($('div[name="currentblock"] span').last()[0]);
Morebits.status.warn('Error fetching user info', msg);
});
});
};
};
JWB.api.del = function() {

if (JWB.isStopped) return; // prevent new API calls when stopped
Twinkle.block.callback.saveFieldset = function twinkleblockCallbacksaveFieldset(fieldset) {
JWB.status(($('#deletePage').is('.undelete') ? 'un' : '') + 'delete');
Twinkle.block[$(fieldset).prop('name')] = {};
var summary = $('#summary').val();
$(fieldset).serializeArray().forEach(function(el) {
if ($('#summary').parent('label').hasClass('viaJWB')) summary += JWB.summarySuffix;
// namespaces and pages for partial blocks are overwritten
JWB.api.call({
// here, but we're handling them elsewhere so that's fine
action: (!JWB.page.exists ? 'un' : '') + 'delete',
Twinkle.block[$(fieldset).prop('name')][el.name] = el.value;
title: JWB.page.name,
token: JWB.page.token,
reason: summary
}, function(response) {
JWB.log((!JWB.page.exists ? 'un' : '') + 'delete', (response['delete']||response.undelete).title);
JWB.status('done', true);
JWB.next(response.undelete && response.undelete.title);
});
});
};
};
JWB.api.protect = function() {

if (JWB.isStopped) return; // prevent new API calls when stopped
Twinkle.block.callback.change_block64 = function twinkleblockCallbackChangeBlock64(e) {
JWB.status('protect');
var $form = $(e.target.form), $block64 = $form.find('[name=block64]');
var summary = $('#summary').val();

if ($('#summary').parent('label').hasClass('viaJWB')) summary += JWB.summarySuffix;
// Show/hide block64 button
var editprot = $('#editProt').val();
// Single IPv6, or IPv6 range smaller than a /64
var moveprot = $('#moveProt').val() || editprot;
var priorName = relevantUserName;
var uploadprot = $('#uploadProt').val() || editprot;
if ($block64.is(':checked')) {
var protstring = 'edit='+editprot+'|move='+moveprot;
relevantUserName = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
if (!JWB.page.exists)
} else {
protstring = 'create='+editprot;
relevantUserName = mw.config.get('wgRelevantUserName');
if (JWB.page.protections.includes('upload'))
}
protstring += '|upload='+uploadprot;
// No templates for ranges, but if the original user is a single IP, offer the option
JWB.api.call({
// (done separately in Twinkle.block.callback.issue_template)
action: 'protect',
var originalIsRange = Morebits.ip.isRange(mw.config.get('wgRelevantUserName'));
title: JWB.page.name,
$form.find('[name=actiontype][value=template]').prop('disabled', originalIsRange).prop('checked', !originalIsRange);
token: JWB.page.token,

reason: summary,
// Refetch/reprocess user info then regenerate the main content
expiry: $('#protectExpiry').val()!==''?$('#protectExpiry').val():'infinite',
var regenerateForm = function() {
protections: protstring,
// Tweak titlebar text. In theory, we could save the dialog
}, function(response) {
// at initialization and then use `.setTitle` or
var protactions = '';
// `dialog('option', 'title')`, but in practice that swallows
var prots = response.protect.protections;
// the scriptName and requires `.display`ing, which jumps the
for (var i=0;i<prots.length;i++) {
// window. It's just a line of text, so this is fine.
if (typeof prots[i].edit == 'string') {
var titleBar = document.querySelector('.ui-dialog-title').firstChild.nextSibling;
protactions += ' edit: '+(prots[i].edit || 'all');
titleBar.nodeValue = titleBar.nodeValue.replace(priorName, relevantUserName);
} else if (typeof prots[i].move == 'string') {
// Tweak unblock link
protactions += ' move: '+(prots[i].move || 'all');
var unblockLink = document.querySelector('.morebits-dialog-footerlinks a');
} else if (typeof prots[i].create == 'string') {
unblockLink.href = unblockLink.href.replace(priorName, relevantUserName);
protactions += ' create: '+(prots[i].create || 'all');
unblockLink.title = unblockLink.title.replace(priorName, relevantUserName);
} else if (typeof prots[i].upload == 'string') {

protactions += ' upload: '+(prots[i].upload || 'all');
// Correct partial state
}
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.getPref('defaultToPartialBlocks'));
if (blockedUserName === relevantUserName) {
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
}
protactions += ' expires: '+prots[0].expiry;
JWB.log('protect', response.protect.title, protactions);
JWB.status('done', false);
JWB.next(response.protect.title);
});
};


JWB.api.watch = function() {
// Set content appropriately
JWB.status('watch');
Twinkle.block.callback.change_action(e);
var data = {
action: 'watch',
title: JWB.page.name,
token: JWB.page.watchtoken
};
};
if (JWB.page.watched) data.unwatch = true;

JWB.api.call(data, function(response) {
if (Twinkle.block.fetchedData[relevantUserName]) {
JWB.status('<span style="color:green;">'+
Twinkle.block.processUserInfo(Twinkle.block.fetchedData[relevantUserName], regenerateForm);
JWB.msg('status-watch-'+(JWB.page.watched ? 'removed' : 'added'), "'"+JWB.page.name+"'")+
} else {
'</span>', true);
Twinkle.block.fetchUserInfo(regenerateForm);
JWB.page.watched = !JWB.page.watched;
}
$('#watchNow').html( JWB.msg('watch-' + (JWB.page.watched ? 'remove' : 'add')) );
});
};
};


/***** Pagelist functions *****/
Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction(e) {
var field_preset, field_template_options, field_block_options, $form = $(e.target.form);
// Make ifs shorter
var blockBox = $form.find('[name=actiontype][value=block]').is(':checked');
var templateBox = $form.find('[name=actiontype][value=template]').is(':checked');
var $partial = $form.find('[name=actiontype][value=partial]');
var partialBox = $partial.is(':checked');
var blockGroup = partialBox ? Twinkle.block.blockGroupsPartial : Twinkle.block.blockGroups;


JWB.pl.iterations = 0;
$partial.prop('disabled', !blockBox && !templateBox);
JWB.pl.done = true;


JWB.pl.stop = function() {
// Add current block parameters as default preset
if (JWB.pl.done) {
var prior = { label: 'Prior block' };
JWB.pl.iterations = 0;
if (blockedUserName === relevantUserName) {
$('#pagelistPopup [disabled]:not(fieldset [disabled]), #pagelistPopup legend input, #pagelistPopup button').prop('disabled', false);
Twinkle.block.blockPresetsInfo.prior = Twinkle.block.currentBlockInfo;
$('#pagelistPopup legend input').trigger('change');
// value not a valid template selection, chosen below by setting templateName
$('#pagelistPopup button img').remove();
prior.list = [{ label: 'Prior block settings', value: 'prior', selected: true }];
}
}


JWB.pl.getNSpaces = function() {
// Arrays of objects are annoying to check
var list = $('#pagelistPopup [name="namespace"]')[0];
if (!blockGroup.some(function(bg) {
return $('#pagelistPopup [name="namespace"]').val().join('|'); //.val() returns an array of selected options.
return bg.label === prior.label;
};
})) {
blockGroup.push(prior);
}


JWB.pl.getList = function(abbrs, lists, data) {
// Always ensure proper template exists/is selected when switching modes
$('#pagelistPopup button, #pagelistPopup input, #pagelistPopup select, #pagelistPopup button').prop('disabled', true);
if (partialBox) {
JWB.pl.iterations++;
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-pblockindef' : 'uw-pblock';
if (data.ask !== undefined) {
} else {
JWB.pl.SMW(data.ask); // execute SMW call in parallel
if (!Twinkle.block.isRegistered) {
JWB.pl.done = false;
Twinkle.block.blockPresetsInfo.prior.templateName = 'uw-ablock';
data.ask = undefined;
} else {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-blockindef' : 'uw-block';
}
}
} else {
// But first remove any prior prior
blockGroup = blockGroup.filter(function(bg) {
return bg.label !== prior.label;
});
}
}
if (!abbrs.length) {

JWB.pl.done = true;
// Can be in preset or template field, so the old one in the template
return; // don't execute the rest; only a SMW query was entered.
// field will linger. No need to keep the old value around, so just
}
// remove it; saves trouble when hiding/evaluating
data.action = 'query';
$form.find('[name=dstopic]').parent().remove();
var nspaces = JWB.pl.getNSpaces();

for (var i=0;i<abbrs.length;i++) {
Twinkle.block.callback.saveFieldset($('[name=field_block_options]'));
if (nspaces) data[abbrs[i]+'namespace'] = data[abbrs[i]+'namespace'] || nspaces; // if namespaces are already set, use that instead (for apnamespace)
Twinkle.block.callback.saveFieldset($('[name=field_template_options]'));
data[abbrs[i]+'limit'] = 'max';

}
if (blockBox) {
let linksList = lists.indexOf('links')
field_preset = new Morebits.quickForm.element({ type: 'field', label: 'Preset', name: 'field_preset' });
if (linksList !== -1) {
field_preset.append({
type: 'select',
data.prop = 'links';
lists.splice(linksList, 1)
name: 'preset',
}
label: 'Choose a preset:',
data.list = lists.join('|');
event: Twinkle.block.callback.change_preset,
console.log('generating:', data);
list: Twinkle.block.callback.filtered_block_groups(blockGroup)
JWB.api.call(data, function(response) {
});
var maxiterate = 100; //allow up to 100 consecutive requests at a time to avoid overloading the server.

if (!response.query) response.query = {};
field_block_options = new Morebits.quickForm.element({ type: 'field', label: 'Block options', name: 'field_block_options' });
if (response.watchlistraw) response.query.watchlistraw = response.watchlistraw; //adding some consistency
field_block_options.append({ type: 'div', name: 'currentblock', label: ' ' });
var plist = [];
field_block_options.append({ type: 'div', name: 'hasblocklog', label: ' ' });
if (response.query.pages) {
field_block_options.append({
type: 'select',
var links;
for (var id in response.query.pages) {
name: 'expiry_preset',
links = response.query.pages[id].links;
label: 'Expiry:',
for (var i=0;i<links.length;i++) {
event: Twinkle.block.callback.change_expiry,
plist.push(links[i].title);
list: [
{ label: 'custom', value: 'custom', selected: true },
{ label: 'indefinite', value: 'infinity' },
{ label: '3 hours', value: '3 hours' },
{ label: '12 hours', value: '12 hours' },
{ label: '24 hours', value: '24 hours' },
{ label: '31 hours', value: '31 hours' },
{ label: '36 hours', value: '36 hours' },
{ label: '48 hours', value: '48 hours' },
{ label: '60 hours', value: '60 hours' },
{ label: '72 hours', value: '72 hours' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '3 months', value: '3 months' },
{ label: '6 months', value: '6 months' },
{ label: '1 year', value: '1 year' },
{ label: '2 years', value: '2 years' },
{ label: '3 years', value: '3 years' }
]
});
field_block_options.append({
type: 'input',
name: 'expiry',
label: 'Custom expiry',
tooltip: 'You can use relative times, like "1 minute" or "19 days", or absolute timestamps, "yyyymmddhhmm" (e.g. "200602011405" is Feb 1, 2006, at 14:05 UTC).',
value: Twinkle.block.field_block_options.expiry || Twinkle.block.field_template_options.template_expiry
});

if (partialBox) { // Partial block
field_block_options.append({
type: 'select',
multiple: true,
name: 'pagerestrictions',
label: 'Specific pages to block from editing',
value: '',
tooltip: '10 page max.'
});
var ns = field_block_options.append({
type: 'select',
multiple: true,
name: 'namespacerestrictions',
label: 'Namespace blocks',
value: '',
tooltip: 'Block from editing these namespaces.'
});
$.each(menuFormattedNamespaces, function(number, name) {
// Ignore -1: Special; -2: Media; and 2300-2303: Gadget (talk) and Gadget definition (talk)
if (number >= 0 && number < 830) {
ns.append({ type: 'option', label: name, value: number });
}
}
});
}
}
}
for (var l in response.query) {

if (l === 'pages') continue;
var blockoptions = [
for (var i=0;i<response.query[l].length;i++) {
{
plist.push(response.query[l][i].title);
checked: Twinkle.block.field_block_options.nocreate,
label: 'Block account creation',
name: 'nocreate',
value: '1'
},
{
checked: Twinkle.block.field_block_options.noemail,
label: 'Block user from sending email',
name: 'noemail',
value: '1'
},
{
checked: Twinkle.block.field_block_options.disabletalk,
label: 'Prevent this user from editing their own talk page while blocked',
name: 'disabletalk',
value: '1',
tooltip: partialBox ? 'If issuing a partial block, this MUST remain unchecked unless you are also preventing them from editing User talk space' : ''
}
}
];

if (Twinkle.block.isRegistered) {
blockoptions.push({
checked: Twinkle.block.field_block_options.autoblock,
label: 'Autoblock any IP addresses used (hardblock)',
name: 'autoblock',
value: '1'
});
} else {
blockoptions.push({
checked: Twinkle.block.field_block_options.hardblock,
label: 'Block logged-in users from using this IP address (hardblock)',
name: 'hardblock',
value: '1'
});
}
}
//add the result to the pagelist immediately, as opposed to saving it all up and adding in 1 go like AWB does

$('#articleList').val($.trim($('#articleList').val()) + '\n' + plist.join('\n'));
blockoptions.push({
JWB.pageCount();
checked: Twinkle.block.field_block_options.watchuser,
var cont = response.continue;
label: 'Watch user and user talk pages',
console.log("Continue",JWB.pl.iterations, cont);
name: 'watchuser',
if (cont && JWB.pl.iterations <= maxiterate) {
value: '1'
var lists = [];
});
if (response.query) { //compatibility with the code I wrote for the old query-continue. TODO: make this unnecessary?

for (var list in response.query) {
field_block_options.append({
lists.push(list); //add to the new array of &list= values
type: 'checkbox',
name: 'blockoptions',
list: blockoptions
});
field_block_options.append({
type: 'textarea',
label: 'Reason (for block log):',
name: 'reason',
tooltip: 'Consider adding helpful details to the default message.',
value: Twinkle.block.field_block_options.reason
});

field_block_options.append({
type: 'div',
name: 'filerlog_label',
label: 'See also:',
style: 'display:inline-block;font-style:normal !important',
tooltip: 'Insert a "see also" message to indicate whether the filter log or deleted contributions played a role in the decision to block.'
});
field_block_options.append({
type: 'checkbox',
name: 'filter_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: 'Filter log',
checked: false,
value: 'filter log'
}
}
]
}
var abbrs = [];
});
for (var abbr in cont) {
field_block_options.append({
data[abbr] = cont[abbr]; //add the &xxcontinue= value to the data
type: 'checkbox',
if (abbr != 'continue') {
name: 'deleted_see_also',
abbrs.push(abbr.replace('continue','')); //find out what xx is and add it to the list of abbrs
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block',
list: [
{
label: 'Deleted contribs',
checked: false,
value: 'deleted contribs'
}
}
]
}
JWB.pl.getList(abbrs, lists, data); //recursive function to get every page of a list
});
} else {

if (JWB.pl.iterations > maxiterate) {
// Yet-another-logevents-doesn't-handle-ranges-well
JWB.status('pl-over-lim', true);
if (blockedUserName === relevantUserName) {
} else {
field_block_options.append({ type: 'hidden', name: 'reblock', value: '1' });
JWB.status('done', true);
}
JWB.pl.stop(); // if JWB.pl.done == true show stopped interface. Otherwise mark as done.
JWB.pl.done = true;
}
}
}, function() { //on error, simply reset and let the user work with what he has
}
JWB.status('done', true);
JWB.pl.stop();
JWB.pl.done = true;
});
};


JWB.pl.SMW = function(query) {
// DS selection visible in either the template field set or preset,
var data = {
// joint settings saved here
action: 'ask',
var dsSelectSettings = {
type: 'select',
query: query
name: 'dstopic',
label: 'DS topic',
value: '',
tooltip: 'If selected, it will inform the template and may be added to the blocking message',
event: Twinkle.block.callback.toggle_ds_reason,
list: $.map(Twinkle.block.dsinfo, function(info, label) {
return {label: label, value: info.code};
})
};
};
JWB.api.call(data, function(response) {
if (templateBox) {
console.log(response);
field_template_options = new Morebits.quickForm.element({ type: 'field', label: 'Template options', name: 'field_template_options' });
let list = response.query.results;
field_template_options.append({
let pagevar = response.query.printrequests[1];
type: 'select',
let pagevar_type = pagevar && pagevar.typeid;
name: 'template',
if (pagevar) {
label: 'Choose talk page template:',
// either pagevar === undefined, or it's the first printrequest.
event: Twinkle.block.callback.change_template,
pagevar = pagevar.label;
list: Twinkle.block.callback.filtered_block_groups(blockGroup, true),
value: Twinkle.block.field_template_options.template
});

// Only visible for aeblock and aepblock, toggled in change_template
field_template_options.append(dsSelectSettings);

field_template_options.append({
type: 'input',
name: 'article',
label: 'Linked page',
value: '',
tooltip: 'A page can be linked within the notice, perhaps if it was the primary target of disruption. Leave empty for no page to be linked.'
});

// Only visible if partial and not blocking
field_template_options.append({
type: 'input',
name: 'area',
label: 'Area blocked from',
value: '',
tooltip: 'Optional explanation of the pages or namespaces the user was blocked from editing.'
});

if (!blockBox) {
field_template_options.append({
type: 'input',
name: 'template_expiry',
label: 'Period of blocking:',
value: '',
tooltip: 'The period the blocking is due for, for example 24 hours, 2 weeks, indefinite etc...'
});
}
}
let plist = [];
field_template_options.append({
for (let l in list) {
type: 'input',
let page = list[l];
name: 'block_reason',
let name = page.fulltext;
label: '"You have been blocked for ..."',
let suff;
tooltip: 'An optional reason, to replace the default generic reason. Only available for the generic block templates.',
if (pagevar) try {
value: Twinkle.block.field_template_options.block_reason
let val = page.printouts[pagevar][0];
});
if (!val) continue; // this page does not contain this property.
switch (pagevar_type) {
case '_boo':
suff = val == 't'; // true if 't' else false;
break;
case '_wpg':
suff = val.fulltext;
break;
case '_dat':
// val.raw is also available but the unconventional format makes it a lot less convenient.
suff = val.timestamp;
break;
case '_qty':
suff = val.value + ' ' + val.unit;
break;
case '_mlt_rec':
// I doubt this is used anywhere, but it's not too hard to support.
suff = val.Text.item[0];
break;
case '_ref_rec':
// not supported; references contain too many properties.
break;
default:
suff = val;
}
} catch(e) {
console.error(e); // show error but ignore. Something is wrong in SMW query/api.
}
if (suff) {
plist.push(name + '|' + suff);
} else {
plist.push(name);
}
}
$('#articleList').val($.trim($('#articleList').val()) + '\n' + plist.join('\n'));
JWB.pageCount();
JWB.pl.stop(); // if JWB.pl.done == true show stopped interface. Otherwise mark as done.
JWB.pl.done = true;
});
}


//JWB.pl.getList(['wr'], ['watchlistraw'], {}) for watchlists
if (blockBox) {
JWB.pl.generate = function() {
field_template_options.append({
var $fields = $('#pagelistPopup fieldset').not('[disabled]');
type: 'checkbox',
$('#pagelistPopup').find('button[type="submit"]').append('<img src="//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif" width="15" height="15" alt="'+JWB.msg('status-alt')+'"/>');
name: 'blank_duration',
var abbrs = [],
list: [
lists = [],
{
data = {'continue': ''};
label: 'Do not include expiry in template',
$fields.each(function() {
checked: Twinkle.block.field_template_options.blank_duration,
var list = $(this).find('legend input').attr('name');
tooltip: 'Instead of including the duration, make the block template read "You have been blocked temporarily..."'
var abbr;
}
if (list === 'linksto') { //Special case since this fieldset features 3 merged lists in 1 fieldset
]
if (!$('[name="title"]').val()) return;
$('[name="backlinks"], [name="embeddedin"], [name="imageusage"]').filter(':checked').each(function() {
var val = this.value;
abbrs.push(val);
lists.push(this.name);
data[val+'title'] = $('[name="title"]').val();
data[val+'filterredir'] = $('[name="filterredir"]:checked').val();
if ($('[name="redirect"]').prop('checked')) data[val+'redirect'] = true;
});
});
} else {
} else if (list === 'smwask') {
data.ask = $(this).find('#smwquery').val();
field_template_options.append({
} else { //default input system
type: 'checkbox',
if ($(this).find('#psstrict').prop('checked')) {
list: [
// different list if prefixsearch is strict
{
let $input = $(this).find('#psstrict')
label: 'Talk page access disabled',
list = $input.attr('name');
name: 'notalk',
abbr = $input.val();
checked: Twinkle.block.field_template_options.notalk,
} else {
tooltip: 'Make the block template state that the user\'s talk page access has been removed'
abbr = $(this).find('legend input').val();
},
{
}
lists.push(list);
label: 'User blocked from sending email',
abbrs.push(abbr);
name: 'noemail_template',
$(this).find('input').not('legend input').each(function() {
checked: Twinkle.block.field_template_options.noemail_template,
if ((this.type === 'checkbox' || this.type === 'radio') && this.checked === false) return;
tooltip: 'If the area is not provided, make the block template state that the user\'s email access has been removed'
if (this.id == 'psstrict') return; // ignore psstrict; it only affects how pssearch is handled
},
{
var name, val;
if (this.id == 'cmtitle') {
label: 'User blocked from creating accounts',
// making sure the page has a Category: prefix, in case the user left it out
name: 'nocreate_template',
let cgns = JWB.ns[14]['*']; // name for Category: namespace
checked: Twinkle.block.field_template_options.nocreate_template,
if (!this.value.startsWith(cgns+':')) {
tooltip: 'If the area is not provided, make the block template state that the user\'s ability to create accounts has been removed'
this.value = cgns+':'+this.value;
}
}
]
});
}

var $previewlink = $('<a id="twinkleblock-preview-link">Preview</a>');
$previewlink.off('click').on('click', function() {
Twinkle.block.callback.preview($form[0]);
});
$previewlink.css({cursor: 'pointer'});
field_template_options.append({ type: 'div', id: 'blockpreview', label: [ $previewlink[0] ] });
field_template_options.append({ type: 'div', id: 'twinkleblock-previewbox', style: 'display: none' });
} else if (field_preset) {
// Only visible for arbitration enforcement, toggled in change_preset
field_preset.append(dsSelectSettings);
}

var oldfield;
if (field_preset) {
oldfield = $form.find('fieldset[name="field_preset"]')[0];
oldfield.parentNode.replaceChild(field_preset.render(), oldfield);
} else {
$form.find('fieldset[name="field_preset"]').hide();
}
if (field_block_options) {
oldfield = $form.find('fieldset[name="field_block_options"]')[0];
oldfield.parentNode.replaceChild(field_block_options.render(), oldfield);
$form.find('fieldset[name="field_64"]').show();


$form.find('[name=pagerestrictions]').select2({
width: '100%',
placeholder: 'Select pages to block user from',
language: {
errorLoading: function() {
return 'Incomplete or invalid search term';
}
}
if (this.id == 'pssearch' && this.name == 'apprefix') {
},
// apprefix needs namespace separate from pagename
maximumSelectionLength: 10, // Software limitation [[phab:T202776]]
name = this.name;
minimumInputLength: 1, // prevent ajax call when empty
let split = this.value.split(':')
ajax: {
val = split[1] || split[0];
url: mw.util.wikiScript('api'),
dataType: 'json',
let nsid = 0;
if (split[1]) { // if a namespace is given
delay: 100,
data: function(params) {
for (let ns in JWB.ns) {
if (JWB.ns[ns]['*'] == split[0]) {
var title = mw.Title.newFromText(params.term);
nsid = JWB.ns[ns].id;
if (!title) {
return;
break;
}
}
}
}
return {
data.apnamespace = nsid;
action: 'query',
} else {
format: 'json',
name = this.name;
list: 'allpages',
val = this.value;
apfrom: title.title,
apnamespace: title.namespace,
aplimit: '10'
};
},
processResults: function(data) {
return {
results: data.query.allpages.map(function(page) {
var title = mw.Title.newFromText(page.title, page.ns).toText();
return {
id: title,
text: title
};
})
};
}
}
if (data.hasOwnProperty(name)) {
},
data[name] += '|'+val;
templateSelection: function(choice) {
} else {
return $('<a>').text(choice.text).attr({
data[name] = val;
href: mw.util.getUrl(choice.text),
}
target: '_blank'
});
});
console.log(abbrs, lists, data);
}
});
}
});
if (abbrs.length || data.ask) JWB.pl.getList(abbrs, lists, data);
else JWB.pl.stop();
};


/***** Setup functions *****/
$form.find('[name=namespacerestrictions]').select2({
width: '100%',
matcher: Morebits.select2.matchers.wordBeginning,
language: {
searching: Morebits.select2.queryInterceptor
},
templateResult: Morebits.select2.highlightSearchMatches,
placeholder: 'Select namespaces to block user from'
});


JWB.setup.save = function(name) {
mw.util.addCSS(
name = name || prompt(JWB.msg('setup-prompt', JWB.msg('setup-prompt-store')), $('#loadSettings').val());
// Reduce padding
if (name === null) return;
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
var self = JWB.settings[name] = {
// Adjust font size
string: {},
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
bool: {},
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }' +
replaces: []
// Remove black border
};
'.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' +
//inputs with a text value
// Make the tiny cross larger
$('textarea, input[type="text"], input[type="number"], select').not('.replaces input, #editBoxArea, #settings *').each(function() {
'.select2-selection__choice__remove { font-size: 130%; }'
if (typeof $(this).val() == 'string') {
);
self.string[this.id] = this.value.replace(/\n{2,}/g,'\n');
} else {
} else {
$form.find('fieldset[name="field_block_options"]').hide();
self.string[this.id] = $(this).val();
$form.find('fieldset[name="field_64"]').hide();
}
// Clear select2 options
});
$form.find('[name=pagerestrictions]').val(null).trigger('change');
self.replaces = [];
$form.find('[name=namespacerestrictions]').val(null).trigger('change');
$('.replaces').each(function() {
if ($(this).find('.replaceText').val() || $(this).find('.replaceWith').val()) {
self.replaces.push({
replaceText: $(this).find('.replaceText').val(),
replaceWith: $(this).find('.replaceWith').val(),
useRegex: $(this).find('.useRegex').prop('checked'),
regexFlags: $(this).find('.regexFlags').val(),
ignoreNowiki: $(this).find('.ignoreNowiki').prop('checked')
});
}
});
$('input[type="radio"], input[type="checkbox"]').not('.replaces input').each(function() {
self.bool[this.id] = this.checked;
});
if (!$('#loadSettings option[value="'+name+'"]').length) {
$('#loadSettings').append('<option value="'+name+'">'+name+'</option>');
}
}
$('#loadSettings').val(name);
console.log(self);
};


JWB.setup.apply = function(name) {
if (field_template_options) {
name = name && JWB.settings[name] ? name : 'default';
oldfield = $form.find('fieldset[name="field_template_options"]')[0];
var self = JWB.settings[name];
oldfield.parentNode.replaceChild(field_template_options.render(), oldfield);
$('#loadSettings').val(name);
e.target.form.root.previewer = new Morebits.wiki.preview($(e.target.form.root).find('#twinkleblock-previewbox').last()[0]);
$('.replaces + .replaces').remove(); //reset find&replace inputs
} else {
$('.replaces input[type="text"]').val('');
$form.find('fieldset[name="field_template_options"]').hide();
$('.useRegex').each(function() {this.checked = false;});
$('#pagelistPopup legend input').trigger('change'); //fix checked state of pagelist generating inputs
for (var a in self.string) {
$('#'+a).val(self.string[a]);
}
}
for (var b in self.bool) {

$('#'+b).prop('checked', self.bool[b]);
// Any block, including ranges
}
if (Twinkle.block.currentBlockInfo) {
var cur;
// false for an ip covered by a range or a smaller range within a larger range;
for (var c=0;c<self.replaces.length;c++) {
// true for a user, single ip block, or the exact range for a range block
if ($('.replaces').length <= c) $('#moreReplaces')[0].click();
var sameUser = blockedUserName === relevantUserName;
cur = self.replaces[c];

for (var d in cur) {
Morebits.status.init($('div[name="currentblock"] span').last()[0]);
if (cur[d] === true || cur[d] === false) {
var statusStr = relevantUserName + ' is ' + (Twinkle.block.currentBlockInfo.partial === '' ? 'partially blocked' : 'blocked sitewide');
$('.replaces').eq(c).find('.'+d).prop('checked', cur[d]);

// Range blocked
if (Twinkle.block.currentBlockInfo.rangestart !== Twinkle.block.currentBlockInfo.rangeend) {
if (sameUser) {
statusStr += ' as a rangeblock';
} else {
} else {
$('.replaces').eq(c).find('.'+d).val(cur[d]);
statusStr += ' within a' + (Morebits.ip.get64(relevantUserName) === blockedUserName ? ' /64' : '') + ' rangeblock';
// Link to the full range
var $rangeblockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: blockedUserName, type: 'block'}) + '">' + blockedUserName + '</a>)'));
statusStr += ' (' + $rangeblockloglink.html() + ')';
}
}
}
}

if (Twinkle.block.currentBlockInfo.expiry === 'infinity') {
statusStr += ' (indefinite)';
} else if (new Morebits.date(Twinkle.block.currentBlockInfo.expiry).isValid()) {
statusStr += ' (expires ' + new Morebits.date(Twinkle.block.currentBlockInfo.expiry).calendar('utc') + ')';
}


var infoStr = 'This form will';
if (sameUser) {
infoStr += ' change that block';
if (Twinkle.block.currentBlockInfo.partial === undefined && partialBox) {
infoStr += ', converting it to a partial block';
} else if (Twinkle.block.currentBlockInfo.partial === '' && !partialBox) {
infoStr += ', converting it to a sitewide block';
}
infoStr += '.';
} else {
infoStr += ' add an additional ' + (partialBox ? 'partial ' : '') + 'block.';
}

Morebits.status.warn(statusStr, infoStr);

// Default to the current block conditions on intial form generation
Twinkle.block.callback.update_form(e, Twinkle.block.currentBlockInfo);
}
}
$('.useRegex, #containRegex,'+
'#pagelistPopup legend input,'+
'#viaJWB, #enableRETF').trigger('change'); //reset disabled inputs
};


JWB.setup.getObj = function() {
// This is where T146628 really comes into play: a rangeblock will
var settings = [];
// only return the correct block log if wgRelevantUserName is the
for (var i in JWB.settings) {
// exact range, not merely a funtional equivalent
if (Twinkle.block.hasBlockLog) {
if (i != '_blank') {
settings.push('"' + i + '": ' + JSON.stringify(JWB.settings[i]));
var $blockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: relevantUserName, type: 'block'}) + '">block log</a>)'));
if (!Twinkle.block.currentBlockInfo) {
var lastBlockAction = Twinkle.block.blockLog[0];
if (lastBlockAction.action === 'unblock') {
$blockloglink.append(' (unblocked ' + new Morebits.date(lastBlockAction.timestamp).calendar('utc') + ')');
} else { // block or reblock
$blockloglink.append(' (' + lastBlockAction.params.duration + ', expired ' + new Morebits.date(lastBlockAction.params.expiry).calendar('utc') + ')');
}
}
}

Morebits.status.init($('div[name="hasblocklog"] span').last()[0]);
Morebits.status.warn(Twinkle.block.currentBlockInfo ? 'Previous blocks' : 'This ' + (Morebits.ip.isRange(relevantUserName) ? 'range' : 'user') + ' has been blocked in the past', $blockloglink[0]);
}

// Make sure all the fields are correct based on initial defaults
if (blockBox) {
Twinkle.block.callback.change_preset(e);
} else if (templateBox) {
Twinkle.block.callback.change_template(e);
}
}
return '{\n\t' + settings.join(',\n\t').split('{{subst:').join('{{#JWB-SAFESUBST:#') + '\n}';
};
};


JWB.setup.submit = function() {
/*
var name = prompt(JWB.msg('setup-prompt', JWB.msg('setup-prompt-save')), $('#loadSettings').val());
* Keep alphabetized by key name, Twinkle.block.blockGroups establishes
if (name === null) return;
* the order they will appear in the interface
if ($.trim(name) === '') name = 'default';
*
JWB.setup.save(name);
* Block preset format, all keys accept only 'true' (omit for false) except where noted:
JWB.status('setup-submit');
* <title of block template> : {
JWB.api.call({
* autoblock: <autoblock any IP addresses used (for registered users only)>
action: 'query',
* disabletalk: <disable user from editing their own talk page while blocked>
meta: 'tokens',
* expiry: <string - expiry timestamp, can include relative times like "5 months", "2 weeks" etc>
}, function(response) {
* forAnonOnly: <show block option in the interface only if the relevant user is an IP>
let edittoken = response.query.tokens.csrftoken;
* forRegisteredOnly: <show block option in the interface only if the relevant user is registered>
JWB.api.call({
* label: <string - label for the option of the dropdown in the interface (keep brief)>
title: 'User:'+JWB.username+'/'+JWB.settingspage,
* noemail: prevent the user from sending email through Special:Emailuser
summary: JWB.msg(['setup-summary', JWB.contentLang]),
* pageParam: <set if the associated block template accepts a page parameter>
action: 'edit',
* prependReason: <string - prepends the value of 'reason' to the end of the existing reason, namely for when revoking talk page access>
token: edittoken,
* nocreate: <block account creation from the user's IP (for anonymous users only)>
text: JWB.setup.getObj(),
* nonstandard: <template does not conform to stewardship of WikiProject User Warnings and may not accept standard parameters>
minor: true
* reason: <string - block rationale, as would appear in the block log,
}, function(response) {
* and the edit summary for when adding block template, unless 'summary' is set>
JWB.status('done', true);
* reasonParam: <set if the associated block template accepts a reason parameter>
JWB.log('edit', response.edit.title, response.edit.newrevid);
* sig: <string - set to ~~~~ if block template does not accept "true" as the value, or set null to omit sig param altogether>
});
* summary: <string - edit summary for when adding block template to user's talk page, if not set, 'reason' is used>
});
* suppressArticleInSummary: <set to suppress showing the article name in the edit summary, as with attack pages>
* templateName: <string - name of template to use (instead of key name), entry will be omitted from the Templates list.
* (e.g. use another template but with different block options)>
* useInitialOptions: <when preset is chosen, only change given block options, leave others as they were>
*
* WARNING: 'anononly' and 'allowusertalk' are enabled by default.
* To disable, set 'hardblock' and 'disabletalk', respectively
*/
Twinkle.block.blockPresetsInfo = {
'anonblock': {
expiry: '31 hours',
forAnonOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{anonblock}}',
sig: '~~~~'
},
'anonblock - school': {
expiry: '36 hours',
forAnonOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{anonblock}} <!-- Likely a school based on behavioral evidence -->',
templateName: 'anonblock',
sig: '~~~~'
},
'blocked proxy': {
expiry: '1 year',
forAnonOnly: true,
nocreate: true,
nonstandard: true,
hardblock: true,
reason: '{{blocked proxy}}',
sig: null
},
'CheckUser block': {
expiry: '1 week',
forAnonOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{CheckUser block}}',
sig: '~~~~'
},
'checkuserblock-account': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-account}}',
sig: '~~~~'
},
'checkuserblock-wide': {
forAnonOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-wide}}',
sig: '~~~~'
},
'colocationwebhost': {
expiry: '1 year',
forAnonOnly: true,
nonstandard: true,
reason: '{{colocationwebhost}}',
sig: null
},
'oversightblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
nonstandard: true,
reason: '{{OversightBlock}}',
sig: '~~~~'
},
'school block': {
forAnonOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{school block}}',
sig: '~~~~'
},
'spamblacklistblock': {
forAnonOnly: true,
expiry: '1 month',
disabletalk: true,
nocreate: true,
reason: '{{spamblacklistblock}} <!-- editor only attempts to add blacklisted links, see [[Special:Log/spamblacklist]] -->'
},
'rangeblock': {
reason: '{{rangeblock}}',
nocreate: true,
nonstandard: true,
forAnonOnly: true,
sig: '~~~~'
},
'tor': {
expiry: '1 year',
forAnonOnly: true,
nonstandard: true,
reason: '{{Tor}}',
sig: null
},
'webhostblock': {
expiry: '1 year',
forAnonOnly: true,
nonstandard: true,
reason: '{{webhostblock}}',
sig: null
},
// uw-prefixed
'uw-3block': {
autoblock: true,
expiry: '24 hours',
nocreate: true,
pageParam: true,
reason: 'Violation of the [[WP:Three-revert rule|three-revert rule]]',
summary: 'You have been blocked from editing for violation of the [[WP:3RR|three-revert rule]]'
},
'uw-ablock': {
autoblock: true,
expiry: '31 hours',
forAnonOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'Your IP address has been blocked from editing',
suppressArticleInSummary: true
},
'uw-adblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: 'Using Wikipedia for [[WP:Spam|spam]] or [[WP:NOTADVERTISING|advertising]] purposes',
summary: 'You have been blocked from editing for [[WP:SOAP|advertising or self-promotion]]'
},
'uw-aeblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:Arbitration enforcement|Arbitration enforcement]]',
reasonParam: true,
summary: 'You have been blocked from editing for violating an [[WP:Arbitration|arbitration decision]]'
},
'uw-bioblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: 'Violations of the [[WP:Biographies of living persons|biographies of living persons]] policy',
summary: 'You have been blocked from editing for violations of Wikipedia\'s [[WP:BLP|biographies of living persons policy]]'
},
'uw-block': {
autoblock: true,
expiry: '24 hours',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'You have been blocked from editing',
suppressArticleInSummary: true
},
'uw-blockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'You have been indefinitely blocked from editing',
suppressArticleInSummary: true
},
'uw-blocknotalk': {
disabletalk: true,
pageParam: true,
reasonParam: true,
summary: 'You have been blocked from editing and your user talk page access has been disabled',
suppressArticleInSummary: true
},
'uw-botblock': {
forRegisteredOnly: true,
pageParam: true,
reason: 'Running a [[WP:BOT|bot script]] without [[WP:BRFA|approval]]',
summary: 'You have been blocked from editing because it appears you are running a [[WP:BOT|bot script]] without [[WP:BRFA|approval]]'
},
'uw-botublock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-botublock}} <!-- Username implies a bot, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] indicates this is a [[WP:BOT|bot]] account, which is currently not approved'
},
'uw-botuhblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-botuhblock}} <!-- Username implies a bot, hard block -->',
summary: 'You have been indefinitely blocked from editing because your username is a blatant violation of the [[WP:U|username policy]].'
},
'uw-causeblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-causeblock}} <!-- Username represents a non-profit, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] gives the impression that the account represents a group, organization or website'
},
'uw-compblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: 'Compromised account',
summary: 'You have been indefinitely blocked from editing because it is believed that your [[WP:SECURE|account has been compromised]]'
},
'uw-copyrightblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '[[WP:Copyright violations|Copyright violations]]',
summary: 'You have been blocked from editing for continued [[WP:COPYVIO|copyright infringement]]'
},
'uw-dblock': {
autoblock: true,
nocreate: true,
reason: 'Persistent removal of content',
pageParam: true,
summary: 'You have been blocked from editing for continued [[WP:VAND|removal of material]]'
},
'uw-disruptblock': {
autoblock: true,
nocreate: true,
reason: '[[WP:Disruptive editing|Disruptive editing]]',
summary: 'You have been blocked from editing for [[WP:DE|disruptive editing]]'
},
'uw-efblock': {
autoblock: true,
nocreate: true,
reason: 'Repeatedly triggering the [[WP:Edit filter|Edit filter]]',
summary: 'You have been blocked from editing for disruptive edits that repeatedly triggered the [[WP:EF|edit filter]]'
},
'uw-ewblock': {
autoblock: true,
expiry: '24 hours',
nocreate: true,
pageParam: true,
reason: '[[WP:Edit warring|Edit warring]]',
summary: 'You have been blocked from editing to prevent further [[WP:DE|disruption]] caused by your engagement in an [[WP:EW|edit war]]'
},
'uw-hblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:No personal attacks|Personal attacks]] or [[WP:Harassment|harassment]]',
summary: 'You have been blocked from editing for attempting to [[WP:HARASS|harass]] other users'
},
'uw-ipevadeblock': {
forAnonOnly: true,
nocreate: true,
reason: '[[WP:Blocking policy#Evasion of blocks|Block evasion]]',
summary: 'Your IP address has been blocked from editing because it has been used to [[WP:EVADE|evade a previous block]]'
},
'uw-lblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
reason: 'Making [[WP:No legal threats|legal threats]]',
summary: 'You have been blocked from editing for making [[WP:NLT|legal threats or taking legal action]]'
},
'uw-nothereblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
reason: 'Clearly [[WP:NOTHERE|not here to build an encyclopedia]]',
forRegisteredOnly: true,
summary: 'You have been indefinitely blocked from editing because it appears that you are not here to [[WP:NOTHERE|build an encyclopedia]]'
},
'uw-npblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: 'Creating [[WP:Patent nonsense|patent nonsense]] or other inappropriate pages',
summary: 'You have been blocked from editing for creating [[WP:PN|nonsense pages]]'
},
'uw-pablock': {
autoblock: true,
expiry: '31 hours',
nocreate: true,
reason: '[[WP:No personal attacks|Personal attacks]] or [[WP:Harassment|harassment]]',
summary: 'You have been blocked from editing for making [[WP:NPA|personal attacks]] toward other users'
},
'uw-sblock': {
autoblock: true,
nocreate: true,
reason: 'Using Wikipedia for [[WP:SPAM|spam]] purposes',
summary: 'You have been blocked from editing for using Wikipedia for [[WP:SPAM|spam]] purposes'
},
'uw-soablock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[WP:Spam|Spam]] / [[WP:NOTADVERTISING|advertising]]-only account',
summary: 'You have been indefinitely blocked from editing because your account is being used only for [[WP:SPAM|spam, advertising, or promotion]]'
},
'uw-socialmediablock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: 'Using Wikipedia as a [[WP:NOTMYSPACE|blog, web host, social networking site or forum]]',
summary: 'You have been blocked from editing for using user and/or article pages as a [[WP:NOTMYSPACE|blog, web host, social networking site or forum]]'
},
'uw-sockblock': {
autoblock: true,
forRegisteredOnly: true,
nocreate: true,
reason: 'Abusing [[WP:Sock puppetry|multiple accounts]]',
summary: 'You have been blocked from editing for abusing [[WP:SOCK|multiple accounts]]'
},
'uw-softerblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-softerblock}} <!-- Promotional username, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] gives the impression that the account represents a group, organization or website'
},
'uw-spamublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-spamublock}} <!-- Promotional username, promotional edits -->',
summary: 'You have been indefinitely blocked from editing because your account is being used only for [[WP:SPAM|spam or advertising]] and your username is a violation of the [[WP:U|username policy]]'
},
'uw-spoablock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '[[WP:SOCK|Sock puppetry]]',
summary: 'This account has been blocked as a [[WP:SOCK|sock puppet]] created to violate Wikipedia policy'
},
'uw-talkrevoked': {
disabletalk: true,
reason: 'Revoking talk page access: inappropriate use of user talk page while blocked',
prependReason: true,
summary: 'Your user talk page access has been disabled',
useInitialOptions: true
},
'uw-ublock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-ublock}} <!-- Username violation, soft block -->',
reasonParam: true,
summary: 'You have been indefinitely blocked from editing because your username is a violation of the [[WP:U|username policy]]'
},
'uw-ublock-double': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-ublock-double}} <!-- Username closely resembles another user, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] is too similar to the username of another Wikipedia user'
},
'uw-ucblock': {
autoblock: true,
expiry: '31 hours',
nocreate: true,
pageParam: true,
reason: 'Persistent addition of [[WP:INTREF|unsourced content]]',
summary: 'You have been blocked from editing for persistent addition of [[WP:INTREF|unsourced content]]'
},
'uw-uhblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-uhblock}} <!-- Username violation, hard block -->',
reasonParam: true,
summary: 'You have been indefinitely blocked from editing because your username is a blatant violation of the [[WP:U|username policy]]'
},
'uw-ublock-wellknown': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-ublock-wellknown}} <!-- Username represents a well-known person, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] matches the name of a well-known living individual'
},
'uw-uhblock-double': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-uhblock-double}} <!-- Attempted impersonation of another user, hard block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] appears to impersonate another established Wikipedia user'
},
'uw-upeblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[WP:PAID|Undisclosed paid editing]] in violation of the WMF [[WP:TOU|Terms of Use]]',
summary: 'You have been indefinitely blocked from editing because your account is being used in violation of [[WP:PAID|Wikipedia policy on undisclosed paid advocacy]]'
},
'uw-vaublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '{{uw-vaublock}} <!-- Username violation, vandalism-only account -->',
summary: 'You have been indefinitely blocked from editing because your account is being [[WP:VOA|used only for vandalism]] and your username is a blatant violation of the [[WP:U|username policy]]'
},
'uw-vblock': {
autoblock: true,
expiry: '31 hours',
nocreate: true,
pageParam: true,
reason: '[[WP:Vandalism|Vandalism]]',
summary: 'You have been blocked from editing to prevent further [[WP:VAND|vandalism]]'
},
'uw-voablock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[WP:Vandalism-only account|Vandalism-only account]]',
summary: 'You have been indefinitely blocked from editing because your account is being [[WP:VOA|used only for vandalism]]'
},
'zombie proxy': {
expiry: '1 month',
forAnonOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{zombie proxy}}',
sig: null
},

// Begin partial block templates, accessed in Twinkle.block.blockGroupsPartial
'uw-acpblock': {
autoblock: true,
expiry: '48 hours',
nocreate: true,
pageParam: false,
reasonParam: true,
reason: 'Misusing [[WP:Sock puppetry|multiple accounts]]',
summary: 'You have been [[WP:PB|blocked from creating accounts]] for misusing [[WP:SOCK|multiple accounts]]'
},
'uw-acpblockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: false,
reasonParam: true,
reason: 'Misusing [[WP:Sock puppetry|multiple accounts]]',
summary: 'You have been indefinitely [[WP:PB|blocked from creating accounts]] for misusing [[WP:SOCK|multiple accounts]]'
},
'uw-aepblock': {
autoblock: true,
nocreate: false,
pageParam: false,
reason: '[[WP:Arbitration enforcement|Arbitration enforcement]]',
reasonParam: true,
summary: 'You have been [[WP:PB|partially blocked]] from editing for violating an [[WP:Arbitration|arbitration decision]]'
},
'uw-epblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
noemail: true,
pageParam: false,
reasonParam: true,
reason: 'Email [[WP:Harassment|harassment]]',
summary: 'You have been [[WP:PB|blocked from emailing]] other editors for [[WP:Harassment|harassment]]'
},
'uw-ewpblock': {
autoblock: true,
expiry: '24 hours',
nocreate: false,
pageParam: false,
reasonParam: true,
reason: '[[WP:Edit warring|Edit warring]]',
summary: 'You have been [[WP:PB|partially blocked]] from editing certain areas of the encyclopedia to prevent further [[WP:DE|disruption]] due to [[WP:EW|edit warring]]'
},
'uw-pblock': {
autoblock: true,
expiry: '24 hours',
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'You have been [[WP:PB|partially blocked]] from certain areas of the encyclopedia'
},
'uw-pblockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'You have been indefinitely [[WP:PB|partially blocked]] from certain areas of the encyclopedia'
}
};
};


//TODO: use blob uri
// Codes and links for Discretionary Sanctions, see [[Template:Ds/topics]]
JWB.setup.download = function() {
// Used for uw-ae(p)block
var name = prompt(JWB.msg('setup-prompt', JWB.msg('setup-prompt-save')), $('#loadSettings').val());
Twinkle.block.dsinfo = {
if (name === null) return;
'': {
if ($.trim(name) === '') name = 'default';
code: ''
JWB.setup.save(name);
},
JWB.status('setup-dload');
'Abortion': {
var url = 'data:application/json;base64,' + btoa(unescape(encodeURIComponent(JWB.setup.getObj())));
code: 'ab',
var elem = $('#download-anchor')[0];
page: 'Wikipedia:Arbitration/Requests/Case/Abortion'
if (HTMLAnchorElement.prototype.hasOwnProperty('download')) { //use download attribute when possible, for its ability to specify a filename
},
elem.href = url;
'American politics post-1992': {
elem.click();
code: 'ap',
setTimeout(function() {elem.removeAttribute('href');}, 2000);
page: 'Wikipedia:Arbitration/Requests/Case/American politics 2'
} else { //fallback to iframes for browsers with no support for download="" attributes
},
elem = $('#download-iframe')[0];
'Ancient Egyptian race controversy': {
elem.src = url.replace('application/json', 'application/octet-stream');
code: 'aerc',
setTimeout(function() {elem.removeAttribute('src');}, 2000);
page: 'Wikipedia:Requests for arbitration/Ancient Egyptian race controversy'
},
'Arab-Israeli conflict': {
code: 'a-i',
page: 'Wikipedia:Arbitration/Index/Palestine-Israel articles'
},
'Armenia, Azerbaijan, or related conflicts': {
code: 'a-a',
page: 'Wikipedia:Requests for arbitration/Armenia-Azerbaijan 2'
},
'Biographies of Living Persons (BLPs)': {
code: 'blp',
page: 'Wikipedia:Requests for arbitration/Editing of Biographies of Living Persons'
},
'Climate change': {
code: 'cc',
page: 'Wikipedia:Arbitration/Requests/Case/Climate change'
},
'Complementary and alternative medicine': {
code: 'com',
page: 'Wikipedia:Arbitration/Requests/Case/Acupuncture'
},
'Eastern Europe or the Balkans': {
code: 'e-e',
page: 'Wikipedia:Requests for arbitration/Eastern Europe'
},
'Electronic cigarettes': {
code: 'ecig',
page: 'Wikipedia:Arbitration/Requests/Case/Editor conduct in e-cigs articles'
},
'Falun Gong': {
code: 'fg',
page: 'Wikipedia:Requests for arbitration/Falun Gong'
},
'Gender-related dispute or controversy and associated people (includes GamerGate)': {
code: 'gas',
page: 'Wikipedia:Arbitration/Requests/Case/Gender and sexuality'
},
'Genetically modified organisms (GMO)': {
code: 'gmo',
page: 'Wikipedia:Arbitration/Requests/Case/Genetically modified organisms'
},
'Gun control': {
code: 'gc',
page: 'Wikipedia:Arbitration/Requests/Case/Gun control'
},
'Horn of Africa (Ethiopia, Somalia, Eritrea, Djibouti)': {
code: 'horn',
page: 'Wikipedia:Arbitration/Requests/Case/Horn of Africa'
},
'India, Pakistan, and Afghanistan': {
code: 'ipa',
page: 'Wikipedia:Requests for arbitration/India-Pakistan'
},
'Infoboxes': {
code: 'cid',
page: 'Wikipedia:Arbitration/Requests/Case/Civility in infobox discussions'
},
'Kurds and Kurdistan': {
code: 'kurd',
page: 'Wikipedia:Arbitration/Requests/Case/Kurds and Kurdistan'
},
'Landmark Worldwide': {
code: 'lw',
page: 'Wikipedia:Arbitration/Requests/Case/Landmark Worldwide'
},
'Liancourt Rocks': {
code: 'lr',
page: 'Wikipedia:Requests for arbitration/Liancourt Rocks'
},
'Manual of Style and article titles': {
code: 'mos',
page: 'Wikipedia:Arbitration/Requests/Case/Article titles and capitalisation'
},
'Muhammad': {
code: 'muh-im',
page: 'Wikipedia:Arbitration/Requests/Case/Muhammad images'
},
'Pharmaceutical drug prices (medicine)': {
code: 'med',
page: 'Wikipedia:Arbitration/Requests/Case/Medicine'
},
'Prem Rawat': {
code: 'pr',
page: 'Wikipedia:Requests for arbitration/Prem Rawat'
},
'Pseudoscience and fringe science': {
code: 'ps',
page: 'Wikipedia:Requests for arbitration/Pseudoscience'
},
'Race/ethnicity and human abilities, behaviour, and intelligence': {
code: 'r-i',
page: 'Wikipedia:Arbitration/Requests/Case/Race and intelligence'
},
'Scientology': {
code: 'sci',
page: 'Wikipedia:Requests for arbitration/Scientology'
},
'Senkaku Islands dispute': {
code: 'sen',
page: 'Wikipedia:Arbitration/Requests/Case/Senkaku Islands'
},
'September 11 attacks': {
code: '9/11',
page: 'Wikipedia:Requests for arbitration/September 11 conspiracy theories'
},
'Shakespeare authorship question': {
code: 'saq',
page: 'Wikipedia:Arbitration/Requests/Case/Shakespeare authorship question'
},
'Transcendental Meditation movement': {
code: 'tm',
page: 'Wikipedia:Arbitration/Requests/Case/Transcendental Meditation movement'
},
'The Troubles': {
code: 'tt',
page: 'Wikipedia:Requests for arbitration/The Troubles'
},
'Waldorf education': {
code: 'we',
page: 'Wikipedia:Requests for arbitration/Waldorf education'
}
}
JWB.status('done', true);
};
};


JWB.setup.import = function(e) {
Twinkle.block.transformBlockPresets = function twinkleblockTransformBlockPresets() {
e.preventDefault();
// supply sensible defaults
file = (e.dataTransfer||this).files[0];
$.each(Twinkle.block.blockPresetsInfo, function(preset, settings) {
if ($(this).is('#import')) { //reset input
settings.summary = settings.summary || settings.reason;
this.outerHTML = this.outerHTML;
settings.sig = settings.sig !== undefined ? settings.sig : 'yes';
$('#import').change(JWB.setup.import);
settings.indefinite = settings.indefinite || Morebits.string.isInfinity(settings.expiry);
}

if (!window.hasOwnProperty('FileReader')) {
if (!Twinkle.block.isRegistered && settings.indefinite) {
alert(JWB.msg('old-browser'));
settings.expiry = '31 hours';
JWB.status('old-browser', '<a target="_blank" href="'+JWB.index_php+'?title=Special:MyPage/'+JWB.settingspage+'">/'+JWB.settingspage+'</a>');
} else {
return;
settings.expiry = settings.expiry || '31 hours';
}
if (file.name.split('.').pop().toLowerCase() !== 'json') {
alert(JWB.msg('not-json'));
return;
}
JWB.status('Processing file');
var reader = new FileReader();
reader.readAsText(file);
reader.onload = function(e) {
JWB.status('done', true);
try {
//Exclusion regex based on http://stackoverflow.com/a/23589204/1256925
//Removes all JS comments from the file, except when they're between quotes.
var c = reader.result;
var data = JSON.parse(c.replace(/("[^"]*")|(\/\*[\w\W]*\*\/|\/\/[^\n]*)/g, function(match, g1, g2) {
if (g1) return g1;
}));
} catch(e) {
alert(JWB.msg('json-err', e.message, JWB.msg('json-err-upload')));
console.log(e); //also log the error for further info
return;
}
}
JWB.setup.extend(data);

};
Twinkle.block.blockPresetsInfo[preset] = settings;
});
JWB.status('Processing file');
};
};


JWB.setup.load = function() {
// These are the groups of presets and defines the order in which they appear. For each list item:
JWB.status('setup-load');
// label: <string, the description that will be visible in the dropdown>
var user = JWB.username||mw.config.get('wgUserName');
// value: <string, the key of a preset in blockPresetsInfo>
var oldtitle = "User:" + user + '/'+JWB.settingspage; // page title for what was used before version 4.0
Twinkle.block.blockGroups = [
var newtitle = "User:" + user + '/JWB-settings.json'; // new page title for all settings pages.
{
var titles = oldtitle;
label: 'Common block reasons',
// if the old title isn't JWB-settings.json, also query the new title.
list: [
if (oldtitle !== newtitle && JWB.hasJSON) {
{ label: 'anonblock', value: 'anonblock' },
titles += '|' + newtitle;
{ label: 'anonblock - likely a school', value: 'anonblock - school' },
{ label: 'school block', value: 'school block' },
{ label: 'Generic block (custom reason)', value: 'uw-block' }, // ends up being default for registered users
{ label: 'Generic block (custom reason) - IP', value: 'uw-ablock', selected: true }, // set only when blocking IP
{ label: 'Generic block (custom reason) - indefinite', value: 'uw-blockindef' },
{ label: 'Disruptive editing', value: 'uw-disruptblock' },
{ label: 'Inappropriate use of user talk page while blocked', value: 'uw-talkrevoked' },
{ label: 'Not here to build an encyclopedia', value: 'uw-nothereblock' },
{ label: 'Unsourced content', value: 'uw-ucblock' },
{ label: 'Vandalism', value: 'uw-vblock' },
{ label: 'Vandalism-only account', value: 'uw-voablock' }
]
},
{
label: 'Extended reasons',
list: [
{ label: 'Advertising', value: 'uw-adblock' },
{ label: 'Arbitration enforcement', value: 'uw-aeblock' },
{ label: 'Block evasion - IP', value: 'uw-ipevadeblock' },
{ label: 'BLP violations', value: 'uw-bioblock' },
{ label: 'Copyright violations', value: 'uw-copyrightblock' },
{ label: 'Creating nonsense pages', value: 'uw-npblock' },
{ label: 'Edit filter-related', value: 'uw-efblock' },
{ label: 'Edit warring', value: 'uw-ewblock' },
{ label: 'Generic block with talk page access revoked', value: 'uw-blocknotalk' },
{ label: 'Harassment', value: 'uw-hblock' },
{ label: 'Legal threats', value: 'uw-lblock' },
{ label: 'Personal attacks or harassment', value: 'uw-pablock' },
{ label: 'Possible compromised account', value: 'uw-compblock' },
{ label: 'Removal of content', value: 'uw-dblock' },
{ label: 'Sock puppetry (master)', value: 'uw-sockblock' },
{ label: 'Sock puppetry (puppet)', value: 'uw-spoablock' },
{ label: 'Social networking', value: 'uw-socialmediablock' },
{ label: 'Spam', value: 'uw-sblock' },
{ label: 'Spam/advertising-only account', value: 'uw-soablock' },
{ label: 'Unapproved bot', value: 'uw-botblock' },
{ label: 'Undisclosed paid editing', value: 'uw-upeblock' },
{ label: 'Violating the three-revert rule', value: 'uw-3block' }
]
},
{
label: 'Username violations',
list: [
{ label: 'Bot username, soft block', value: 'uw-botublock' },
{ label: 'Bot username, hard block', value: 'uw-botuhblock' },
{ label: 'Promotional username, hard block', value: 'uw-spamublock' },
{ label: 'Promotional username, soft block', value: 'uw-softerblock' },
{ label: 'Similar username, soft block', value: 'uw-ublock-double' },
{ label: 'Username violation, soft block', value: 'uw-ublock' },
{ label: 'Username violation, hard block', value: 'uw-uhblock' },
{ label: 'Username impersonation, hard block', value: 'uw-uhblock-double' },
{ label: 'Username represents a well-known person, soft block', value: 'uw-ublock-wellknown' },
{ label: 'Username represents a non-profit, soft block', value: 'uw-causeblock' },
{ label: 'Username violation, vandalism-only account', value: 'uw-vaublock' }
]
},
{
label: 'Templated reasons',
list: [
{ label: 'blocked proxy', value: 'blocked proxy' },
{ label: 'CheckUser block', value: 'CheckUser block' },
{ label: 'checkuserblock-account', value: 'checkuserblock-account' },
{ label: 'checkuserblock-wide', value: 'checkuserblock-wide' },
{ label: 'colocationwebhost', value: 'colocationwebhost' },
{ label: 'oversightblock', value: 'oversightblock' },
{ label: 'rangeblock', value: 'rangeblock' }, // Only for IP ranges, selected for non-/64 ranges in filtered_block_groups
{ label: 'spamblacklistblock', value: 'spamblacklistblock' },
{ label: 'tor', value: 'tor' },
{ label: 'webhostblock', value: 'webhostblock' },
{ label: 'zombie proxy', value: 'zombie proxy' }
]
}
}
JWB.api.call({
];
action: 'query',
titles: titles,
prop: 'info|revisions',
meta: 'tokens',
rvprop: 'content',
indexpageids: true
}, function(response) {
if (JWB === false) return; //user is not allowed to use JWB
var firstrun = !JWB.setup.initialised;
JWB.setup.initialised = true;
var edittoken = response.query.tokens.csrftoken;


// determine correct page to get settings from
Twinkle.block.blockGroupsPartial = [
var pages = response.query.pages,
{
ids = response.query.pageids;
label: 'Common partial block reasons',
var page, exists = true;
list: [
if (ids.length == 2) {
{ label: 'Generic partial block (custom reason)', value: 'uw-pblock', selected: true },
var page0 = pages[ids[0]],
{ label: 'Generic partial block (custom reason) - indefinite', value: 'uw-pblockindef' },
page1 = pages[ids[1]];
{ label: 'Edit warring', value: 'uw-ewpblock' }
var oldpage, newpage;
]
if (page0.title == oldtitle) {
},
oldpage = page0;
{
newpage = page1;
label: 'Extended partial block reasons',
list: [
} else {
oldpage = page1;
{ label: 'Arbitration enforcement', value: 'uw-aepblock' },
newpage = page0;
{ label: 'Email harassment', value: 'uw-epblock' },
{ label: 'Misusing multiple accounts', value: 'uw-acpblock' },
{ label: 'Misusing multiple accounts - indefinite', value: 'uw-acpblockindef' }
]
}
];


Twinkle.block.callback.filtered_block_groups = function twinkleblockCallbackFilteredBlockGroups(group, show_template) {
return $.map(group, function(blockGroup) {
var list = $.map(blockGroup.list, function(blockPreset) {
switch (blockPreset.value) {
case 'uw-talkrevoked':
if (blockedUserName !== relevantUserName) {
return;
}
break;
case 'rangeblock':
if (!Morebits.ip.isRange(relevantUserName)) {
return;
}
blockPreset.selected = !Morebits.ip.get64(relevantUserName);
break;
case 'CheckUser block':
case 'checkuserblock-account':
case 'checkuserblock-wide':
if (!Morebits.userIsInGroup('checkuser')) {
return;
}
break;
case 'oversightblock':
if (!Morebits.userIsInGroup('oversight')) {
return;
}
break;
default:
break;
}
}
if (oldpage.missing === undefined && oldpage.redirect === undefined) {

// old page exists and is not a redirect
var blockSettings = Twinkle.block.blockPresetsInfo[blockPreset.value];
if (newpage.missing === undefined) {
var registrationRestrict = blockSettings.forRegisteredOnly ? Twinkle.block.isRegistered : blockSettings.forAnonOnly ? !Twinkle.block.isRegistered : true;
// both old AND new page exist; throw error and load neither page.
if (!(blockSettings.templateName && show_template) && registrationRestrict) {
let jsredir = "https://www.mediawiki.org/wiki/Help:Redirects#JavaScript_page_redirect";
var templateName = blockSettings.templateName || blockPreset.value;
prompt(JWB.msg('duplicate-settings', oldtitle, newtitle, jsredir), jsredir);
return {
exists = false;
label: (show_template ? '{{' + templateName + '}}: ' : '') + blockPreset.label,
} else {
value: blockPreset.value,
// old page exists but new page doesn't; move the page to the new location.
data: [{
JWB.setup.moveNew(oldtitle, newtitle, edittoken);
name: 'template-name',
JWB.settingspage = 'JWB-settings.json';
value: templateName
}],
return;
}
selected: !!blockPreset.selected,
} else {
disabled: !!blockPreset.disabled
// Old page either doesn't exist or is a redirect. Don't bother with it.
};
page = newpage;
exists = (page.missing === undefined);
JWB.settingspage = 'JWB-settings.json';
}
}
});
} else {
page = pages[ids[0]];
if (list.length) {
exists = (page.missing === undefined);
return {
}
label: blockGroup.label,
if (!exists) {
list: list
// settings page does not exist; don't load anything
};
if (JWB.allowed && firstrun) JWB.setup.save('default'); //this runs when this callback returns after the init has loaded.
return;
}
}
var data = page.revisions[0]['*'].split('{{#JWB-SAFESUBST:#').join('{{subst:');
if (!data) {
// settings page is empty; don't load anything.
if (JWB.allowed && firstrun) JWB.setup.save('default'); //this runs when this callback returns after the init has loaded.
return;
}
try {
data = JSON.parse(data);
} catch(e) {
alert(JWB.msg('json-err', e.message, JWB.msg('json-err-page', JWB.settingspage)) || 'JSON error:\n'+e.message);
JWB.setup.save('default');
return;
}
JWB.setup.extend(data);
JWB.status('done', true);
});
});
};
};


JWB.setup.moveNew = function(from, to, token) {
Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset(e) {
(new mw.Api()).post({
var form = e.target.form, key = form.preset.value;
action: 'move',
if (!key) {
from: from,
return;
to: to,
}
token: token,
reason: JWB.msg(['setup-move-summary', JWB.contentLang]),
noredirect: true, // if possible, suppress redirects; the old page will no longer be needed if the new page exists.
movesubpages: true, // if any
movetalk: true, // if any
ignorewarnings: true,
}).done(function(response) {
if (response.error === undefined) {
JWB.log('move', from, to);
JWB.settingspage = to.split('/')[1];
alert(JWB.msg('moved-settings', from, to, JWB.msg('tab-log')));
JWB.setup.load(); // load settings from newly moved page.
}
});
}


JWB.setup.extend = function(obj) {
Twinkle.block.callback.update_form(e, Twinkle.block.blockPresetsInfo[key]);
$.extend(JWB.settings, obj);
if (form.template) {
if (!JWB.settings.hasOwnProperty('default')) {
form.template.value = Twinkle.block.blockPresetsInfo[key].templateName || key;
JWB.setup.save('default');
Twinkle.block.callback.change_template(e);
} else {
}
for (var i in JWB.settings) {
Morebits.quickForm.setElementVisibility(form.dstopic.parentNode, key === 'uw-aeblock' || key === 'uw-aepblock');
if ($('#loadSettings').find('option[value="'+i+'"]').length) continue;
$('#loadSettings').append('<option value="'+i+'">'+i+'</option>');
}
}
JWB.setup.apply($('#loadSettings').val());
};
};


JWB.setup.del = function() {
Twinkle.block.callback.change_expiry = function twinkleblockCallbackChangeExpiry(e) {
var name = $('#loadSettings').val();
var expiry = e.target.form.expiry;
if (name === '_blank') return alert(JWB.msg('setup-delete-blank'));
if (e.target.value === 'custom') {
var temp = {};
Morebits.quickForm.setElementVisibility(expiry.parentNode, true);
temp[name] = JWB.settings[name];
JWB.setup.temp = $.extend({}, temp);
delete JWB.settings[name];
$('#loadSettings').val('default');
if (name === 'default') {
JWB.setup.apply('_blank');
JWB.setup.save('default');
JWB.status(['del-default', '<a href="javascript:JWB.setup.undelete();">'+JWB.msg('status-del-undo')+'</a>'], true);
} else {
} else {
$('#loadSettings').find('[value="'+name+'"]').remove();
Morebits.quickForm.setElementVisibility(expiry.parentNode, false);
JWB.setup.apply();
expiry.value = e.target.value;
JWB.status(['del-setup', name, '<a href="javascript:JWB.setup.undelete();">'+JWB.msg('status-del-undo')+'</a>'], true);
}
}
};
JWB.setup.undelete = function() {
JWB.setup.extend(JWB.setup.temp);
JWB.status('done', true);
};
};


/***** Main other functions *****/
Twinkle.block.seeAlsos = [];
Twinkle.block.callback.toggle_see_alsos = function twinkleblockCallbackToggleSeeAlso() {
var reason = this.form.reason.value.replace(
new RegExp('( <!--|;) ' + 'see also ' + Twinkle.block.seeAlsos.join(' and ') + '( -->)?'), ''
);


//Show status message status-`action`, or status-`action[0]` with arguments `action[1:]`
Twinkle.block.seeAlsos = Twinkle.block.seeAlsos.filter(function(el) {
JWB.status = function(action, done) {
return el !== this.value;
if (JWB.bot && $('#autosave').prop('checked') && !JWB.isStopped) {
}.bind(this));
$('#summary, .editbutton, #movePage, #deletePage, #protectPage, #skipPage').prop('disabled', true); //Disable summary when auto-saving

} else {
if (this.checked) {
$('#summary, .editbutton, #movePage, #deletePage, #protectPage, #skipPage').prop('disabled', !done); //Disable box when not done (so busy loading). re-enable when done loading.
Twinkle.block.seeAlsos.push(this.value);
}
}
var status;
var seeAlsoMessage = Twinkle.block.seeAlsos.join(' and ');
if (action instanceof Array) {

action[0] = 'status-'+action[0];
if (!Twinkle.block.seeAlsos.length) {
status = JWB.msg.apply(this, action)
this.form.reason.value = reason;
} else if (reason.indexOf('{{') !== -1) {
this.form.reason.value = reason + ' <!-- see also ' + seeAlsoMessage + ' -->';
} else {
} else {
status = JWB.msg('status-'+action);
this.form.reason.value = reason + '; see also ' + seeAlsoMessage;
}
}
if (status === false) return;
};
if (status) {

if (!done) { //spinner if not done
Twinkle.block.dsReason = '';
status += ' <img src="//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif" width="15" height="15" alt="'+JWB.msg('status-alt')+'"/>';
Twinkle.block.callback.toggle_ds_reason = function twinkleblockCallbackToggleDSReason() {
}
var reason = this.form.reason.value.replace(
new RegExp(' ?\\(\\[\\[' + Twinkle.block.dsReason + '\\]\\]\\)'), ''
);

Twinkle.block.dsReason = Twinkle.block.dsinfo[this.options[this.selectedIndex].label].page;
if (!this.value) {
this.form.reason.value = reason;
} else {
} else {
status = action;
this.form.reason.value = reason + ' ([[' + Twinkle.block.dsReason + ']])';
}
}
$('#status').html(status);
JWB.pageCount();
return action=='done';
};
};


JWB.pageCount = function() {
Twinkle.block.callback.update_form = function twinkleblockCallbackUpdateForm(e, data) {
if (JWB.allowed === false||!$('#articleList').length) return;
var form = e.target.form, expiry = data.expiry;
$('#articleList').val(($('#articleList').val()||'').replace(/(^[ \t]*$\n)*/gm, ''));
JWB.list = $('#articleList').val().split('\n');
var count = JWB.list.length;
if (count === 1 && JWB.list[0] === '') count = 0;
$('#totPages').html(count);
};


//Perform all specified find&replace actions
// don't override original expiry if useInitialOptions is set
JWB.replace = function(input, callback) {
if (!data.useInitialOptions) {
JWB.status('replacing');
if (Date.parse(expiry)) {
if (!JWB.worker.isWorking() && JWB.worker.supported) {
expiry = new Date(expiry).toGMTString();
// if the worker is not already working, then re-init to make sure we've not got any broken leftovers from the previous page
form.expiry_preset.value = 'custom';
JWB.worker.init();
} else {
}
form.expiry_preset.value = data.expiry || 'custom';
JWB.newContent = input;
JWB.pageCount();
var varOffset = JWB.list[0].indexOf('|') !== -1 ? JWB.list[0].indexOf('|') + 1 : 0;
JWB.page.pagevar = JWB.list[0].substr(varOffset);
$('.replaces').each(function() {
var $this = $(this);
var replaceText = $this.find('.replaceText').val(),
replaceWith = $this.find('.replaceWith').val();
if (replaceText.length == 0 && replaceWith.length == 0) return; // don't bother replacing 2 empty strings.
var regexFlags = $this.find('.regexFlags').val();
var replace = replaceText.replace(/\$x/gi, JWB.page.pagevar).replace(/\\{2}/g, '\\').replace(/\\n/g,'\n') || '$';
var useRegex = replaceText.length == 0 || $this.find('.useRegex').prop('checked');
if (useRegex && regexFlags.indexOf('_') !== -1) {
replace = replace.replace(/[ _]/g, '[ _]'); //replaces any of [Space OR underscore] with a match for spaces or underscores.
replace = replace.replace(/(\[[^\]]*)\[ _\]/g, '$1 _'); //in case a [ _] was placed inside another [] match, remove the [].
regexFlags = regexFlags.replace('_', '');
}
}
//apply replaces where \n and \\ work in both regular text and regex mode.

var rWith = replaceWith.replace(/\$x/gi, JWB.page.pagevar).replace(/\\{2}/g, '\\').replace(/\\n/g,'\n');
form.expiry.value = expiry;
if (form.expiry_preset.value === 'custom') {
if (rWith.length === 0 && replace === '$') return;
try {
Morebits.quickForm.setElementVisibility(form.expiry.parentNode, true);
let replaceDone = function(result, err) {
} else {
console.log('done replacing', result, err);
Morebits.quickForm.setElementVisibility(form.expiry.parentNode, false);
if (err === undefined) {
JWB.newContent = result;
if (JWB.worker.queue.length == 0 && JWB.worker.supported) {
// all workers are done
JWB.status('done', true);
callback(JWB.newContent);
}
} else if (err == 'Timeout exceeded') {
if (JWB.worker.queue.length == 0 && JWB.worker.supported) {
// all workers have exceeded their time and/or have finished
JWB.status('done', true);
callback(JWB.newContent); // newContent remains unmodified due to timeout.
}
}
}
if ($this.find('.ignoreNowiki').prop('checked')) {
if (!useRegex) {
replace = replace.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
regexFlags = 'g';
}
JWB.worker.unparsedReplace("~"+"~~JWB.newContent", replace, regexFlags, rWith, replaceDone);
} else if (useRegex) {
JWB.worker.replace("~"+"~~JWB.newContent", replace, regexFlags, rWith, replaceDone);
} else {
JWB.newContent = JWB.newContent.split(replace).join(rWith); //global replacement without having to escape all special chars.
}
} catch(e) {
console.log('Regex error:', e)
JWB.stop();
return JWB.status('regex-err', false);
}
}
});
if ($('#enableRETF').prop('checked')) {
JWB.newContent = RETF.replace(JWB.newContent);
}
}
if (!JWB.worker.isWorking()) {

// no workers were called
// boolean-flipped options, more at [[mw:API:Block]]
JWB.status('done', true);
data.disabletalk = data.disabletalk !== undefined ? data.disabletalk : false;
callback(JWB.newContent);
data.hardblock = data.hardblock !== undefined ? data.hardblock : false;

// disable autoblock if blocking a bot
if (Twinkle.block.userIsBot || /bot\b/i.test(relevantUserName)) {
data.autoblock = false;
}
}
};


JWB.skipRETF = function() {
$(form).find('[name=field_block_options]').find(':checkbox').each(function(i, el) {
if (!$('#enableRETF').prop('checked')) return; // RETF is not enabled to begin with
// don't override original options if useInitialOptions is set
if (JWB.isStopped === true) return; // don't mess with the edit box when stopped
if (data.useInitialOptions && data[el.name] === undefined) {
$('#enableRETF').prop('checked', false);
return;
JWB.replace(JWB.page.content, function(newContent) {
}
JWB.editPage(newContent);

JWB.updateButtons();
var check = data[el.name] === '' || !!data[el.name];
$(el).prop('checked', check);
$('#enableRETF').prop('checked', true);
});
});
}


// Edit the current page and pre-fill the newContent.
if (data.prependReason && data.reason) {
JWB.editPage = function(newContent) {
form.reason.value = data.reason + '; ' + form.reason.value;
$('#editBoxArea').val(newContent);
$('#currentpage').html(JWB.msg('editbox-currentpage', JWB.page.name, encodeURIComponent(JWB.page.name)));
if ($('#preparse').prop('checked')) {
$('#articleList').val($.trim($('#articleList').val()) + '\n' + JWB.list[0]); //move current page to the bottom
JWB.next();
return;
} else if (JWB.bot && $('#autosave').prop('checked')) {
JWB.api.diff(function() {
//timeout will take #throttle's value * 1000, if it's a number above 0. Currently defaults to 0.
setTimeout(JWB.api.submit, Math.max(+$('#throttle').val() || 0, 0) * 1000, JWB.page.name);
});
} else {
} else {
JWB.api.diff();
form.reason.value = data.reason || '';
}
}
}


//Adds a line to the logs tab.
// Clear and/or set any partial page or namespace restrictions
JWB.log = function(action, page, info) {
if (form.pagerestrictions) {
var d = new Date();
var $pageSelect = $(form).find('[name=pagerestrictions]');
var pagee = encodeURIComponent(page);
var $namespaceSelect = $(form).find('[name=namespacerestrictions]');
var extraInfo = '', actionStat = '';

switch (action) {
// Respect useInitialOptions by clearing data when switching presets
case 'edit':
// In practice, this will always clear, since no partial presets use it
if (!data.useInitialOptions) {
if (typeof info === 'undefined') {
action = 'null-edit';
$pageSelect.val(null).trigger('change');
actionStat = 'nullEdits';
$namespaceSelect.val(null).trigger('change');
}
} else {
extraInfo = ' (<a target="_blank" href="'+JWB.index_php+'?title='+pagee+'&diff='+info+'">diff</a>)';

actionStat = 'pagesSaved';
// Add any preset options; in practice, just used for prior block settings
if (data.restrictions) {
if (data.restrictions.pages && !$pageSelect.val().length) {
var pages = data.restrictions.pages.map(function(pr) {
return pr.title;
});
// since page restrictions use an ajax source, we
// short-circuit that and just add a new option
pages.forEach(function(page) {
if (!$pageSelect.find("option[value='" + $.escapeSelector(page) + "']").length) {
var newOption = new Option(page, page, true, true);
$pageSelect.append(newOption);
}
});
$pageSelect.val($pageSelect.val().concat(pages)).trigger('change');
}
}
break;
if (data.restrictions.namespaces) {
case 'nobots':
$namespaceSelect.val($namespaceSelect.val().concat(data.restrictions.namespaces)).trigger('change');
action = 'bot-skip';
}
extraInfo = ' (<a target="_blank" href="https://en.wikipedia.org/wiki/Template:Bots">{{bots}}</a>)';
}
// no break;
case 'skip':
actionStat = 'pagesSkipped';
break;
case 'move':
extraInfo = ' to <a target="_blank" href="/wiki/'+encodeURIComponent(info)+'" title="'+info+'">'+info+'</a>';
break;
case 'protect':
extraInfo = info;
break;
}
}
actionStat = '#' + (actionStat || 'otherActions');
$(actionStat).html(+$(actionStat).html() + 1);
$('#actionlog tbody')
.append('<tr>'+
'<td>'+(JWB.fn.pad0(d.getHours())+':'+JWB.fn.pad0(d.getMinutes())+':'+JWB.fn.pad0(d.getSeconds()))+'</td>'+
'<th>'+action+'</th>'+
'<td><a target="_blank" href="/wiki/'+pagee+'" title="'+page+'">'+page+'</a>'+ extraInfo +'</td>'+
'</tr>')
.parents('.JWBtabc').scrollTop($('#actionlog tbody').parents('.JWBtabc')[0].scrollHeight);
};
};


//Move to the next page in the list
Twinkle.block.callback.change_template = function twinkleblockcallbackChangeTemplate(e) {
JWB.next = function(nextPage) {
var form = e.target.form, value = form.template.value, settings = Twinkle.block.blockPresetsInfo[value];
// cancel any still ongoing regex match/replace functions, since we're moving on to another page.
JWB.worker.cancelAll();
if ($.trim(nextPage) && !$('#skipAfterAction').prop('checked')) {
nextPage = $.trim(nextPage) + '\n';
} else {
nextPage = '';
}
$('#articleList').val($('#articleList').val().replace(/^.*\n?/, nextPage));
JWB.list.splice(0,1);
JWB.pageCount();
JWB.api.get(JWB.list[0].split('|')[0]);
};


//Stop everything, reset inputs and editor
var blockBox = $(form).find('[name=actiontype][value=block]').is(':checked');
JWB.stop = function() {
var partialBox = $(form).find('[name=actiontype][value=partial]').is(':checked');
console.trace('stopped');
var templateBox = $(form).find('[name=actiontype][value=template]').is(':checked');
$('#stopbutton,'+
'.editbutton,'+
'#watchNow,'+
'.JWBtabc[data-tab="2"] .editbutton,'+
'#watchNow'+
'.JWBtabc[data-tab="4"] button,'+
'#skipRETF').prop('disabled', true);
$('#startbutton, #articleList,'+
'.JWBtabc[data-tab="1"] button,'+
'#replacesPopup button,'+
'#replacesPopup input,'+
'.JWBtabc input, select').prop('disabled', false);
$('#resultWindow').html('');
$('#editBoxArea').val('');
$('#currentpage').html(JWB.msg('editbox-currentpage', ' ', ' '));
JWB.pl.done = true;
JWB.pl.stop();
JWB.status('done', true);
JWB.isStopped = true;
};


//Start AutoWikiBrowsing
// Block form is not present
JWB.start = function() {
if (!blockBox) {
JWB.pageCount();
if (settings.indefinite || settings.nonstandard) {
if (JWB.list.length === 0 || (JWB.list.length === 1 && !JWB.list[0])) {
if (Twinkle.block.prev_template_expiry === null) {
alert(JWB.msg('no-pages-listed'));
Twinkle.block.prev_template_expiry = form.template_expiry.value || '';
} else if ($('#skipNoChange').prop('checked') && !$('.replaceText').val() && !$('.replaceWith').val() && !$('#enableRETF').prop('checked')) {
alert(JWB.msg('infinite-skip-notice'));
} else {
JWB.isStopped = false;
if ($('#preparse').prop('checked')) {
if (!$('#articleList').val().match('#PRE-PARSE-STOP')) {
$('#articleList').val($.trim($('#articleList').val()) + '\n#PRE-PARSE-STOP'); //mark where to stop pre-parsing
}
}
} else {
form.template_expiry.parentNode.style.display = 'none';
$('#preparse-reset').click();
form.template_expiry.value = 'infinity';
} else if (form.template_expiry.parentNode.style.display === 'none') {
if (Twinkle.block.prev_template_expiry !== null) {
form.template_expiry.value = Twinkle.block.prev_template_expiry;
Twinkle.block.prev_template_expiry = null;
}
form.template_expiry.parentNode.style.display = 'block';
}
}
$('#stopbutton, .editbutton, #watchNow, .JWBtabc[data-tab="2"] button, .JWBtabc[data-tab="4"] button, #skipRETF').prop('disabled', false);
if (Twinkle.block.prev_template_expiry) {
$('#startbutton, #articleList, .JWBtabc[data-tab="1"] button, #replacesPopup button, #replacesPopup input, .JWBtabc input, select').prop('disabled', true);
form.expiry.value = Twinkle.block.prev_template_expiry;
if (!JWB.bot || !$('#autosave').prop('checked')) {
// keep summary / watchlist options enabled when not in autosave mode
$('#minorEdit, #summary, #viaJWB, #watchPage').prop('disabled', false);
}
}
JWB.api.get(JWB.list[0].split('|')[0]);
Morebits.quickForm.setElementVisibility(form.notalk.parentNode, !settings.nonstandard);
// Partial
Morebits.quickForm.setElementVisibility(form.noemail_template.parentNode, partialBox);
Morebits.quickForm.setElementVisibility(form.nocreate_template.parentNode, partialBox);
} else if (templateBox) { // Only present if block && template forms both visible
Morebits.quickForm.setElementVisibility(
form.blank_duration.parentNode,
!settings.indefinite && !settings.nonstandard
);
}
}

Morebits.quickForm.setElementVisibility(form.dstopic.parentNode, value === 'uw-aeblock' || value === 'uw-aepblock');

// Only particularly relevant if template form is present
Morebits.quickForm.setElementVisibility(form.article.parentNode, settings && !!settings.pageParam);
Morebits.quickForm.setElementVisibility(form.block_reason.parentNode, settings && !!settings.reasonParam);

// Partial block
Morebits.quickForm.setElementVisibility(form.area.parentNode, partialBox && !blockBox);

form.root.previewer.closePreview();
};
};
Twinkle.block.prev_template_expiry = null;


Twinkle.block.callback.preview = function twinkleblockcallbackPreview(form) {
JWB.updateButtons = function() {
if (!JWB.page.exists && $('#deletePage').is('.delete')) {
var params = {
$('#deletePage').removeClass('delete').addClass('undelete').html('Undelete');
article: form.article.value,
JWB.fn.blink('#deletePage'); //Indicate the button has changed
blank_duration: form.blank_duration ? form.blank_duration.checked : false,
} else if (JWB.page.exists && $('#deletePage').is('.undelete')) {
disabletalk: form.disabletalk.checked || (form.notalk ? form.notalk.checked : false),
$('#deletePage').removeClass('undelete').addClass('delete').html('Delete');
expiry: form.template_expiry ? form.template_expiry.value : form.expiry.value,
JWB.fn.blink('#deletePage'); //Indicate the button has changed
hardblock: Twinkle.block.isRegistered ? form.autoblock.checked : form.hardblock.checked,
}
indefinite: Morebits.string.isInfinity(form.template_expiry ? form.template_expiry.value : form.expiry.value),
if (!JWB.page.exists) {
reason: form.block_reason.value,
$('#movePage').prop('disabled', true);
template: form.template.value,
} else {
dstopic: form.dstopic.value,
$('#movePage').prop('disabled', false);
partial: $(form).find('[name=actiontype][value=partial]').is(':checked'),
}
pagerestrictions: $(form.pagerestrictions).val() || [],
$('#watchNow').html( JWB.msg('watch-' + (JWB.page.watched ? 'remove' : 'add')) );
namespacerestrictions: $(form.namespacerestrictions).val() || [],
noemail: form.noemail.checked || (form.noemail_template ? form.noemail_template.checked : false),
nocreate: form.nocreate.checked || (form.nocreate_template ? form.nocreate_template.checked : false),
area: form.area.value
};

var templateText = Twinkle.block.callback.getBlockNoticeWikitext(params);

form.previewer.beginRender(templateText, 'User_talk:' + relevantUserName); // Force wikitext/correct username
};
};


/***** Web Worker functions *****/
Twinkle.block.callback.evaluate = function twinkleblockCallbackEvaluate(e) {
JWB.worker.supported = !!window.Worker; // if window.Worker exists, we can use workers. Unless CSP blocks us.
var $form = $(e.target),
JWB.worker.queue = [];
toBlock = $form.find('[name=actiontype][value=block]').is(':checked'),
toWarn = $form.find('[name=actiontype][value=template]').is(':checked'),
toPartial = $form.find('[name=actiontype][value=partial]').is(':checked'),
blockoptions = {}, templateoptions = {};


// Load function required to properly load the worker, since directly using `new Worker(url)` for cross-origin URLs does not work even with CORS/CSP rules all allowing it.
Twinkle.block.callback.saveFieldset($form.find('[name=field_block_options]'));
// See https://stackoverflow.com/q/66188950/1256925 for this exact question
Twinkle.block.callback.saveFieldset($form.find('[name=field_template_options]'));
JWB.worker.load = function(callback) {
if (JWB.worker.blob) return callback(); // already successfully built
$.getScript(JWB.imports['worker.js'], function() {
// Firefox does not understand try..catch for content security policy violations, so define the worker functions regardless of the blob support.
JWB.worker.functions = JWB.worker.function();
// the loaded script just defined JWB.worker.function; convert it to a blob url
// Based on https://stackoverflow.com/a/33432215/1256925
if (JWB.worker.supported) try {
let blob = new Blob(['('+JWB.worker.function.toString()+')()'], {type: 'text/javascript'});
JWB.worker.blob = URL.createObjectURL(blob);
callback();
} catch(e) {
if (e.code == 18) {
JWB.worker.supported = false;
}
}
});
}


// Create a worker to be able to preform regex operations without hanging the current process.
blockoptions = Twinkle.block.field_block_options;
// Based on https://stackoverflow.com/q/66153487/1256925

JWB.worker.init = function() {
templateoptions = Twinkle.block.field_template_options;
JWB.worker.load(function() {

JWB.worker.worker = new Worker(JWB.worker.blob);
templateoptions.disabletalk = !!(templateoptions.disabletalk || blockoptions.disabletalk);
JWB.worker.callback = undefined; // explicitly set to the implicit value of undefined.
templateoptions.hardblock = !!blockoptions.hardblock;
JWB.worker.timeout = 0;

JWB.worker.queue = [];
delete blockoptions.expiry_preset; // remove extraneous
JWB.worker.worker.onmessage = function(e) {

clearTimeout(JWB.worker.timeout);
// Partial API requires this to be gone, not false or 0
JWB.worker.timeout = 0;
if (toPartial) {
if (JWB.isStopped) {
blockoptions.partial = templateoptions.partial = true;
// we're stopped; clear the queue and stop.
}
JWB.worker.queue = [];
templateoptions.pagerestrictions = $form.find('[name=pagerestrictions]').val() || [];
} else if (JWB.worker.callback !== undefined) {
templateoptions.namespacerestrictions = $form.find('[name=namespacerestrictions]').val() || [];
JWB.worker.callback(e.data.result, e.data.err);
// Format for API here rather than in saveFieldset
} else {
blockoptions.pagerestrictions = templateoptions.pagerestrictions.join('|');
console.error("Worker finished without callback set:", e.data, e);
blockoptions.namespacerestrictions = templateoptions.namespacerestrictions.join('|');

// use block settings as warn options where not supplied
templateoptions.summary = templateoptions.summary || blockoptions.reason;
templateoptions.expiry = templateoptions.template_expiry || blockoptions.expiry;

if (toBlock) {
if (blockoptions.partial) {
if (blockoptions.disabletalk && blockoptions.namespacerestrictions.indexOf('3') === -1) {
return alert('Partial blocks cannot prevent talk page access unless also restricting them from editing User talk space!');
}
if (!blockoptions.namespacerestrictions && !blockoptions.pagerestrictions) {
if (!blockoptions.noemail && !blockoptions.nocreate) { // Blank entries technically allowed [[phab:T208645]]
return alert('No pages or namespaces were selected, nor were email or account creation restrictions applied; please select at least one option to apply a partial block!');
} else if ((templateoptions.template !== 'uw-epblock' || $form.find('select[name="preset"]').val() !== 'uw-epblock') &&
// Don't require confirmation if email harassment defaults are set
!confirm('You are about to block with no restrictions on page or namespace editing, are you sure you want to proceed?')) {
return;
}
}
}
JWB.worker.next(true);
}
}
});
if (!blockoptions.expiry) {
};
return alert('Please provide an expiry!');
} else if (Morebits.string.isInfinity(blockoptions.expiry) && !Twinkle.block.isRegistered) {
return alert("Can't indefinitely block an IP address!");
}
if (!blockoptions.reason) {
return alert('Please provide a reason for the block!');
}


// Boolean; check if the worker is currently occupied.
Morebits.simpleWindow.setButtonsEnabled(false);
JWB.worker.isWorking = function() {
Morebits.status.init(e.target);
return JWB.worker.callback !== undefined;
var statusElement = new Morebits.status('Executing block');
};
blockoptions.action = 'block';


// Cancel current worker's task (e.g. due to timeout)
blockoptions.user = relevantUserName;
JWB.worker.terminate = function() {
console.log('terminating');
let w = JWB.worker;
w.worker.terminate();
w.callback(undefined, 'Timeout exceeded');
let queue = w.queue; // save old queue
w.init(); // re-init this worker, since the previous one is presumed dead (and terminated).
w.queue = queue; // restore queue
};


// Cancel all workers (e.g. due to no longer needing the worker's queued services)
// boolean-flipped options
JWB.worker.cancelAll = function() {
blockoptions.anononly = blockoptions.hardblock ? undefined : true;
JWB.worker.queue = [];
blockoptions.allowusertalk = blockoptions.disabletalk ? undefined : true;
if (JWB.worker.worker) JWB.worker.worker.terminate(); // do not call the callback.
}


// Set worker to work, or queue the worker task.
/*
JWB.worker.do = function(msg, callback) {
Check if block status changed while processing the form.
if (JWB.worker.isWorking()) {

JWB.worker.queue.push({msg: msg, callback: callback});
There's a lot to consider here. list=blocks provides the
} else {
current block status, but there are at least two issues with
var timelimit = parseInt($('#timelimit').val()) || 3000;
relying on it. First, the id doesn't update on a reblock,
JWB.worker.callback = callback;
meaning the individual parameters need to be compared. This
// Expand "JWB.string" into JWB['string']; to allow the string to be loaded at execution time instead of queue time.
can be done roughly with JSON.stringify - we can thankfully
// Start with 3x ~ because that cannot exist as the start of an actual page
rely on order from the server, although sorting would be
if (msg.str && msg.str.indexOf('~'+'~~JWB.') === 0) msg.str = JWB[msg.str.substr(7)]; // For now, 1-deep expansion is sufficient.
fine if not - but falsey values are problematic and is
JWB.worker.worker.postMessage(msg);
non-ideal. More importantly, list=blocks won't indicate if a
JWB.worker.timeout = setTimeout(function() {
non-blocked user is blocked then unblocked. This should be
if (!JWB.worker.isWorking()) {
exceedingy rare, but regardless, we thus need to check
console.error('Worker error');
list=logevents, which has a nicely updating logid
JWB.worker.next(true);
parameter. We can't rely just on that, though, since it
return;
doesn't account for blocks that have expired on their own.

As such, we use both. Using some ternaries, the logid
variables are false if there's no logevents, so if they
aren't equal we defintely have a changed entry (send
confirmation). If they are equal, then either the user was
never blocked (the block statuses will be equal, no
confirmation) or there's no new block, in which case either
a block expired (different statuses, confirmation) or the
same block is still active (same status, no confirmation).
*/
var query = {
format: 'json',
action: 'query',
list: 'blocks|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + blockoptions.user
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(blockoptions.user, true)) {
query.bkip = blockoptions.user;
} else {
query.bkusers = blockoptions.user;
}
api.get(query).then(function(data) {
var block = data.query.blocks[0];
// As with the initial data fetch, if an IP is blocked
// *and* rangeblocked, this would only grab whichever
// block is more recent, which would likely mean a
// mismatch. However, if the rangeblock is updated
// while filling out the form, this won't detect that,
// but that's probably fine.
if (data.query.blocks.length > 1 && block.user !== relevantUserName) {
block = data.query.blocks[1];
}
}
JWB.worker.terminate();
var logevents = data.query.logevents[0];
JWB.worker.next(true);
var logid = data.query.logevents.length ? logevents.logid : false;
}, timelimit);
}
};


// Execute the next task in the queue
if (logid !== Twinkle.block.blockLogId || !!block !== !!Twinkle.block.currentBlockInfo) {
JWB.worker.next = function(force = false) {
var message = 'The block status of ' + blockoptions.user + ' has changed. ';
if (block) {
if (force) {
// force means the function that's calling next() has handled the previous worker task. Clean up after it.
message += 'New status: ';
JWB.worker.callback = undefined;
} else {
} else if (JWB.worker.isWorking()) {
message += 'Last entry: ';
// still working and the calling function did not specify proper exit of the previous task yet.
}
return false;
}
if (JWB.worker.queue.length === 0) return true;
var q = JWB.worker.queue.shift();
JWB.worker.do(q.msg, q.callback);
};


/***** Functions using workers *****/
var logExpiry = '';
JWB.worker.match = function(str, pattern, flags, callback) {
if (logevents.params.duration) {
if (JWB.worker.supported) {
if (logevents.params.duration === 'infinity') {
JWB.worker.do({cmd: 'match', str, pattern, flags}, callback);
logExpiry = 'indefinitely';
} else {
} else {
if (str && str.indexOf('~'+'~~JWB.') === 0) str = JWB[str.substr(7)]; // For now, 1-deep expansion is sufficient.
var expiryDate = new Morebits.date(logevents.params.expiry);
JWB.worker.functions.match(str, pattern, flags, callback);
logExpiry += (expiryDate.isBefore(new Date()) ? ', expired ' : ' until ') + expiryDate.calendar();
}
}
};
} else { // no duration, action=unblock, just show timestamp
logExpiry = ' ' + new Morebits.date(logevents.timestamp).calendar();
}
message += Morebits.string.toUpperCaseFirstChar(logevents.action) + 'ed by ' + logevents.user + logExpiry +
' for "' + logevents.comment + '". Do you want to override with your settings?';


JWB.worker.replace = function(str, pattern, flags, rWith, callback) {
if (!confirm(message)) {
if (JWB.worker.supported) {
Morebits.status.info('Executing block', 'Canceled by user');
JWB.worker.do({cmd: 'replace', str, pattern, flags, rWith}, callback);
return;
} else {
}
if (str && str.indexOf('~'+'~~JWB.') === 0) str = JWB[str.substr(7)]; // For now, 1-deep expansion is sufficient.
blockoptions.reblock = 1; // Writing over a block will fail otherwise
JWB.worker.functions.replace(str, pattern, flags, rWith, callback);
}
}
};


JWB.worker.unparsedReplace = function(str, pattern, flags, rWith, callback) {
// execute block
if (JWB.worker.supported) {
blockoptions.tags = Twinkle.changeTags;
JWB.worker.do({cmd: 'unparsedreplace', str, pattern, flags, rWith}, callback);
blockoptions.token = mw.user.tokens.get('csrfToken');
var mbApi = new Morebits.wiki.api('Executing block', blockoptions, function() {
statusElement.info('Completed');
if (toWarn) {
Twinkle.block.callback.issue_template(templateoptions);
}
});
mbApi.post();
});
} else if (toWarn) {
Morebits.simpleWindow.setButtonsEnabled(false);

Morebits.status.init(e.target);
Twinkle.block.callback.issue_template(templateoptions);
} else {
} else {
if (str && str.indexOf('~'+'~~JWB.') === 0) str = JWB[str.substr(7)]; // For now, 1-deep expansion is sufficient.
return alert('Please give Twinkle something to do!');
JWB.worker.functions.unparsedreplace(str, pattern, flags, rWith, callback);
}
}
};
};


/***** General functions *****/
Twinkle.block.callback.issue_template = function twinkleblockCallbackIssueTemplate(formData) {
//Clear all existing timers to prevent them from getting errors
// Use wgRelevantUserName to ensure the block template goes to a single IP and not to the
JWB.fn.clearAllTimeouts = function() {
// "talk page" of an IP range (which does not exist)
var i = setTimeout(function() {
var userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
return void(0);

}, 1000);
var params = $.extend(formData, {
for (var n=0;n<=i;n++) {
messageData: Twinkle.block.blockPresetsInfo[formData.template],
clearTimeout(n);
reason: Twinkle.block.field_template_options.block_reason,
clearInterval(n);
disabletalk: Twinkle.block.field_template_options.notalk,
}
noemail: Twinkle.block.field_template_options.noemail_template,
console.log('Cleared all running intervals up to index',i);
nocreate: Twinkle.block.field_template_options.nocreate_template
});

Morebits.wiki.actionCompleted.redirect = userTalkPage;
Morebits.wiki.actionCompleted.notice = 'Actions complete, loading user talk page in a few seconds';

var wikipedia_page = new Morebits.wiki.page(userTalkPage, 'User talk page modification');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.block.callback.main);
};
};


//Filter an array to only contain unique values.
Twinkle.block.callback.getBlockNoticeWikitext = function(params) {
JWB.fn.uniques = function(arr) {
var text = '{{', settings = Twinkle.block.blockPresetsInfo[params.template];
var a = [];
if (!settings.nonstandard) {
for (var i=0, l=arr.length; i<l; i++) {
text += 'subst:' + params.template;
if (params.article && settings.pageParam) {
if (a.indexOf(arr[i]) === -1 && arr[i] !== '') {
a.push(arr[i]);
text += '|page=' + params.article;
}
if (params.dstopic) {
text += '|topic=' + params.dstopic;
}
}
}
return a;
};


// code taken directly from [[Template:Bots]] and changed structurally (not functionally) for readability. The user in this case is "JWB" to deny this script.
if (!/te?mp|^\s*$|min/.exec(params.expiry)) {
// the user parameter is still kept as an optional parameter to maintain functionality as given on that template page.
if (params.indefinite) {
JWB.fn.allowBots = function(text, user = "JWB") {
text += '|indef=yes';
var usr = user.replace(/([\(\)\*\+\?\.\-\:\!\=\/\^\$])/g, "\\$1");
} else if (!params.blank_duration && !new Morebits.date(params.expiry).isValid()) {
if (!new RegExp("\\{\\{\\s*(nobots|bots[^}]*)\\s*\\}\\}", "i").test(text))
// Block template wants a duration, not date
return true;
text += '|time=' + params.expiry;
if (new RegExp("\\{\\{\\s*bots\\s*\\|\\s*deny\\s*=\\s*([^}]*,\\s*)*" + usr + "\\s*(?=[,\\}])[^}]*\\s*\\}\\}", "i").test(text))
}
return false
}
else
return new RegExp("\\{\\{\\s*((?!nobots)|bots(\\s*\\|\\s*allow\\s*=\\s*((?!none)|([^}]*,\\s*)*" + usr +
"\\s*(?=[,\\}])[^}]*|all))?|bots\\s*\\|\\s*deny\\s*=\\s*(?!all)[^}]*|bots\\s*\\|\\s*optout=(?!all)[^}]*)\\s*\\}\\}", "i").test(text);
}


if (!Twinkle.block.isRegistered && !params.hardblock) {
text += '|anon=yes';
}


//Prepends zeroes until the number has the desired length of len (default 2)
if (params.reason) {
JWB.fn.pad0 = function(n, len = 2) {
text += '|reason=' + params.reason;
n = n.toString();
}
return n.length < len ? Array(len-n.length+1).join('0')+n : n;
if (params.disabletalk) {
};
text += '|notalk=yes';
}


JWB.fn.blink = function(el,t) {
// Currently, all partial block templates are "standard"
t=t?t:500;
// Building the template, however, takes a fair bit of logic
$(el).prop('disabled', true)
if (params.partial) {
.children().animate({opacity:'0.1'},t-100)
if (params.pagerestrictions.length || params.namespacerestrictions.length) {
.animate({opacity:'1'},t)
var makeSentence = function (array) {
.animate({opacity:'0.1'},t-100)
if (array.length < 3) {
.animate({opacity:'1'},t);
return array.join(' and ');
setTimeout("$('"+el+"').prop('disabled', false)",t*4-400);
}
};
var last = array.pop();
return array.join(', ') + ', and ' + last;


JWB.fn.setSelection = function(el, start, end, dir) {
};
dir = dir||'none'; //Default value
text += '|area=' + (params.indefinite ? 'certain ' : 'from certain ');
end = end||start; //If no end is specified, assume the caret is placed without creating text selection.
if (params.pagerestrictions.length) {
if (el.setSelectionRange) {
text += 'pages (' + makeSentence(params.pagerestrictions.map(function(p) {
el.focus();
return '[[:' + p + ']]';
el.setSelectionRange(start, end, dir);
}));
} else if (el.createTextRange) {
text += params.namespacerestrictions.length ? ') and certain ' : ')';
var rng = el.createTextRange();
}
rng.collapse(true);
if (params.namespacerestrictions.length) {
rng.moveStart('character', start);
// 1 => Talk, 2 => User, etc.
rng.moveEnd('character', end);
var namespaceNames = params.namespacerestrictions.map(function(id) {
rng.select();
return menuFormattedNamespaces[id];
}
});
};
text += '[[Wikipedia:Namespace|namespaces]] (' + makeSentence(namespaceNames) + ')';

}
JWB.fn.scrollSelection = function(el, index) { //function to fix scrolling to selection - doesn't do that automatically.
} else if (params.area) {
var newEl = document.createElement('textarea'); //create a new textarea to simulate the same conditions
text += '|area=' + params.area;
var elStyle = getComputedStyle(el);
} else {
newEl.style.height = elStyle.height; //copy over size-influencing styles
if (params.noemail) {
newEl.style.width = elStyle.width;
text += '|email=yes';
newEl.style.lineHeight = elStyle.lineHeight;
}
newEl.style.fontSize = elStyle.fontSize;
if (params.nocreate) {
newEl.value = el.value.substr(0,index);
text += '|accountcreate=yes';
document.body.appendChild(newEl); //needs to be added to the HTML for the scrollHeight and clientHeight to work.
}
if (newEl.scrollHeight != newEl.clientHeight) {
}
el.scrollTop = newEl.scrollHeight - 2;
}
} else {
} else {
el.scrollTop = 0;
text += params.template;
}
}
newEl.remove(); //clean up the mess I've made
};


//i18n function
if (settings.sig) {
JWB.msg = function(message) {
text += '|sig=' + settings.sig;
var args = arguments;
var lang = JWB.lang;
if (typeof message === 'object') {
lang = message[1];
message = message[0];
}
}
if (lang == 'qqx') return message;
return text + '}}';
if (!JWB.messages || !JWB.messages.en) return '\u29FC'+message+'\u29FD'; // same surrounding <> as used in mw.msg();
var msg;
if (JWB.messages.hasOwnProperty(lang) && JWB.messages[lang].hasOwnProperty(message)) {
msg = JWB.messages[lang][message];
} else {
msg = (JWB.messages.en.hasOwnProperty(message)) ? JWB.messages.en[message] : '\u29FC'+message+'\u29FD';
}
msg = msg.replace(/\$(\d+)/g, function(match, num) {
return args[+num] || match;
});
return msg;
};
};


/***** Init *****/
Twinkle.block.callback.main = function twinkleblockcallbackMain(pageobj) {
var params = pageobj.getCallbackParameters(),
date = new Morebits.date(pageobj.getLoadTime()),
messageData = params.messageData,
text;


JWB.init = function() {
params.indefinite = Morebits.string.isInfinity(params.expiry);
console.log(JWB.messages.en, !!JWB.messages.en);
JWB.setup.load();
JWB.worker.init();
JWB.fn.clearAllTimeouts();


var findreplace = '<div class="replaces">'+
if (Twinkle.getPref('blankTalkpageOnIndefBlock') && params.template !== 'uw-lblock' && params.indefinite) {
'<label style="display:block;">'+JWB.msg('label-replace')+' <input type="text" class="replaceText"/></label>'+
Morebits.status.info('Info', 'Blanking talk page per preferences and creating a new talk page section for this month');
'<label style="display:block;">'+JWB.msg('label-rwith')+' <input type="text" class="replaceWith"/></label>'+
text = date.monthHeader() + '\n';
'<div class="regexswitch">'+
} else {
'<label><input type="checkbox" class="useRegex"> '+JWB.msg('label-useregex')+'</label>'+
text = pageobj.getPageText();
'<a class="re101" href="http://regex101.com/#javascript" target="_blank">?</a>'+
'<label class="divisor" title="'+JWB.msg('tip-regex-flags')+'" style="display:none;">'+
JWB.msg('label-regex-flags')+' <input type="text" class="regexFlags" value="g"/>'+ //default: global replacement
'</label>'+
'<br/>'+
'</div>'+
'<label title="'+JWB.msg('tip-ignore-comment')+'">'+
'<input type="checkbox" class="ignoreNowiki"> '+JWB.msg('label-ignore-comment')+
'</label>'+
'</div>';
var NSList = '<select multiple name="namespace" id="namespacelist">';
for (var i in JWB.ns) {
if (parseInt(i) < 0) continue; //No Special: or Media: in the list
NSList += '<option value="'+JWB.ns[i].id+'" selected>'+(JWB.ns[i]['*'] || '('+JWB.msg('namespace-main')+')')+'</option>';
}
NSList += '</select>';
/***** Interface *****/
document.title = 'AutoWikiBrowser Script'+(document.title.split('-')[1] ? ' -'+document.title.split('-')[1] : '');
$('body').html(
'<article id="resultWindow"></article>'+
'<main id="inputsWindow">'+
'<div id="inputsBox">'+
'<aside id="articleBox">'+
'<b>'+JWB.msg('pagelist-caption')+'</b>'+
'<textarea id="articleList"></textarea>'+
'</aside>'+
'<section id="tabs">'+
'<nav class="tabholder">'+
'<span class="JWBtab" data-tab="1">'+JWB.msg('tab-setup')+'</span> '+
'<span class="JWBtab active" data-tab="2">'+JWB.msg('tab-editing')+'</span> '+
'<span class="JWBtab" data-tab="3">'+JWB.msg('tab-skip')+'</span> '+
(JWB.sysop?'<span class="JWBtab" data-tab="4">'+JWB.msg('tab-other')+'</span> ':'')+
' <span class="JWBtab log" data-tab="5">'+JWB.msg('tab-log')+'</span> '+
'</nav>'+
'<section class="JWBtabc" data-tab="1"></section>'+
'<section class="JWBtabc active" data-tab="2"></section>'+
'<section class="JWBtabc" data-tab="3"></section>'+
(JWB.sysop?'<section class="JWBtabc" data-tab="4"></section>':'')+
'<section class="JWBtabc log" data-tab="5"></section>'+
'<footer id="status">done</footer>'+
'</section>'+
'<aside id="editBox">'+
'<b>'+JWB.msg('editbox-caption')+' - <span id="currentpage">'+JWB.msg('editbox-currentpage', ' ', ' ')+'</span></b>'+
'<textarea id="editBoxArea"></textarea>'+
'</aside>'+
'</div>'+
'</main>'+
'<footer id="stats">'+
JWB.msg('stat-pages')+' <span id="totPages">0</span>;&emsp;'+
JWB.msg('stat-save')+' <span id="pagesSaved">0</span>;&emsp;'+
JWB.msg('stat-null')+' <span id="nullEdits">0</span>;&emsp;'+
JWB.msg('stat-skip')+' <span id="pagesSkipped">0</span>;&emsp;'+
JWB.msg('stat-other')+' <span id="otherActions">0</span>;&emsp;'+
'</footer>'+
'<div id="overlay" style="display:none;"></div>'+
'<section class="JWBpopup" id="replacesPopup" style="display:none;">'+
'<button id="moreReplaces">'+JWB.msg('button-more-fields')+'</button>'+
'<br>'+findreplace+
'</section>'+
'<section class="JWBpopup" id="pagelistPopup" style="display:none;">'+
'<form action="#" id="pl-form"></form>'+
'</section>'
);
$('.JWBtabc[data-tab="1"]').html(
'<fieldset id="pagelist">'+
'<legend>'+JWB.msg('label-pagelist')+'</legend>'+
'<button id="removeDupes">'+JWB.msg('button-remove-dupes')+'</button> '+
'<button id="sortArticles">'+JWB.msg('button-sort')+'</button>'+
'<br>'+
'<label title="'+JWB.msg('tip-preparse')+'">'+
'<input type="checkbox" id="preparse"> '+JWB.msg('preparse')+
'</label>'+
'<span class="divisor"></span>'+
'<button id="preparse-reset" title="'+JWB.msg('tip-preparse-reset')+'">'+JWB.msg('preparse-reset')+'</button>'+
'<br>'+
'<button id="pagelistButton">'+JWB.msg('pagelist-generate')+'</button>'+
'</fieldset>'+
'<fieldset id="settings">'+
'<legend>'+JWB.msg('label-settings')+'</legend>'+
'<button id="saveAs" title="'+JWB.msg('tip-store-setup')+'">'+JWB.msg('store-setup')+'</button>'+
'<br>'+
'<label>'+
JWB.msg('load-settings') + ' '+
'<select id="loadSettings">'+
'<option value="default" selected>default</option>'+
'<option value="_blank">'+JWB.msg('blank-setup')+'</option>'+
'</select>'+
'</label>'+
'<span class="divisor"></span>'+
'<button id="deleteSetup" title="'+JWB.msg('tip-delete-setup')+'">'+JWB.msg('delete-setup')+'</button>'+
'<hr>'+
'<button id="saveToWiki">'+JWB.msg('save-setup')+'</button>'+
'<span class="divisor"></span>'+
'<button id="download">'+JWB.msg('download-setup')+'</button>'+
'<hr>'+
'<label class="button" id="importLabel" title="'+JWB.msg('tip-import-setup')+'">'+
'<input type="file" id="import" accept=".json">'+
JWB.msg('import-setup')+
'</label>'+
'<span class="divisor"></span>'+
'<button id="updateSetups" title="'+JWB.msg('tip-update-setup', JWB.settingspage)+'">'+JWB.msg('update-setup')+'</button>'+
'<div id="downloads">'+
'<a download="JWB-settings.json" target="_blank" id="download-anchor"></a>'+
'<iframe id="download-iframe"></iframe>'+
'</div>'+
'</fieldset>'+
'<fieldset id="limits">'+
'<legend>'+JWB.msg('label-limits')+'</legend>'+
'<label class="timelimit-label" title="'+JWB.msg('tip-time-limit')+'">'+
JWB.msg('time-limit')+
'<input type="number" id="timelimit" value="3000" min="0">'+
'</label>'+
'<label title="'+JWB.msg('tip-diff-size-limit')+'">'+
JWB.msg('diff-size-limit')+
'<input type="number" id="sizelimit" value="0" min="0">'+
'</label>'+
'</fieldset>'
);
$('.JWBtabc[data-tab="2"]').html(
'<label class="minorEdit"><input type="checkbox" id="minorEdit" checked> '+JWB.msg('minor-edit')+'</label>'+
'<label class="editSummary viaJWB">'+JWB.msg('edit-summary')+'<br/> <input class="fullwidth" type="text" id="summary" maxlength="500"></label>'+
' <input type="checkbox" id="viaJWB" checked title="'+JWB.msg('tip-via-JWB')+'">'+
'<select id="watchPage">'+
'<option value="watch">'+JWB.msg('watch-watch')+'</option>'+
'<option value="unwatch">'+JWB.msg('watch-unwatch')+'</option>'+
'<option value="nochange" selected>'+JWB.msg('watch-nochange')+'</option>'+
'<option value="preferences">'+JWB.msg('watch-preferences')+'</option>'+
'</select>'+
'<span class="divisor"></span>'+
'<button id="watchNow" disabled accesskey="w">'+
JWB.msg('watch-add')+
'</button>'+
'<br>'+
(JWB.bot?
'<label><input type="checkbox" id="autosave"> '+JWB.msg('auto-save')+'</label>'+
'<label title="'+JWB.msg('tip-save-interval')+'" class="divisor">'+
JWB.msg('save-interval', '<input type="number" min="0" value="0" style="width:50px" id="throttle" disabled>')+
'</label>'+
'<br>'
:'')+
'<span id="startstop">'+
'<button id="startbutton" accesskey="a">'+JWB.msg('editbutton-start')+'</button>'+
'<br>'+
'<button id="stopbutton" disabled accesskey="q">'+JWB.msg('editbutton-stop')+'</button> '+
'</span>'+
'<button class="editbutton" id="skipButton" disabled accesskey="n">'+JWB.msg('editbutton-skip')+'</button>'+
'<button class="editbutton" id="submitButton" disabled accesskey="s">'+JWB.msg('editbutton-save')+'</button>'+
'<br>'+
'<button class="editbutton" id="previewButton" disabled accesskey="p">'+JWB.msg('editbutton-preview')+'</button>'+
'<button class="editbutton" id="diffButton" disabled accesskey="d">'+JWB.msg('editbutton-diff')+'</button>'+
'<button id="replacesButton">'+JWB.msg('button-open-popup')+'</button>'+
findreplace+
'<hr>'+
'<label><input type="checkbox" id="enableRETF"> '+
JWB.msg('label-enable-RETF',
'<a href="/wiki/Project:AutoWikiBrowser/Typos" target="_blank">'+
JWB.msg('label-RETF')+
'</a>')+
'</label>'+
' <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Gnome-view-refresh.svg/20px-Gnome-view-refresh.svg.png"'+
'id="refreshRETF" title="'+JWB.msg('tip-refresh-RETF')+'">'+
'<br/>'+
'<button id="skipRETF" title="'+JWB.msg('tip-skip-RETF')+'" disabled>'+JWB.msg('skip-RETF')+'</button>'
);
$('.JWBtabc[data-tab="3"]').html(
'<fieldset>'+
'<legend>'+JWB.msg('label-redirects')+'</legend>'+
'<label title="'+JWB.msg('tip-redirects-follow')+'">'+
'<input type="radio" class="redirects" value="follow" name="redir" id="redir-follow"> '+JWB.msg('redirects-follow')+' '+
'</label>'+
'<label title="'+JWB.msg('tip-redirects-skip')+'">'+
'<input type="radio" class="redirects" value="skip" name="redir" id="redir-skip"> '+JWB.msg('redirects-skip')+' '+
'</label>'+
'<label title="'+JWB.msg('tip-redirects-edit')+'">'+
'<input type="radio" class="redirects" value="edit" name="redir" id="redir-edit" checked> '+JWB.msg('redirects-edit')+''+
'</label>'+
'</fieldset>'+
'<fieldset>'+
'<legend>'+JWB.msg('label-skip-when')+'</legend>'+
'<label><input type="checkbox" id="skipNoChange"> '+JWB.msg('skip-no-change')+'</label>'+
'<br>'+
'<label><input type="radio" id="exists-yes" name="exists" value="yes"> '+JWB.msg('skip-exists-yes')+'</label>'+
'<label><input type="radio" id="exists-no" name="exists" value="no" checked> '+JWB.msg('skip-exists-no')+'</label>'+
'<label><input type="radio" id="exists-neither" name="exists" value="neither">'+JWB.msg('skip-exists-neither')+'</label>'+
(JWB.sysop?'<br><label><input type="checkbox" id="skipAfterAction" checked> '+JWB.msg('skip-after-action')+'</label>':'')+
'<hr/>'+
'<label>'+JWB.msg('skip-contains')+' <input class="fullwidth" type="text" id="skipContains"></label>'+
'<label>'+JWB.msg('skip-not-contains')+' <input class="fullwidth" type="text" id="skipNotContains"></label>'+
'<div class="regexswitch">'+
'<label><input type="checkbox" id="containRegex"> '+JWB.msg('label-useregex')+'</label>'+
'<a class="re101" href="http://regex101.com/#javascript" target="_blank">?</a>'+
'<label class="divisor" title="'+JWB.msg('tip-regex-flags')+'" style="display:none;">'+
JWB.msg('label-regex-flags')+' <input type="text" id="containFlags"/>'+
'</label>'+
'</div>'+
'<hr/>'+
'<label title="'+JWB.msg('skip-cg-prefix')+'">'+JWB.msg('skip-category')+' <input class="fullwidth" type="text" id="skipCategories"></label>'+
'</fieldset>'
);
if (JWB.sysop) $('.JWBtabc[data-tab="4"]').html(
'<fieldset>'+
'<legend>'+JWB.msg('move-header')+'</legend>'+
'<label><input type="checkbox" id="suppressRedir"> '+JWB.msg('move-redir-suppress')+'</label>'+
'<br>'+
JWB.msg('move-also')+' '+
'<label><input type="checkbox" id="movetalk"> '+JWB.msg('move-talk-page')+'</label> '+
'<label><input type="checkbox" id="movesubpage"> '+JWB.msg('move-subpage')+'</label>'+
'<br>'+
'<label>'+JWB.msg('move-new-name')+' <input type="text" id="moveTo"></label>'+
'</fieldset>'+
'<fieldset>'+
'<legend>'+JWB.msg('protect-header')+'</legend>'+
JWB.msg('protect-edit')+
' <select id="editProt">'+
'<option value="all" selected>'+JWB.msg('protect-none')+'</option>'+
'<option value="autoconfirmed">'+JWB.msg('protect-autoconf')+'</option>'+
'<option value="sysop">'+JWB.msg('protect-sysop')+'</option>'+
'</select> '+
'<br>'+
JWB.msg('protect-move')+
' <select id="moveProt">'+
'<option value="" selected>('+JWB.msg('protect-like-edit')+')</option>'+
'<option value="all">'+JWB.msg('protect-none')+'</option>'+
'<option value="autoconfirmed">'+JWB.msg('protect-autoconf')+'</option>'+
'<option value="sysop">'+JWB.msg('protect-sysop')+'</option>'+
'</select> '+
'<br>'+
JWB.msg('protect-upload')+
' <select id="uploadProt">'+
'<option value="" selected>('+JWB.msg('protect-like-edit')+')</option>'+
'<option value="all">'+JWB.msg('protect-none')+'</option>'+
'<option value="autoconfirmed">'+JWB.msg('protect-autoconf')+'</option>'+
'<option value="sysop">'+JWB.msg('protect-sysop')+'</option>'+
'</select> '+
'<br>'+
'<label>'+JWB.msg('protect-expiry')+' <input type="text" id="protectExpiry"/></label>'+
'</fieldset>'+
'<button id="movePage" disabled accesskey="m">'+JWB.msg('editbutton-move')+'</button> '+
'<button id="deletePage" disabled accesskey="x">'+JWB.msg('editbutton-delete')+'</button> '+
'<button id="protectPage" disabled accesskey="z">'+JWB.msg('editbutton-protect')+'</button> '+
'<button id="skipPage" disabled title="['+JWB.tooltip+'n]">'+JWB.msg('editbutton-skip')+'</button>'
);
$('.JWBtabc[data-tab="5"]').html('<table id="actionlog"><tbody></tbody></table>');
$('#pagelistPopup form').html(
'<div id="ns-filter" title="'+JWB.msg('tip-ns-select')+'">' + JWB.msg('label-ns-select') + NSList + '</div>'+
'<fieldset>'+
'<legend><label><input type="checkbox" id="categorymembers" name="categorymembers" value="cm"> '+JWB.msg('legend-cm')+'</label></legend>'+
'<label title="'+JWB.msg('tip-cm')+'">'+JWB.msg('label-cm')+' <input type="text" name="cmtitle" id="cmtitle"></label>'+
'<div>'+JWB.msg('cm-include')+' '+
'<label><input type="checkbox" id="cmtype-page" name="cmtype" value="page" checked> '+JWB.msg('cm-include-pages')+'</label>'+
'<label><input type="checkbox" id="cmtype-subcg" name="cmtype" value="subcat" checked> '+JWB.msg('cm-include-subcgs')+'</label>'+
'<label><input type="checkbox" id="cmtype-file" name="cmtype" value="file" checked> '+JWB.msg('cm-include-files')+'</label>'+
'</div>'+
'</fieldset>'+
'<fieldset>'+
'<legend><label><input type="checkbox" name="linksto" id="linksto"> '+JWB.msg('legend-linksto')+'</label></legend>'+
'<label>'+JWB.msg('label-linksto')+' <input type="text" name="title" id="linksto-title"></label>'+
'<div>'+JWB.msg('links-include')+' '+
'<label><input type="checkbox" id="backlinks" name="backlinks" value="bl" checked> '+JWB.msg('links-include-links')+'</label>'+
'<label><input type="checkbox" id="embeddedin" name="embeddedin" value="ei"> '+JWB.msg('links-include-templ')+'</label>'+
'<label><input type="checkbox" id="imageusage" name="imageusage" value="iu"> '+JWB.msg('links-include-files')+'</label>'+
'</div>'+
'<div>'+JWB.msg('links-redir')+' '+
'<label><input type="radio" id="rfilter-redir" name="filterredir" value="redirects"> '+JWB.msg('links-redir-redirs')+'</label>'+
'<label><input type="radio" id="rfilter-nonredir" name="filterredir" value="nonredirects"> '+JWB.msg('links-redir-noredirs')+'</label>'+
'<label><input type="radio" id="rfilter-all" name="filterredir" value="all" checked> '+JWB.msg('links-redir-all')+'</label>'+
'</div>'+
'<label title="'+JWB.msg('tip-link-redir')+'">'+
'<input type="checkbox" name="redirect" value="true" checked id="linksto-redir"> '+JWB.msg('label-link-redir')+
'</label>'+
'</fieldset>'+
'<fieldset>'+
'<legend><label><input type="checkbox" id="prefixsearch" name="prefixsearch" value="ps"> '+JWB.msg('legend-ps')+'</label></legend>'+
'<label>'+JWB.msg('label-ps')+' <input type="text" name="pssearch" id="pssearch"></label>'+
'<label title="'+JWB.msg('tip-ps-strict')+'"><input type="checkbox" name="allpages" value="ap" id="psstrict" checked> '+JWB.msg('label-ps-strict')+'</label>'+
'</fieldset>'+
'<fieldset>'+
'<legend><label><input type="checkbox" id="watchlistraw" name="watchlistraw" value="wr"> '+JWB.msg('legend-wr')+'</label></legend>'+
JWB.msg('label-wr')+
'</fieldset>'+
'<fieldset>'+
'<legend><label><input type="checkbox" id="proplinks" name="links" value="pl"> '+JWB.msg('legend-pl')+'</label></legend>'+
'<label title="'+JWB.msg('tip-pl')+'">'+JWB.msg('label-pl')+' <input type="text" id="titles" name="titles"></label>'+
'</fieldset>'+
'<fieldset>'+
'<legend><label><input type="checkbox" id="proplinks" name="search" value="sr"> '+JWB.msg('legend-sr')+'</label></legend>'+
'<label title="'+JWB.msg('tip-sr')+'\n'+JWB.msg('placeholder-sr', 'insource:', 'intitle:')+'">'+
JWB.msg('label-sr')+' <input type="text" id="srsearch" name="srsearch" placeholder="'+JWB.msg('placeholder-sr', 'insource:', 'intitle:')+'">'+
'</label>'+
'</fieldset>'+
'<fieldset class="listSMW">'+
'<legend><label><input type="checkbox" id="smwask" name="smwask" value="smw"> '+JWB.msg('legend-smw', JWB.msg('smw-slow'))+'</label></legend>'+
'<textarea id="smwquery" name="smwquery" placeholder="'+JWB.msg('label-smw', '\n|limit=500')+'"></textarea>'+
'</fieldset>'+
'<button type="submit">'+JWB.msg('pagelist-generate')+'</button>'
);
if (JWB.hasSMW) {
$('#pagelistPopup').addClass('hasSMW')
}
$('body').addClass('AutoWikiBrowser'); //allow easier custom styling of JWB.
$('[accesskey]').each(function() {
let lbl = this.accessKeyLabel || this.accessKey; // few browsers support accessKeyLabel, so fallback to accessKey.
$(this).attr('title', '['+lbl+']');
});
/***** Setup *****/
JWB.setup.save('_blank'); //default setup
if (JWB.settings.hasOwnProperty('default')) {
JWB.setup.apply();
} else if (JWB.setup.initialised) {
// If we already initialised, create the default settings profile.
JWB.setup.save('default');
}
JWB.setup.extend({});


/***** Event handlers *****/
var dateHeaderRegex = date.monthHeaderRegex(), dateHeaderRegexLast, dateHeaderRegexResult;
while ((dateHeaderRegexLast = dateHeaderRegex.exec(text)) !== null) {
//Alert user when leaving the tab, to prevent accidental closing.
dateHeaderRegexResult = dateHeaderRegexLast;
onbeforeunload = function() {
}
return "Closing this tab will cause you to lose all progress.";
// If dateHeaderRegexResult is null then lastHeaderIndex is never checked. If it is not null but
};
// \n== is not found, then the date header must be at the very start of the page. lastIndexOf
ondragover = function(e) {
// returns -1 in this case, so lastHeaderIndex gets set to 0 as desired.
e.preventDefault();
var lastHeaderIndex = text.lastIndexOf('\n==') + 1;
};

document.addEventListener("securitypolicyviolation", function(e) {
if (text.length > 0) {
console.log('violated CSP:', e);
text += '\n\n';
if (e.blockedURI == 'blob') {
}
JWB.worker.supported = false; // tell the next JWB.worker.init() that it shouldn't even try.

} else if (JWB && JWB.msg) {
if (!dateHeaderRegexResult || dateHeaderRegexResult.index !== lastHeaderIndex) {
alert(JWB.msg('csp-error', e.violatedDirective))
Morebits.status.info('Info', 'Will create a new talk page section for this month, as none was found');
text += date.monthHeader() + '\n';
}
}
})
$('.JWBtab').click(function() {
$('.active').removeClass('active');
$(this).addClass('active');
$('.JWBtabc[data-tab="'+$(this).attr('data-tab')+'"]').addClass('active');
});
function showRegexFlags() {
// >>this<< is the element that's triggered
$(this).parent().nextAll('label').toggle(this.checked);
}
}
$('body').on('change', '#useRegex, #containRegex, .useRegex', showRegexFlags);

params.expiry = typeof params.template_expiry !== 'undefined' ? params.template_expiry : params.expiry;
$('#preparse-reset').click(function() {

$('#articleList').val($('#articleList').val().replace(/#PRE-PARSE-STOP/g,'').replace(/\n\n/g, '\n'));
text += Twinkle.block.callback.getBlockNoticeWikitext(params);
});

$('#saveAs').click(function() {
// build the edit summary
JWB.setup.save();
var summary = messageData.summary;
});
if (messageData.suppressArticleInSummary !== true && params.article) {
$('#loadSettings').change(function() {
summary += ' on [[:' + params.article + ']]';
JWB.setup.apply(this.value);
});
$('#download').click(JWB.setup.download);
$('#saveToWiki').click(JWB.setup.submit);
$('#import').change(JWB.setup.import);
ondrop = JWB.setup.import;
$('#updateSetups').click(JWB.setup.load);
$('#deleteSetup').click(JWB.setup.del);
if (window.RETF) {
$('#refreshRETF').click(RETF.load);
$('#skipRETF').click(JWB.skipRETF)
$('#enableRETF').change(function() {
$('#skipRETF').css('visibility', this.checked ? 'visible' : 'hidden');
});
}
}
summary += '.';


$('#replacesButton, #pagelistButton').click(function() {
pageobj.setPageText(text);
var popup = this.id.slice(0, -6); //omits the 'Button' in the id by cutting off the last 6 characters
pageobj.setEditSummary(summary);
$('#'+popup+'Popup, #overlay').show();
pageobj.setChangeTags(Twinkle.changeTags);
});
pageobj.setWatchlist(Twinkle.getPref('watchWarnings'));
$('#overlay').click(function() {
pageobj.save();
$('#replacesPopup, #pagelistPopup, #overlay').hide();
JWB.pl.done = true;
JWB.pl.stop();
});
$('#moreReplaces').click(function() {
$('#replacesPopup').append(findreplace);
});
$('#replacesPopup').on('keydown', '.replaces:last', function(e) {
if (e.which === 9) $('#moreReplaces')[0].click();
});
$('#pl-form').submit(function(e) {
e.preventDefault();
JWB.pl.generate();
return false;
});
$('#pagelistPopup legend input').change(function() {
//remove disabled attr when checked, add when not.
$(this).parents('fieldset').find('input').not('legend input').prop('disabled', !this.checked);
$(this).parents('fieldset').prop('disabled', !this.checked);
}).trigger('change');
$('#psstrict').change(function() {
if (this.checked) {
$('#pssearch').attr('name', 'apprefix');
} else {
$('#pssearch').attr('name', 'pssearch');
}
}).trigger('change');
$('#resultWindow').on('click', 'tr[data-line]:not(.lineheader) *', function(e) {
var line = +$(e.target).closest('tr[data-line]').data('line');
var index = $('#editBoxArea').val().split('\n').slice(0, line-1).join('\n').length;
$('#editBoxArea')[0].focus();
JWB.fn.setSelection($('#editBoxArea')[0], index+1);
JWB.fn.scrollSelection($('#editBoxArea')[0], index);
});
$('#removeDupes').click(function() {
$('#articleList').val(JWB.fn.uniques($('#articleList').val().split('\n')).join('\n'));
JWB.pageCount();
});
$('#sortArticles').click(function() {
$('#articleList').val($('#articleList').val().split('\n').sort().join('\n'));
JWB.pageCount();
});
$('#watchNow').click(JWB.api.watch);
$('#autosave').change(function() {
$('#throttle').prop('disabled', !this.checked);
});
$('#viaJWB').change(function() {
$('#summary').parent('label')
.toggleClass('viaJWB', this.checked)
.attr('maxlength', 500 - this.checked*JWB.summarySuffix.length); // Change the max size of the allowed summary according to having a suffix or not.
});
$('#startbutton').click(JWB.start);
$('#stopbutton').click(JWB.stop);
$('#submitButton').click(JWB.api.submit);
$('#previewButton').click(JWB.api.preview);
$('#diffButton').click(JWB.api.diff);
$('#skipButton, #skipPage').click(function() {
JWB.log('skip', JWB.list[0].split('|')[0]);
JWB.next();
});
if (JWB.sysop) {
$('#movePage').click(function() {
if ($('#moveTo').val().length === 0) {
return alert(JWB.msg('alert-no-move'));
}
JWB.api.move();
});
$('#protectPage').click(JWB.api.protect);
$('#deletePage').click(JWB.api.del);
}
};
};


//Disable JWB altogether when it's loaded on a page other than Project:AutoWikiBrowser/Script. This script shouldn't be loaded on any other page in the first place.
Twinkle.addInitCallback(Twinkle.block, 'block');
if (JWB.allowed === false) JWB = false;
})(jQuery);


// </nowiki>

Revision as of 22:00, 22 January 2022

/** <nowiki>
 * Install this script by pasting the following in your personal JavaScript file:

mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.js/load.js&action=raw&ctype=text/javascript');

 * Or for users on en.wikipedia.org:

{{subst:lusc|User:Joeytje50/JWB.js/load.js}}

 * Note that this script will only run on the 'Project:AutoWikiBrowser/Script' page.
 * This script is based on the downloadable AutoWikiBrowser.
 * 
 * @licence
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 * @version 4.3.1
 * @author Joeytje50
 * </nowiki>
 */

window.JWBdeadman = false; // ADMINS: in case of fire, set this variable to true to disable this entire tool for all users

//TODO: more advanced pagelist-generating options
//TODO: generate page list based on images on a page
//TODO: Add feature to perform general cleanup (<table> to {|, fullurl-links to wikilinks, removing underscores from wikilinks)
//TODO: Add report button to AJAX error alert box
//TODO: Re-add errored pages to the page list

//Cleanup / modernize:
// .indexOf('') != -1 -> .includes()
// mw for requests, instead of ajax
// Optional?.chaining?.properties

/***** Global object/variables *****/
window.JWB = {}; //The main global object for the script.

(function() {
	// Easier way to change import location for local debugging etc.
	JWB.imports = {
		'JWB.css':	'//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.css&action=raw&ctype=text/css',
		'i18n.js':	'//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.js/i18n.js&action=raw&ctype=text/javascript',
		'i18n':		{},
		'RETF.js':	'//en.wikipedia.org/w/index.php?title=User:Joeytje50/RETF.js&action=raw&ctype=text/javascript',
		'worker.js':'//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.js/worker.js&action=raw&ctype=text/javascript',
	};
	
	let objs = ['page', 'api', 'worker', 'fn', 'pl', 'messages', 'setup', 'settings', 'ns'];
	for (let i=0;i<objs.length;i++) {
		JWB[objs[i]] = {};
	}
	JWB.summarySuffix = ' (via JWB)';
	if (document.location.hostname == 'en.wikipedia.org') JWB.summarySuffix = ' (via [[WP:JWB]])';
	JWB.lang = mw.config.get('wgUserLanguage').replace('-', '_');
	JWB.contentLang = mw.config.get('wgContentLanguage').replace('-', '_');
	JWB.index_php = mw.config.get('wgScript');
	JWB.isStopped = true;
	JWB.tooltip = window.tooltipAccessKeyPrefix || '';
	let configext = 'js';
	if (document.location.hostname.split('.').slice(-2).join('.') == 'wikia.com' || document.location.hostname.split('.').slice(-2).join('.') == 'fandom.com') {
		//LEGACY: fallback to settings on css for Wikia; uses JSON now.
		configext = 'css';
	}
	JWB.settingspage = 'JWB-settings.'+configext;
	if (window.hasOwnProperty('JWBSETTINGS')) {
		JWB.settingspage = JWBSETTINGS+'-settings.'+configext;
		delete window.JWBSETTINGS; //clean up the global variable
	}
	JWB.hasJSON = false; // whether or not the wiki supports JSON userpages (mw 1.31+).
	JWB.hasSMW = false; // whether or not the wiki has SMW installed.
})();

/***** User verification *****/

(function() {
	if (mw.config.get('wgCanonicalNamespace')+':'+mw.config.get('wgTitle') !== 'Project:AutoWikiBrowser/Script' || JWB.allowed === false || mw.config.get('wgUserName') === null) {
		JWB.allowed = false;
		return;
	}
	mw.loader.load(JWB.imports['JWB.css'], 'text/css');
	mw.loader.load('mediawiki.diff.styles');
	
	$.getScript(JWB.imports['i18n.js'], function() {
		if (JWB.allowed === false) {
			alert(JWB.msg('not-on-list'));
			return;
		}
		let langs = [];
		if (JWB.lang !== 'en' && JWB.imports.i18n.hasOwnProperty(JWB.lang)) {
			langs.push(JWB.imports.i18n[JWB.lang]);
			JWB.messages[JWB.lang] = JWB.messages[JWB.lang] || null;
		} else if (JWB.lang !== 'en' && JWB.lang !== 'qqx') {
			// this only happens if the language file does not exist.
			JWB.lang = 'en';
		}
		if (JWB.contentLang !== 'en' && JWB.contentLang !== JWB.lang && JWB.imports.i18n.hasOwnProperty(JWB.contentLang)) {
			langs.push(JWB.imports.i18n[JWB.contentLang]);
			JWB.messages[JWB.contentLang] = JWB.messages[JWB.contentLang] || null;
		}
		if (langs.length) {
			$.when.apply($, langs.map(url => $.getScript(url))).done(function(r) {
				if (JWB.allowed === true && JWB.messages.length == langs + 1) { // if there are two languages to load, wait for them both.
					console.log('langs loaded');
					JWB.init(); //init if verification has already returned true
				} else if (JWB.allowed === false) {
					alert(JWB.msg('not-on-list'));
				}
			});
		} else if (JWB.allowed === true) { // no more languages to load.
			console.log('no langs loaded');
			JWB.init();
		}
	});
	
	//RegEx Typo Fixing
	$.getScript(JWB.imports['RETF.js'], function() {
			$('#refreshRETF').click(RETF.load);
	});

	if (window.JWBdeadman === true) {
		window.JWB = false; // disable all access
		alert("This tool has been temporarily been disabled by Wikipedia admins due to issues it would otherwise cause. Please check back soon to see if it is working again.");
		return false;
	} else if (!window.Worker) {
		// https://caniuse.com/webworkers - this should not happen for any sensible human being. Either you're on IE<10, or you're just testing my patience.
		alert("Web Workers are not supported in this browser. Please use a more modern browser to use JWB. Most matching and replacing features are not supported in this browser.");
	}

	(new mw.Api()).get({
		action: 'query',
		titles: 'Project:AutoWikiBrowser/CheckPageJSON',
		prop: 'info|revisions',
		meta: 'userinfo|siteinfo',
		rvprop: 'content',
		rvlimit: 1,
		uiprop: 'groups',
		siprop: 'namespaces|usergroups|extensions',
		indexpageids: true,
		format: 'json',
	}).done(function(response) {
		if (response.error) {
			alert('API error: ' + response.error.info);
			JWB = false; //preventing further access. No verification => no access.
			return;
		}
		JWB.ns = response.query.namespaces; //saving for later
		
		// This will execute before JWB.init() and therefore before JWB.setup.load() loading the user's settings.
		let wikigroups = response.query.usergroups;
		for (var u of wikigroups) {
			if (u.rights.indexOf('edituserjson') !== -1) {
				JWB.hasJSON = true;
				break;
			}
		}
		
		// Check if we've got SMW on this wiki
		let extensions = response.query.extensions;
		for (var e of extensions) {
			if (e.name == "SemanticMediaWiki") {
				JWB.hasSMW = true;
				break;
			}
		}
		
		JWB.username = response.query.userinfo.name; //preventing any "hacks" that change wgUserName or mw.config.wgUserName
		var groups = response.query.userinfo.groups;
		var page = response.query.pages[response.query.pageids[0]];
		var users = [];
		var bots = [];
		JWB.sysop = groups.indexOf('sysop') !== -1;
		if (response.query.pageids[0] !== '-1') {
			var checkPageData = JSON.parse(page.revisions[0]['*']);
			users = checkPageData.enabledusers;
			if ("enabledbots" in checkPageData) {
				bots = checkPageData.enabledbots;
			}
		} else {
			users = false; //fallback when page doesn't exist
			if (JWB.sysop) { // Check and inform admins if their checkpage is the unsupported format.
				(new mw.Api()).get({
					action: 'query',
					titles: 'Project:AutoWikiBrowser/CheckPage',
					prop: 'info',
					indexpageids: true,
				}).done(function(oldpage){
					var q = oldpage.query;
					if (q.pageids[0] != '-1' && !q.pages[q.pageids[0]].hasOwnProperty('redirect')) {
						// CheckPageJSON does not exist, and CheckPage does exist, and is not a redirect.
						// This indicates the checkpage needs to be ported to JSON. Notify admins.
						prompt('Warning: The AWB checkpage found at Project:AutoWikiBrowser/CheckPage is no longer supported.\n'+
								'Please convert this checkpage to a JSON checkpage. See the URL below for more information.\n'+
								'After creating the JSON checkpage, you can use "Special:ChangeContentModel" to change the content model to JSON.',
								'https://en.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/CheckPage_format');
					}
				});
			}
		}
		JWB.bot = groups.indexOf('bot') !== -1 && (users === false || bots.includes(JWB.username));
		// Temporary global debugging variables
		JWB.debug = [groups.indexOf('bot'), users === false, bots && bots.indexOf(JWB.username)];
		if (JWB.username === "Joeytje50" && response.query.userinfo.id === 13299994) {//TEMP: Dev full access to entire interface.
			JWB.bot = true;
			users.push("Joeytje50");
		}
		var allLoaded = true;
		for (var m in JWB.messages) if (JWB.messages[m] === null) allLoaded = false;
		if (JWB.sysop || response.query.pageids[0] === '-1' || users === false || users.includes(JWB.username) || bots.includes(JWB.username)) {
			JWB.allowed = true;
			if (allLoaded) JWB.init(); //init if messages have already loaded
		} else {
			if (allLoaded) {
				//run this after messages have loaded, so the message that shows is in the user's language
				alert(JWB.msg('not-on-list'));
			}
			JWB = false; //prevent further access
		}
	}).fail(function(xhr, error) {
		alert(JWB.msg('verify-error') + '\n' + error);
		JWB = false; //preventing further access. No verification => no access.
	});
})();

/***** API functions *****/

//Main template for API calls
JWB.api.call = function(data, callback, onerror) {
	data.format = 'json';
	if (data.action !== 'query' && data.action !== 'compare' && data.action !== 'ask') {
		data.bot = true; // mark edits as bot
	}
	$.ajax({
		data: data,
		dataType: 'json',
		url: mw.config.get('wgScriptPath') + '/api.php',
		type: 'POST',
		success: function(response) {
			if (response.error) {
				if (onerror && onerror(response, 'API') === false) return;
				alert('API error: ' + response.error.info);
				JWB.stop();
			} else {
				callback(response);
			}
		},
		// onerror: if it exists and returns false, do not show error alert. Otherwise, do show alert.
		error: function(xhr, error) {
			if (onerror && onerror(error, 'AJAX') === false) return;
			alert('AJAX error: ' + error);
			JWB.stop();
		}
	});
};

//Get page diff, and process it for more interactivity
JWB.api.diff = function(callback) {
	if (JWB.isStopped) return; // prevent new API calls when stopped
	JWB.status('diff');
	var editBoxInput = $('#editBoxArea').val();
	var redirect = $('input.redirects:checked').val();
	var data = {
		action: 'compare',
		indexpageids: true,
		fromtitle: JWB.page.name,
		//toslots: 'main', // TODO: Once this gets supported more widely, convert to the non-deprecated toslots system.
		//'totext-main': editBoxInput,
		totext: editBoxInput,
		topst: true,
	};
	if (redirect=='follow') data.redirects = true;
	JWB.api.call(data, function(response) {
		var diff;
		diff = response.compare['*'];
		if (diff === '') {
			diff = '<h2>'+JWB.msg('no-changes-made')+'</h2>';
		} else {
			diff = '<table class="diff">'+
				'<colgroup>'+
					'<col class="diff-marker">'+
					'<col class="diff-content">'+
					'<col class="diff-marker">'+
					'<col class="diff-content">'+
				'</colgroup>'+
				'<tbody>'+diff+'</tbody></table>';
		}
		$('#resultWindow').html(diff);
		$('.diff-lineno').each(function() {
			var lineNumMatch = $(this).html().match(/\d+/);
			if (lineNumMatch) {
				$(this).parent().attr('data-line',parseInt(lineNumMatch[0])-1).addClass('lineheader');
			}
		});
		$('table.diff tr').each(function() { //add data-line attribute to every line, relative to the previous one. Used for click event.
			if (!$(this).next().is('[data-line]') && !$(this).next().has('td.diff-deletedline + td.diff-empty')) {
				$(this).next().attr('data-line',parseInt($(this).data('line'))+1);
			} else if ($(this).next().has('td.diff-deletedline + td.diff-empty')) {
				$(this).next().attr('data-line',$(this).data('line')); //copy over current data-line for deleted lines to prevent them from messing up counting.
			}
		});
		JWB.status('done', true);
		if (typeof(callback) === 'function') {
			callback();
		}
	}, function(err, type) {
		if (type == 'API' && err.error.code == 'missingtitle') {
			// missingtitle is to be expected when editing a page that doesn't exist; just show a message and move on.
			$('#resultWindow').html('<span style="font-weight:bold;color:red;">'+JWB.msg('page-not-exists')+'</span>');
			JWB.status('done', true);
			if (typeof(callback) === 'function') {
				callback();
			}
			return false; // stop propagation of error; do not show alerts.
		}
	});
};

//Retrieve page contents/info, process them, and store information in JWB.page object.
JWB.api.get = function(pagename) {
	if (JWB.isStopped) return; // prevent new API calls when stopped
	JWB.pageCount();
	if (!JWB.list[0] || JWB.isStopped) {
		return JWB.stop();
	}
	if (pagename === '#PRE-PARSE-STOP') {
		var curval = $('#articleList').val();
		$('#articleList').val(curval.substr(curval.indexOf('\n') + 1));
		$('#preparse').prop('checked', false);
		JWB.stop();
		return;
	}
	let cgns = JWB.ns[14]['*'];
	let skipcg = $('#skipCategories').val();
	// prepend Category: before all categories and turn CSV(,) into CSV(|).
	skipcg = skipcg.replace(new RegExp('(^|,|\\|)('+cgns+':)?', 'gi'), '|'+cgns+':').substr(1);
	var redirect = $('input.redirects:checked').val();
	var data = {
		action: 'query',
		prop: 'info|revisions|categories',
		inprop: 'watched|protection',
		type: 'csrf|watch',
		titles: pagename,
		rvprop: 'content|timestamp|ids',
		rvlimit: '1',
		cllimit: 'max',
		clcategories: skipcg,
		indexpageids: true,
		meta: 'userinfo|tokens',
		uiprop: 'hasmsg'
	};
	if (redirect=='follow'||redirect=='skip') data.redirects = true;
	if (JWB.sysop) {
		data.list = 'deletedrevs';
	}
	JWB.status('load-page');
	JWB.api.call(data, function(response) {
		if (response.query.userinfo.hasOwnProperty('messages')) {
			var view = mw.config.get('wgScriptPath') + '?title=Special:MyTalk';
			var viewNew = view + '&diff=cur';
			JWB.status(
				'<span style="color:red;font-weight:bold;">'+
					JWB.msg('status-newmsg', 
						'<a href="'+view+'" target="_blank">'+JWB.msg('status-talklink')+'</a>',
						'<a href="'+viewNew+'" target="_blank">'+JWB.msg('status-difflink')+'</a>')+
				'</span>', true);
			alert(JWB.msg('new-message'));
			JWB.stop();
			return;
		}
		JWB.page = response.query.pages[response.query.pageids[0]];
		JWB.page.token = response.query.tokens.csrftoken;
		JWB.page.watchtoken = response.query.tokens.watchtoken;
		JWB.page.name = JWB.list[0].split('|')[0];
	 	var varOffset = JWB.list[0].indexOf('|') !== -1 ? JWB.list[0].indexOf('|') + 1 : 0;
	 	JWB.page.pagevar = JWB.list[0].substr(varOffset);
		JWB.page.content = JWB.page.revisions ? JWB.page.revisions[0]['*'] : '';
		JWB.page.exists = !response.query.pages["-1"];
		JWB.page.deletedrevs = response.query.deletedrevs;
		JWB.page.watched = JWB.page.hasOwnProperty('watched');
		JWB.page.protections = JWB.page.restrictiontypes;

		if (response.query.redirects) {
			JWB.page.name = response.query.redirects[0].to;
		}
		// check for skips that can be determined before replacing
		if (!JWB.fn.allowBots(JWB.page.content, JWB.username) || !JWB.fn.allowBots(JWB.page.content)) {
			// skip if {{bots}} template forbids editing on this page by user OR by JWB in general
			JWB.log('nobots', JWB.page.name);
			return JWB.next();
		} else if (JWB.page.categories !== undefined || // skip because of a matching category as passed via clcategories.
				($('#exists-no').prop('checked') && !JWB.page.exists) ||
				($('#exists-yes').prop('checked') && JWB.page.exists) ||
				(redirect==='skip' && response.query.redirects) // variable  redirect  is defined outside this callback function.
		) {
			// simple skip rules
			JWB.log('skip', JWB.page.name);
			return JWB.next();
		}
		// Check skip contains rules.
		var containRegex = $('#containRegex').prop('checked'),
			containFlags = $('#containFlags').val();
		var skipContains, skipNotContains;
		if (containRegex) {
			JWB.status('check-skips');
			var skipping = false; // for tracking if match is found in synchronous calls.
			if ($('#skipContains').val().length) {
				JWB.worker.match(JWB.page.content, $('#skipContains').val(), containFlags, function(result, err) {
					console.log('Contains', result, err);
					if (result !== null && err === undefined) {
						JWB.log('skip', JWB.page.name);
						JWB.next(); // next() also cancels the skipNotContains.
						skipping = true;
						return;
					} // else continue with the queued worker job that checks skipNotContains
				});
			}
			if (skipping) {
				console.log('skipped page before replaces')
				return;
			}
			if ($('#skipNotContains').val().length) {
				JWB.worker.match(JWB.page.content, $('#skipNotContains').val(), containFlags, function(result, err) {
					console.log('Not contains', result, err);
					if (result === null && err === undefined) {
						JWB.log('skip', JWB.page.name);
						JWB.next(); // also cancels the replace
						skipping = true;
						return;
					} // else move on to replacing
				});
			}
			if (skipping) {
				console.log('skipped page before replaces')
				return;
			}
		} else {
			skipContains = $('#skipContains').val();
			skipNotContains = $('#skipNotContains').val();
			if ((skipContains && JWB.page.content.includes(skipContains)) ||
				(skipNotContains && !JWB.page.content.includes(skipNotContains))) {
				console.log('skipped page before replaces')
				return JWB.next();
			}
			JWB.status('done', true);
		}
		JWB.replace(JWB.page.content, function(newContent) {
			if (JWB.isStopped === true) return;
			if ($('#skipNoChange').prop('checked') && JWB.page.content === newContent) { //skip if no changes are made
				JWB.log('skip', JWB.page.name);
				return JWB.next();
			} else {
				JWB.editPage(newContent);
			}
			JWB.updateButtons();
		});
	});
};

//Some functions with self-explanatory names:
JWB.api.submit = function(page) {
	if (JWB.isStopped) return; // prevent new API calls when stopped
	JWB.status('submit');
	var summary = $('#summary').val();
	if ($('#summary').parent('label').hasClass('viaJWB')) summary += JWB.summarySuffix;
	if ((typeof page === 'text' && page !== JWB.page.name) || $('#currentpage a').html().replace(/&amp;/g, '&') !== JWB.page.name) {
		console.log(page, JWB.page.name, $('#currentpage a').html())
		JWB.stop();
		alert(JWB.msg('autosave-error', JWB.msg('tab-log')));
		$('#currentpage').html(JWB.msg('editbox-currentpage', ' ', ' '));
		return;
	}
	var newval = $('#editBoxArea').val();
	var diffsize = newval.length - JWB.page.content.length;
	if ($('#sizelimit').val() != 0 && Math.abs(diffsize) > parseInt($('#sizelimit').val())){
		alert(JWB.msg('size-limit-exceeded', diffsize > 0 ? '+'+diffsize : diffsize));
		JWB.status('done', true);
		return;
	}
	var data = {
		title: JWB.page.name,
		summary: summary,
		action: 'edit',
		//tags: 'JWB',
		basetimestamp: JWB.page.revisions ? JWB.page.revisions[0].timestamp : '',
		token: JWB.page.token,
		text: newval,
		watchlist: $('#watchPage').val()
	};
	if ($('#minorEdit').prop('checked')) data.minor = true;
	JWB.api.call(data, function(response) {
		JWB.log('edit', response.edit.title, response.edit.newrevid);
	}, function(error, errtype) {
		var cont = false;
		if (errtype == 'API') {
			cont = confirm("API error: " + error.error.info + "\n" + JWB.msg('confirm-continue'));
		} else {
			cont = confirm("AJAX error: " + error + "\n" + JWB.msg('confirm-continue'));
		}
		if (!cont) {
			JWB.stop();
		}
		return false; // do not fall back on default error handling
	});
	// While the edit is submitting, continue to the next page to edit.
	JWB.status('done', true);
	JWB.next();
};
JWB.api.preview = function() {
	if (JWB.isStopped) return; // prevent new API calls when stopped
	JWB.status('preview');
	JWB.api.call({
		title: JWB.page.name,
		action: 'parse',
		pst: true,
		text: $('#editBoxArea').val()
	}, function(response) {
		$('#resultWindow').html(response.parse.text['*']);
		$('#resultWindow div.previewnote').remove();
		JWB.status('done', true);
	});
};
JWB.api.move = function() {
	if (JWB.isStopped) return; // prevent new API calls when stopped
	JWB.status('move');
	var topage = $('#moveTo').val().replace(/\$x/gi, JWB.page.pagevar);
	var summary = $('#summary').val();
	if ($('#summary').parent('label').hasClass('viaJWB')) summary += JWB.summarySuffix;
	var data = {
		action: 'move',
		from: JWB.page.name,
		to: topage,
		token: JWB.page.token,
		reason: summary,
		ignorewarnings: 'yes'
	};
	if ($('#moveTalk').prop('checked')) data.movetalk = true;
	if ($('#moveSubpage').prop('checked')) data.movesubpages = true;
	if ($('#suppressRedir').prop('checked')) data.noredirect = true;
	JWB.api.call(data, function(response) {
		JWB.log('move', response.move.from, response.move.to);
		JWB.status('done', true);
		if (!$('#moveTo').val().match(/\$x/i)) $('#moveTo').val('')[0].focus(); //clear entered move-to pagename if it's not based on the pagevar
		JWB.next(topage);
	});
};
JWB.api.del = function() {
	if (JWB.isStopped) return; // prevent new API calls when stopped
	JWB.status(($('#deletePage').is('.undelete') ? 'un' : '') + 'delete');
	var summary = $('#summary').val();
	if ($('#summary').parent('label').hasClass('viaJWB')) summary += JWB.summarySuffix;
	JWB.api.call({
		action: (!JWB.page.exists ? 'un' : '') + 'delete',
		title: JWB.page.name,
		token: JWB.page.token,
		reason: summary
	}, function(response) {
		JWB.log((!JWB.page.exists ? 'un' : '') + 'delete', (response['delete']||response.undelete).title);
		JWB.status('done', true);
		JWB.next(response.undelete && response.undelete.title);
	});
};
JWB.api.protect = function() {
	if (JWB.isStopped) return; // prevent new API calls when stopped
	JWB.status('protect');
	var summary = $('#summary').val();
	if ($('#summary').parent('label').hasClass('viaJWB')) summary += JWB.summarySuffix;
	var editprot = $('#editProt').val();
	var moveprot = $('#moveProt').val() || editprot;
	var uploadprot = $('#uploadProt').val() || editprot;
	var protstring = 'edit='+editprot+'|move='+moveprot;
	if (!JWB.page.exists)
		protstring = 'create='+editprot;
	if (JWB.page.protections.includes('upload'))
		protstring += '|upload='+uploadprot;
	JWB.api.call({
		action: 'protect',
		title: JWB.page.name,
		token: JWB.page.token,
		reason: summary,
		expiry: $('#protectExpiry').val()!==''?$('#protectExpiry').val():'infinite',
		protections: protstring,
	}, function(response) {
		var protactions = '';
		var prots = response.protect.protections;
		for (var i=0;i<prots.length;i++) {
			if (typeof prots[i].edit == 'string') {
				protactions += ' edit: '+(prots[i].edit || 'all');
			} else if (typeof prots[i].move == 'string') {
				protactions += ' move: '+(prots[i].move || 'all');
			} else if (typeof prots[i].create == 'string') {
				protactions += ' create: '+(prots[i].create || 'all');
			} else if (typeof prots[i].upload == 'string') {
				protactions += ' upload: '+(prots[i].upload || 'all');
			}
		}
		protactions += ' expires: '+prots[0].expiry;
		JWB.log('protect', response.protect.title, protactions);
		JWB.status('done', false);
		JWB.next(response.protect.title);
	});
};

JWB.api.watch = function() {
	JWB.status('watch');
	var data = {
		action: 'watch',
		title: JWB.page.name,
		token: JWB.page.watchtoken
	};
	if (JWB.page.watched) data.unwatch = true;
	JWB.api.call(data, function(response) {
		JWB.status('<span style="color:green;">'+
			JWB.msg('status-watch-'+(JWB.page.watched ? 'removed' : 'added'), "'"+JWB.page.name+"'")+
		'</span>', true);
		JWB.page.watched = !JWB.page.watched;
		$('#watchNow').html( JWB.msg('watch-' + (JWB.page.watched ? 'remove' : 'add')) );
	});
};

/***** Pagelist functions *****/

JWB.pl.iterations = 0;
JWB.pl.done = true;

JWB.pl.stop = function() {
	if (JWB.pl.done) {
		JWB.pl.iterations = 0;
		$('#pagelistPopup [disabled]:not(fieldset [disabled]), #pagelistPopup legend input, #pagelistPopup button').prop('disabled', false);
		$('#pagelistPopup legend input').trigger('change');
		$('#pagelistPopup button img').remove();
	}
}

JWB.pl.getNSpaces = function() {
	var list = $('#pagelistPopup [name="namespace"]')[0];
	return $('#pagelistPopup [name="namespace"]').val().join('|'); //.val() returns an array of selected options.
};

JWB.pl.getList = function(abbrs, lists, data) {
	$('#pagelistPopup button, #pagelistPopup input, #pagelistPopup select, #pagelistPopup button').prop('disabled', true);
	JWB.pl.iterations++;
	if (data.ask !== undefined) {
		JWB.pl.SMW(data.ask); // execute SMW call in parallel
		JWB.pl.done = false;
		data.ask = undefined;
	}
	if (!abbrs.length) {
		JWB.pl.done = true;
		return; // don't execute the rest; only a SMW query was entered.
	}
	data.action = 'query';
	var nspaces = JWB.pl.getNSpaces();
	for (var i=0;i<abbrs.length;i++) {
		if (nspaces) data[abbrs[i]+'namespace'] = data[abbrs[i]+'namespace'] || nspaces; // if namespaces are already set, use that instead (for apnamespace)
		data[abbrs[i]+'limit'] = 'max';
	}
	let linksList = lists.indexOf('links')
	if (linksList !== -1) {
		data.prop = 'links';
		lists.splice(linksList, 1)
	}
	data.list = lists.join('|');
	console.log('generating:', data);
	JWB.api.call(data, function(response) {
		var maxiterate = 100; //allow up to 100 consecutive requests at a time to avoid overloading the server.
		if (!response.query) response.query = {};
		if (response.watchlistraw) response.query.watchlistraw = response.watchlistraw; //adding some consistency
		var plist = [];
		if (response.query.pages) {
			var links;
			for (var id in response.query.pages) {
				links = response.query.pages[id].links;
				for (var i=0;i<links.length;i++) {
					plist.push(links[i].title);
				}
			}
		}
		for (var l in response.query) {
			if (l === 'pages') continue;
			for (var i=0;i<response.query[l].length;i++) {
				plist.push(response.query[l][i].title);
			}
		}
		//add the result to the pagelist immediately, as opposed to saving it all up and adding in 1 go like AWB does
		$('#articleList').val($.trim($('#articleList').val()) + '\n' + plist.join('\n'));
		JWB.pageCount();
		var cont = response.continue;
		console.log("Continue",JWB.pl.iterations, cont);
		if (cont && JWB.pl.iterations <= maxiterate) {
			var lists = [];
			if (response.query) { //compatibility with the code I wrote for the old query-continue. TODO: make this unnecessary?
				for (var list in response.query) {
					lists.push(list); //add to the new array of &list= values
				}
			}
			var abbrs = [];
			for (var abbr in cont) {
				data[abbr] = cont[abbr]; //add the &xxcontinue= value to the data
				if (abbr != 'continue') {
					abbrs.push(abbr.replace('continue','')); //find out what xx is and add it to the list of abbrs
				}
			}
			JWB.pl.getList(abbrs, lists, data); //recursive function to get every page of a list
		} else {
			if (JWB.pl.iterations > maxiterate) {
				JWB.status('pl-over-lim', true);
			} else {
				JWB.status('done', true);
			}
			JWB.pl.stop(); // if JWB.pl.done == true show stopped interface. Otherwise mark as done.
			JWB.pl.done = true;
		}
	}, function() { //on error, simply reset and let the user work with what he has
		JWB.status('done', true);
		JWB.pl.stop();
		JWB.pl.done = true;
	});
};

JWB.pl.SMW = function(query) {
	var data = {
		action: 'ask',
		query: query
	};
	JWB.api.call(data, function(response) {
		console.log(response);
		let list = response.query.results;
		let pagevar = response.query.printrequests[1];
		let pagevar_type = pagevar && pagevar.typeid;
		if (pagevar) {
			// either pagevar === undefined, or it's the first printrequest.
			pagevar = pagevar.label;
		}
		let plist = [];
		for (let l in list) {
			let page = list[l];
			let name = page.fulltext;
			let suff;
			if (pagevar) try {
				let val = page.printouts[pagevar][0];
				if (!val) continue; // this page does not contain this property.
				switch (pagevar_type) {
					case '_boo':
						suff = val == 't'; // true if 't' else false;
						break;
					case '_wpg':
						suff = val.fulltext;
						break;
					case '_dat':
						// val.raw is also available but the unconventional format makes it a lot less convenient.
						suff = val.timestamp;
						break;
					case '_qty':
						suff = val.value + ' ' + val.unit;
						break;
					case '_mlt_rec':
						// I doubt this is used anywhere, but it's not too hard to support.
						suff = val.Text.item[0];
						break;
					case '_ref_rec':
						// not supported; references contain too many properties.
						break;
					default:
						suff = val;
				}
			} catch(e) {
				console.error(e); // show error but ignore. Something is wrong in SMW query/api.
			}
			if (suff) {
				plist.push(name + '|' + suff);
			} else {
				plist.push(name);
			}
		}
		$('#articleList').val($.trim($('#articleList').val()) + '\n' + plist.join('\n'));
		JWB.pageCount();
		JWB.pl.stop(); // if JWB.pl.done == true show stopped interface. Otherwise mark as done.
		JWB.pl.done = true;
	});
}

//JWB.pl.getList(['wr'], ['watchlistraw'], {}) for watchlists
JWB.pl.generate = function() {
	var $fields = $('#pagelistPopup fieldset').not('[disabled]');
	$('#pagelistPopup').find('button[type="submit"]').append('<img src="//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif" width="15" height="15" alt="'+JWB.msg('status-alt')+'"/>');
	var abbrs = [],
		lists = [],
		data = {'continue': ''};
	$fields.each(function() {
		var list = $(this).find('legend input').attr('name');
		var abbr;
		if (list === 'linksto') { //Special case since this fieldset features 3 merged lists in 1 fieldset
			if (!$('[name="title"]').val()) return;
			$('[name="backlinks"], [name="embeddedin"], [name="imageusage"]').filter(':checked').each(function() {
				var val = this.value;
				abbrs.push(val);
				lists.push(this.name);
				data[val+'title'] = $('[name="title"]').val();
				data[val+'filterredir'] = $('[name="filterredir"]:checked').val();
				if ($('[name="redirect"]').prop('checked')) data[val+'redirect'] = true;
			});
		} else if (list === 'smwask') {
			data.ask = $(this).find('#smwquery').val();
		} else { //default input system
			if ($(this).find('#psstrict').prop('checked')) {
				// different list if prefixsearch is strict
				let $input = $(this).find('#psstrict')
				list = $input.attr('name');
				abbr = $input.val();
			} else {
				abbr = $(this).find('legend input').val();
			}
			lists.push(list);
			abbrs.push(abbr);
			$(this).find('input').not('legend input').each(function() {
				if ((this.type === 'checkbox' || this.type === 'radio') && this.checked === false) return;
				if (this.id == 'psstrict') return; // ignore psstrict; it only affects how pssearch is handled
				var name, val;
				if (this.id == 'cmtitle') {
					// making sure the page has a Category: prefix, in case the user left it out
					let cgns = JWB.ns[14]['*']; // name for Category: namespace
					if (!this.value.startsWith(cgns+':')) {
						this.value = cgns+':'+this.value;
					}
				}
				if (this.id == 'pssearch' && this.name == 'apprefix') {
					// apprefix needs namespace separate from pagename
					name = this.name;
					let split = this.value.split(':')
					val = split[1] || split[0];
					let nsid = 0;
					if (split[1]) { // if a namespace is given
						for (let ns in JWB.ns) {
							if (JWB.ns[ns]['*'] == split[0]) {
								nsid = JWB.ns[ns].id;
								break;
							}
						}
					}
					data.apnamespace = nsid;
				} else {
					name = this.name;
					val = this.value;
				}
				if (data.hasOwnProperty(name)) {
					data[name] += '|'+val;
				} else {
					data[name] = val;
				}
			});
			console.log(abbrs, lists, data);
		}
	});
	if (abbrs.length || data.ask) JWB.pl.getList(abbrs, lists, data);
	else JWB.pl.stop();
};

/***** Setup functions *****/

JWB.setup.save = function(name) {
	name = name || prompt(JWB.msg('setup-prompt', JWB.msg('setup-prompt-store')), $('#loadSettings').val());
	if (name === null) return;
	var self = JWB.settings[name] = {
		string: {},
		bool: {},
		replaces: []
	};
	//inputs with a text value
	$('textarea, input[type="text"], input[type="number"], select').not('.replaces input, #editBoxArea, #settings *').each(function() {
		if (typeof $(this).val() == 'string') { 
			self.string[this.id] = this.value.replace(/\n{2,}/g,'\n');
		} else {
			self.string[this.id] = $(this).val();
		}
	});
	self.replaces = [];
	$('.replaces').each(function() {
		if ($(this).find('.replaceText').val() || $(this).find('.replaceWith').val()) {
			self.replaces.push({
				replaceText: $(this).find('.replaceText').val(),
				replaceWith: $(this).find('.replaceWith').val(),
				useRegex: $(this).find('.useRegex').prop('checked'),
				regexFlags: $(this).find('.regexFlags').val(),
				ignoreNowiki: $(this).find('.ignoreNowiki').prop('checked')
			});
		}
	});
	$('input[type="radio"], input[type="checkbox"]').not('.replaces input').each(function() {
		self.bool[this.id] = this.checked;
	});
	if (!$('#loadSettings option[value="'+name+'"]').length) {
		$('#loadSettings').append('<option value="'+name+'">'+name+'</option>');
	}
	$('#loadSettings').val(name);
	console.log(self);
};

JWB.setup.apply = function(name) {
	name = name && JWB.settings[name] ? name : 'default';
	var self = JWB.settings[name];
	$('#loadSettings').val(name);
	$('.replaces + .replaces').remove(); //reset find&replace inputs
	$('.replaces input[type="text"]').val('');
	$('.useRegex').each(function() {this.checked = false;});
	$('#pagelistPopup legend input').trigger('change'); //fix checked state of pagelist generating inputs
	for (var a in self.string) {
		$('#'+a).val(self.string[a]);
	}
	for (var b in self.bool) {
		$('#'+b).prop('checked', self.bool[b]);
	}
	var cur;
	for (var c=0;c<self.replaces.length;c++) {
		if ($('.replaces').length <= c) $('#moreReplaces')[0].click();
		cur = self.replaces[c];
		for (var d in cur) {
			if (cur[d] === true || cur[d] === false) {
				$('.replaces').eq(c).find('.'+d).prop('checked', cur[d]);
			} else {
				$('.replaces').eq(c).find('.'+d).val(cur[d]);
			}
		}
	}
	$('.useRegex, #containRegex,'+
	  '#pagelistPopup legend input,'+
	  '#viaJWB, #enableRETF').trigger('change'); //reset disabled inputs
};

JWB.setup.getObj = function() {
	var settings = [];
	for (var i in JWB.settings) {
		if (i != '_blank') {
			settings.push('"' + i + '": ' + JSON.stringify(JWB.settings[i]));
		}
	}
	return '{\n\t' + settings.join(',\n\t').split('{{subst:').join('{{#JWB-SAFESUBST:#') + '\n}';
};

JWB.setup.submit = function() {
	var name = prompt(JWB.msg('setup-prompt', JWB.msg('setup-prompt-save')), $('#loadSettings').val());
	if (name === null) return;
	if ($.trim(name) === '') name = 'default';
	JWB.setup.save(name);
	JWB.status('setup-submit');
	JWB.api.call({
		action: 'query',
		meta: 'tokens',
	}, function(response) {
		let edittoken = response.query.tokens.csrftoken;
		JWB.api.call({
			title: 'User:'+JWB.username+'/'+JWB.settingspage,
			summary: JWB.msg(['setup-summary', JWB.contentLang]),
			action: 'edit',
			token: edittoken,
			text: JWB.setup.getObj(),
			minor: true
		}, function(response) {
			JWB.status('done', true);
			JWB.log('edit', response.edit.title, response.edit.newrevid);
		});
	});
};

//TODO: use blob uri
JWB.setup.download = function() {
	var name = prompt(JWB.msg('setup-prompt', JWB.msg('setup-prompt-save')), $('#loadSettings').val());
	if (name === null) return;
	if ($.trim(name) === '') name = 'default';
	JWB.setup.save(name);
	JWB.status('setup-dload');
	var url = 'data:application/json;base64,' + btoa(unescape(encodeURIComponent(JWB.setup.getObj())));
	var elem = $('#download-anchor')[0];
	if (HTMLAnchorElement.prototype.hasOwnProperty('download')) { //use download attribute when possible, for its ability to specify a filename
		elem.href = url;
		elem.click();
		setTimeout(function() {elem.removeAttribute('href');}, 2000);
	} else { //fallback to iframes for browsers with no support for download="" attributes
		elem = $('#download-iframe')[0];
		elem.src = url.replace('application/json', 'application/octet-stream');
		setTimeout(function() {elem.removeAttribute('src');}, 2000);
	}
	JWB.status('done', true);
};

JWB.setup.import = function(e) {
	e.preventDefault();
	file = (e.dataTransfer||this).files[0];
	if ($(this).is('#import')) { //reset input
		this.outerHTML = this.outerHTML;
		$('#import').change(JWB.setup.import);
	}
	if (!window.hasOwnProperty('FileReader')) {
		alert(JWB.msg('old-browser'));
		JWB.status('old-browser', '<a target="_blank" href="'+JWB.index_php+'?title=Special:MyPage/'+JWB.settingspage+'">/'+JWB.settingspage+'</a>');
		return;
	}
	if (file.name.split('.').pop().toLowerCase() !== 'json') {
		alert(JWB.msg('not-json'));
		return;
	}
	JWB.status('Processing file');
	var reader = new FileReader();
	reader.readAsText(file);
	reader.onload = function(e) {
		JWB.status('done', true);
		try {
			//Exclusion regex based on http://stackoverflow.com/a/23589204/1256925
			//Removes all JS comments from the file, except when they're between quotes.
			var c = reader.result;
			var data = JSON.parse(c.replace(/("[^"]*")|(\/\*[\w\W]*\*\/|\/\/[^\n]*)/g, function(match, g1, g2) {
				if (g1) return g1;
			}));
		} catch(e) {
			alert(JWB.msg('json-err', e.message, JWB.msg('json-err-upload')));
			console.log(e); //also log the error for further info
			return;
		}
		JWB.setup.extend(data);
	};
	
	JWB.status('Processing file');
};

JWB.setup.load = function() {
	JWB.status('setup-load');
	var user = JWB.username||mw.config.get('wgUserName');
	var oldtitle = "User:" + user + '/'+JWB.settingspage; // page title for what was used before version 4.0
	var newtitle = "User:" + user + '/JWB-settings.json'; // new page title for all settings pages.
	var titles = oldtitle;
	// if the old title isn't JWB-settings.json, also query the new title.
	if (oldtitle !== newtitle && JWB.hasJSON) {
		titles += '|' + newtitle;
	}
	JWB.api.call({
		action: 'query',
		titles: titles,
		prop: 'info|revisions',
		meta: 'tokens',
		rvprop: 'content',
		indexpageids: true
	}, function(response) {
		if (JWB === false) return; //user is not allowed to use JWB
		var firstrun = !JWB.setup.initialised;
		JWB.setup.initialised = true;
		var edittoken = response.query.tokens.csrftoken;

		// determine correct page to get settings from
		var pages = response.query.pages,
			ids = response.query.pageids;
		var page, exists = true;
		if (ids.length == 2) {
			var page0 = pages[ids[0]],
				page1 = pages[ids[1]];
			var oldpage, newpage;
			if (page0.title == oldtitle) {
				oldpage = page0;
				newpage = page1;
			} else {
				oldpage = page1;
				newpage = page0;
			}
			if (oldpage.missing === undefined && oldpage.redirect === undefined) {
				// old page exists and is not a redirect
				if (newpage.missing === undefined) {
					// both old AND new page exist; throw error and load neither page.
					let jsredir = "https://www.mediawiki.org/wiki/Help:Redirects#JavaScript_page_redirect";
					prompt(JWB.msg('duplicate-settings', oldtitle, newtitle, jsredir), jsredir);
					exists = false;
				} else {
					// old page exists but new page doesn't; move the page to the new location.
					JWB.setup.moveNew(oldtitle, newtitle, edittoken);
					JWB.settingspage = 'JWB-settings.json';
					return;
				}
			} else {
				// Old page either doesn't exist or is a redirect. Don't bother with it.
				page = newpage;
				exists = (page.missing === undefined);
				JWB.settingspage = 'JWB-settings.json';
			}
		} else {
			page = pages[ids[0]];
			exists = (page.missing === undefined);
		}
		if (!exists) {
			// settings page does not exist; don't load anything
			if (JWB.allowed && firstrun) JWB.setup.save('default'); //this runs when this callback returns after the init has loaded.
			return;
		}
		var data = page.revisions[0]['*'].split('{{#JWB-SAFESUBST:#').join('{{subst:');
		if (!data) {
			// settings page is empty; don't load anything.
			if (JWB.allowed && firstrun) JWB.setup.save('default'); //this runs when this callback returns after the init has loaded.
			return;
		}
		try {
			data = JSON.parse(data);
		} catch(e) {
			alert(JWB.msg('json-err', e.message, JWB.msg('json-err-page', JWB.settingspage)) || 'JSON error:\n'+e.message);
			JWB.setup.save('default');
			return;
		}
		JWB.setup.extend(data);
		JWB.status('done', true);
	});
};

JWB.setup.moveNew = function(from, to, token) {
	(new mw.Api()).post({
		action: 'move',
		from: from,
		to: to,
		token: token,
		reason: JWB.msg(['setup-move-summary', JWB.contentLang]),
		noredirect: true, // if possible, suppress redirects; the old page will no longer be needed if the new page exists.
		movesubpages: true, // if any
		movetalk: true, // if any
		ignorewarnings: true,
	}).done(function(response) {
		if (response.error === undefined) {
			JWB.log('move', from, to);
			JWB.settingspage = to.split('/')[1];
			alert(JWB.msg('moved-settings', from, to, JWB.msg('tab-log')));
			JWB.setup.load(); // load settings from newly moved page.
		}
	});
}

JWB.setup.extend = function(obj) {
	$.extend(JWB.settings, obj);
	if (!JWB.settings.hasOwnProperty('default')) {
		JWB.setup.save('default');
	}
	for (var i in JWB.settings) {
		if ($('#loadSettings').find('option[value="'+i+'"]').length) continue;
		$('#loadSettings').append('<option value="'+i+'">'+i+'</option>');
	}
	JWB.setup.apply($('#loadSettings').val());
};

JWB.setup.del = function() {
	var name = $('#loadSettings').val();
	if (name === '_blank') return alert(JWB.msg('setup-delete-blank'));
	var temp = {};
	temp[name] = JWB.settings[name];
	JWB.setup.temp = $.extend({}, temp);
	delete JWB.settings[name];
	$('#loadSettings').val('default');
	if (name === 'default') {
		JWB.setup.apply('_blank');
		JWB.setup.save('default');
		JWB.status(['del-default', '<a href="javascript:JWB.setup.undelete();">'+JWB.msg('status-del-undo')+'</a>'], true);
	} else {
		$('#loadSettings').find('[value="'+name+'"]').remove();
		JWB.setup.apply();
		JWB.status(['del-setup', name, '<a href="javascript:JWB.setup.undelete();">'+JWB.msg('status-del-undo')+'</a>'], true);
	}
};
JWB.setup.undelete = function() {
	JWB.setup.extend(JWB.setup.temp);
	JWB.status('done', true);
};

/***** Main other functions *****/

//Show status message status-`action`, or status-`action[0]` with arguments `action[1:]`
JWB.status = function(action, done) {
	if (JWB.bot && $('#autosave').prop('checked') && !JWB.isStopped) {
		$('#summary, .editbutton, #movePage, #deletePage, #protectPage, #skipPage').prop('disabled', true); //Disable summary when auto-saving
	} else {
		$('#summary, .editbutton, #movePage, #deletePage, #protectPage, #skipPage').prop('disabled', !done); //Disable box when not done (so busy loading). re-enable when done loading.
	}
	var status;
	if (action instanceof Array) {
		action[0] = 'status-'+action[0];
		status = JWB.msg.apply(this, action)
	} else {
		status = JWB.msg('status-'+action);
	}
	if (status === false) return;
	if (status) {
		if (!done) { //spinner if not done
			status += ' <img src="//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif" width="15" height="15" alt="'+JWB.msg('status-alt')+'"/>';
		}
	} else {
		status = action;
	}
	$('#status').html(status);
	JWB.pageCount();
	return action=='done';
};

JWB.pageCount = function() {
	if (JWB.allowed === false||!$('#articleList').length) return;
	$('#articleList').val(($('#articleList').val()||'').replace(/(^[ \t]*$\n)*/gm, ''));
	JWB.list = $('#articleList').val().split('\n');
	var count = JWB.list.length;
	if (count === 1 && JWB.list[0] === '') count = 0;
	$('#totPages').html(count);
};

//Perform all specified find&replace actions
JWB.replace = function(input, callback) {
	JWB.status('replacing');
	if (!JWB.worker.isWorking() && JWB.worker.supported) {
		// if the worker is not already working, then re-init to make sure we've not got any broken leftovers from the previous page
		JWB.worker.init();
	}
	JWB.newContent = input;
	JWB.pageCount();
 	var varOffset = JWB.list[0].indexOf('|') !== -1 ? JWB.list[0].indexOf('|') + 1 : 0;
 	JWB.page.pagevar = JWB.list[0].substr(varOffset);
	$('.replaces').each(function() {
		var $this = $(this);
		var replaceText = $this.find('.replaceText').val(),
			replaceWith = $this.find('.replaceWith').val();
		if (replaceText.length == 0 && replaceWith.length == 0) return; // don't bother replacing 2 empty strings.
		var regexFlags = $this.find('.regexFlags').val();
		var replace = replaceText.replace(/\$x/gi, JWB.page.pagevar).replace(/\\{2}/g, '\\').replace(/\\n/g,'\n') || '$';
		var useRegex = replaceText.length == 0 || $this.find('.useRegex').prop('checked');
		if (useRegex && regexFlags.indexOf('_') !== -1) {
			replace = replace.replace(/[ _]/g, '[ _]'); //replaces any of [Space OR underscore] with a match for spaces or underscores.
			replace = replace.replace(/(\[[^\]]*)\[ _\]/g, '$1 _'); //in case a [ _] was placed inside another [] match, remove the [].
			regexFlags = regexFlags.replace('_', '');
		}
		//apply replaces where \n and \\ work in both regular text and regex mode.
		var rWith = replaceWith.replace(/\$x/gi, JWB.page.pagevar).replace(/\\{2}/g, '\\').replace(/\\n/g,'\n');
		if (rWith.length === 0 && replace === '$') return;
		try {
			let replaceDone = function(result, err) {
				console.log('done replacing', result, err);
				if (err === undefined) {
					JWB.newContent = result;
					if (JWB.worker.queue.length == 0 && JWB.worker.supported) {
						// all workers are done
						JWB.status('done', true);
						callback(JWB.newContent);
					}
				} else if (err == 'Timeout exceeded') {
					if (JWB.worker.queue.length == 0 && JWB.worker.supported) {
						// all workers have exceeded their time and/or have finished
						JWB.status('done', true);
						callback(JWB.newContent); // newContent remains unmodified due to timeout.
					}
				}
			}
			if ($this.find('.ignoreNowiki').prop('checked')) {
				if (!useRegex) {
					replace = replace.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
					regexFlags = 'g';
				}
				JWB.worker.unparsedReplace("~"+"~~JWB.newContent", replace, regexFlags, rWith, replaceDone);
			} else if (useRegex) {
				JWB.worker.replace("~"+"~~JWB.newContent", replace, regexFlags, rWith, replaceDone);
			} else {
				JWB.newContent = JWB.newContent.split(replace).join(rWith); //global replacement without having to escape all special chars.
			}
		} catch(e) {
			console.log('Regex error:', e)
			JWB.stop();
			return JWB.status('regex-err', false);
		}
	});
	if ($('#enableRETF').prop('checked')) {
		JWB.newContent = RETF.replace(JWB.newContent);
	}
	if (!JWB.worker.isWorking()) {
		// no workers were called
		JWB.status('done', true);
		callback(JWB.newContent);
	}
};

JWB.skipRETF = function() {
	if (!$('#enableRETF').prop('checked')) return; // RETF is not enabled to begin with
	if (JWB.isStopped === true) return; // don't mess with the edit box when stopped
	$('#enableRETF').prop('checked', false);
	JWB.replace(JWB.page.content, function(newContent) {
		JWB.editPage(newContent);
		JWB.updateButtons();
		$('#enableRETF').prop('checked', true);
	});
}

// Edit the current page and pre-fill the newContent.
JWB.editPage = function(newContent) {
	$('#editBoxArea').val(newContent);
	$('#currentpage').html(JWB.msg('editbox-currentpage', JWB.page.name, encodeURIComponent(JWB.page.name)));
	if ($('#preparse').prop('checked')) {
		$('#articleList').val($.trim($('#articleList').val()) + '\n' + JWB.list[0]); //move current page to the bottom
		JWB.next();
		return;
	} else if (JWB.bot && $('#autosave').prop('checked')) {
		JWB.api.diff(function() {
			//timeout will take #throttle's value * 1000, if it's a number above 0. Currently defaults to 0.
			setTimeout(JWB.api.submit, Math.max(+$('#throttle').val() || 0, 0) * 1000, JWB.page.name);
		});
	} else {
		JWB.api.diff();
	}
}

//Adds a line to the logs tab.
JWB.log = function(action, page, info) {
	var d = new Date();
	var pagee = encodeURIComponent(page);
	var extraInfo = '', actionStat = '';
	switch (action) {
		case 'edit':
			if (typeof info === 'undefined') {
				action = 'null-edit';
				actionStat = 'nullEdits';
			} else {
				extraInfo = ' (<a target="_blank" href="'+JWB.index_php+'?title='+pagee+'&diff='+info+'">diff</a>)';
				actionStat = 'pagesSaved';
			}
			break;
		case 'nobots':
			action = 'bot-skip';
			extraInfo = ' (<a target="_blank" href="https://en.wikipedia.org/wiki/Template:Bots">{{bots}}</a>)';
			// no break;
		case 'skip':
			actionStat = 'pagesSkipped';
			break;
		case 'move':
			extraInfo = ' to <a target="_blank" href="/wiki/'+encodeURIComponent(info)+'" title="'+info+'">'+info+'</a>';
			break;
		case 'protect':
			extraInfo = info;
			break;
	}
	actionStat = '#' + (actionStat || 'otherActions');
	$(actionStat).html(+$(actionStat).html() + 1);
	$('#actionlog tbody')
		.append('<tr>'+
			'<td>'+(JWB.fn.pad0(d.getHours())+':'+JWB.fn.pad0(d.getMinutes())+':'+JWB.fn.pad0(d.getSeconds()))+'</td>'+
			'<th>'+action+'</th>'+
			'<td><a target="_blank" href="/wiki/'+pagee+'" title="'+page+'">'+page+'</a>'+ extraInfo +'</td>'+
		'</tr>')
		.parents('.JWBtabc').scrollTop($('#actionlog tbody').parents('.JWBtabc')[0].scrollHeight);
};

//Move to the next page in the list
JWB.next = function(nextPage) {
	// cancel any still ongoing regex match/replace functions, since we're moving on to another page.
	JWB.worker.cancelAll();
	if ($.trim(nextPage) && !$('#skipAfterAction').prop('checked')) {
		nextPage = $.trim(nextPage) + '\n';
	} else {
		nextPage = '';
	}
	$('#articleList').val($('#articleList').val().replace(/^.*\n?/, nextPage));
	JWB.list.splice(0,1);
	JWB.pageCount();
	JWB.api.get(JWB.list[0].split('|')[0]);
};

//Stop everything, reset inputs and editor
JWB.stop = function() {
	console.trace('stopped');
	$('#stopbutton,'+
	  '.editbutton,'+
	  '#watchNow,'+
	  '.JWBtabc[data-tab="2"] .editbutton,'+
	  '#watchNow'+
	  '.JWBtabc[data-tab="4"] button,'+
	  '#skipRETF').prop('disabled', true);
	$('#startbutton, #articleList,'+
	  '.JWBtabc[data-tab="1"] button,'+
	  '#replacesPopup button,'+
	  '#replacesPopup input,'+
	  '.JWBtabc input, select').prop('disabled', false);
	$('#resultWindow').html('');
	$('#editBoxArea').val('');
	$('#currentpage').html(JWB.msg('editbox-currentpage', ' ', ' '));
	JWB.pl.done = true;
	JWB.pl.stop();
	JWB.status('done', true);
	JWB.isStopped = true;
};

//Start AutoWikiBrowsing
JWB.start = function() {
	JWB.pageCount();
	if (JWB.list.length === 0 || (JWB.list.length === 1 && !JWB.list[0])) {
		alert(JWB.msg('no-pages-listed'));
	} else if ($('#skipNoChange').prop('checked') && !$('.replaceText').val() && !$('.replaceWith').val() && !$('#enableRETF').prop('checked')) {
		alert(JWB.msg('infinite-skip-notice'));
	} else {
		JWB.isStopped = false;
		if ($('#preparse').prop('checked')) {
			if (!$('#articleList').val().match('#PRE-PARSE-STOP')) {
				$('#articleList').val($.trim($('#articleList').val()) + '\n#PRE-PARSE-STOP'); //mark where to stop pre-parsing
			}
		} else {
			$('#preparse-reset').click();
		}
		$('#stopbutton, .editbutton, #watchNow, .JWBtabc[data-tab="2"] button, .JWBtabc[data-tab="4"] button, #skipRETF').prop('disabled', false);
		$('#startbutton, #articleList, .JWBtabc[data-tab="1"] button, #replacesPopup button, #replacesPopup input, .JWBtabc input, select').prop('disabled', true);
		if (!JWB.bot || !$('#autosave').prop('checked')) {
			// keep summary / watchlist options enabled when not in autosave mode
			$('#minorEdit, #summary, #viaJWB, #watchPage').prop('disabled', false);
		}
		JWB.api.get(JWB.list[0].split('|')[0]);
	}
};

JWB.updateButtons = function() {
	if (!JWB.page.exists && $('#deletePage').is('.delete')) {
		$('#deletePage').removeClass('delete').addClass('undelete').html('Undelete');
		JWB.fn.blink('#deletePage'); //Indicate the button has changed
	} else if (JWB.page.exists && $('#deletePage').is('.undelete')) {
		$('#deletePage').removeClass('undelete').addClass('delete').html('Delete');
		JWB.fn.blink('#deletePage'); //Indicate the button has changed
	}
	if (!JWB.page.exists) {
		$('#movePage').prop('disabled', true);
	} else {
		$('#movePage').prop('disabled', false);
	}
	$('#watchNow').html( JWB.msg('watch-' + (JWB.page.watched ? 'remove' : 'add')) );
};

/***** Web Worker functions *****/
JWB.worker.supported = !!window.Worker; // if window.Worker exists, we can use workers. Unless CSP blocks us.
JWB.worker.queue = [];

// Load function required to properly load the worker, since directly using `new Worker(url)` for cross-origin URLs does not work even with CORS/CSP rules all allowing it.
// See https://stackoverflow.com/q/66188950/1256925 for this exact question
JWB.worker.load = function(callback) {
	if (JWB.worker.blob) return callback(); // already successfully built
	$.getScript(JWB.imports['worker.js'], function() {
		// Firefox does not understand try..catch for content security policy violations, so define the worker functions regardless of the blob support.
		JWB.worker.functions = JWB.worker.function();
		// the loaded script just defined JWB.worker.function; convert it to a blob url
		// Based on https://stackoverflow.com/a/33432215/1256925
		if (JWB.worker.supported) try {
			let blob = new Blob(['('+JWB.worker.function.toString()+')()'], {type: 'text/javascript'});
			JWB.worker.blob = URL.createObjectURL(blob);
			callback();
		} catch(e) {
			if (e.code == 18) {
				JWB.worker.supported = false;
			}
		}
	});
}

// Create a worker to be able to preform regex operations without hanging the current process.
// Based on https://stackoverflow.com/q/66153487/1256925
JWB.worker.init = function() {
	JWB.worker.load(function() {
		JWB.worker.worker = new Worker(JWB.worker.blob);
		JWB.worker.callback = undefined; // explicitly set to the implicit value of undefined.
		JWB.worker.timeout = 0;
		JWB.worker.queue = [];
		JWB.worker.worker.onmessage = function(e) {
			clearTimeout(JWB.worker.timeout);
			JWB.worker.timeout = 0;
			if (JWB.isStopped) {
				// we're stopped; clear the queue and stop.
				JWB.worker.queue = [];
			} else if (JWB.worker.callback !== undefined) {
				JWB.worker.callback(e.data.result, e.data.err);
			} else {
				console.error("Worker finished without callback set:", e.data, e);
			}
			JWB.worker.next(true);
		}
	});
};

// Boolean; check if the worker is currently occupied. 
JWB.worker.isWorking = function() {
	return JWB.worker.callback !== undefined;
};

// Cancel current worker's task (e.g. due to timeout)
JWB.worker.terminate = function() {
	console.log('terminating');
	let w = JWB.worker;
	w.worker.terminate();
	w.callback(undefined, 'Timeout exceeded');
	let queue = w.queue; // save old queue
	w.init(); // re-init this worker, since the previous one is presumed dead (and terminated).
	w.queue = queue; // restore queue
};

// Cancel all workers (e.g. due to no longer needing the worker's queued services)
JWB.worker.cancelAll = function() {
	JWB.worker.queue = [];
	if (JWB.worker.worker) JWB.worker.worker.terminate(); // do not call the callback.
}

// Set worker to work, or queue the worker task.
JWB.worker.do = function(msg, callback) {
	if (JWB.worker.isWorking()) {
		JWB.worker.queue.push({msg: msg, callback: callback});
	} else {
		var timelimit = parseInt($('#timelimit').val()) || 3000;
		JWB.worker.callback = callback;
		// Expand "JWB.string" into JWB['string']; to allow the string to be loaded at execution time instead of queue time.
		// Start with 3x ~ because that cannot exist as the start of an actual page
		if (msg.str && msg.str.indexOf('~'+'~~JWB.') === 0) msg.str = JWB[msg.str.substr(7)]; // For now, 1-deep expansion is sufficient.
		JWB.worker.worker.postMessage(msg);
		JWB.worker.timeout = setTimeout(function() {
			if (!JWB.worker.isWorking()) {
				console.error('Worker error');
				JWB.worker.next(true);
				return;
			}
			JWB.worker.terminate();
			JWB.worker.next(true);
		}, timelimit);
	}
};

// Execute the next task in the queue
JWB.worker.next = function(force = false) {
	if (force) {
		// force means the function that's calling next() has handled the previous worker task. Clean up after it.
		JWB.worker.callback = undefined;
	} else if (JWB.worker.isWorking()) {
		// still working and the calling function did not specify proper exit of the previous task yet.
		return false;
	}
	if (JWB.worker.queue.length === 0) return true;
	var q = JWB.worker.queue.shift();
	JWB.worker.do(q.msg, q.callback);
};

/***** Functions using workers *****/
JWB.worker.match = function(str, pattern, flags, callback) {
	if (JWB.worker.supported) {
		JWB.worker.do({cmd: 'match', str, pattern, flags}, callback);
	} else {
		if (str && str.indexOf('~'+'~~JWB.') === 0) str = JWB[str.substr(7)]; // For now, 1-deep expansion is sufficient.
		JWB.worker.functions.match(str, pattern, flags, callback);
	}
};

JWB.worker.replace = function(str, pattern, flags, rWith, callback) {
	if (JWB.worker.supported) {
		JWB.worker.do({cmd: 'replace', str, pattern, flags, rWith}, callback);
	} else {
		if (str && str.indexOf('~'+'~~JWB.') === 0) str = JWB[str.substr(7)]; // For now, 1-deep expansion is sufficient.
		JWB.worker.functions.replace(str, pattern, flags, rWith, callback);
	}
};

JWB.worker.unparsedReplace = function(str, pattern, flags, rWith, callback) {
	if (JWB.worker.supported) {
		JWB.worker.do({cmd: 'unparsedreplace', str, pattern, flags, rWith}, callback);
	} else {
		if (str && str.indexOf('~'+'~~JWB.') === 0) str = JWB[str.substr(7)]; // For now, 1-deep expansion is sufficient.
		JWB.worker.functions.unparsedreplace(str, pattern, flags, rWith, callback);
	}
};

/***** General functions *****/
//Clear all existing timers to prevent them from getting errors
JWB.fn.clearAllTimeouts = function() {
	var i = setTimeout(function() {
		return void(0);
	}, 1000);
	for (var n=0;n<=i;n++) {
		clearTimeout(n);
		clearInterval(n);
	}
	console.log('Cleared all running intervals up to index',i);
};

//Filter an array to only contain unique values.
JWB.fn.uniques = function(arr) {
	var a = [];
	for (var i=0, l=arr.length; i<l; i++) {
		if (a.indexOf(arr[i]) === -1 && arr[i] !== '') {
			a.push(arr[i]);
		}
	}
	return a;
};

// code taken directly from [[Template:Bots]] and changed structurally (not functionally) for readability. The user in this case is "JWB" to deny this script.
// the user parameter is still kept as an optional parameter to maintain functionality as given on that template page.
JWB.fn.allowBots = function(text, user = "JWB") {
	var usr = user.replace(/([\(\)\*\+\?\.\-\:\!\=\/\^\$])/g, "\\$1");
	if (!new RegExp("\\{\\{\\s*(nobots|bots[^}]*)\\s*\\}\\}", "i").test(text))
		return true;
	if (new RegExp("\\{\\{\\s*bots\\s*\\|\\s*deny\\s*=\\s*([^}]*,\\s*)*" + usr + "\\s*(?=[,\\}])[^}]*\\s*\\}\\}", "i").test(text))
		return false
	else
		return new RegExp("\\{\\{\\s*((?!nobots)|bots(\\s*\\|\\s*allow\\s*=\\s*((?!none)|([^}]*,\\s*)*" + usr +
			"\\s*(?=[,\\}])[^}]*|all))?|bots\\s*\\|\\s*deny\\s*=\\s*(?!all)[^}]*|bots\\s*\\|\\s*optout=(?!all)[^}]*)\\s*\\}\\}", "i").test(text);
}


//Prepends zeroes until the number has the desired length of len (default 2)
JWB.fn.pad0 = function(n, len = 2) {
	n = n.toString();
	return n.length < len ? Array(len-n.length+1).join('0')+n : n;
};

JWB.fn.blink = function(el,t) {
	t=t?t:500;
	$(el).prop('disabled', true)
	.children().animate({opacity:'0.1'},t-100)
	.animate({opacity:'1'},t)
	.animate({opacity:'0.1'},t-100)
	.animate({opacity:'1'},t);
	setTimeout("$('"+el+"').prop('disabled', false)",t*4-400);
};

JWB.fn.setSelection = function(el, start, end, dir) {
    dir = dir||'none'; //Default value
    end = end||start; //If no end is specified, assume the caret is placed without creating text selection.
    if (el.setSelectionRange) {
        el.focus();
        el.setSelectionRange(start, end, dir);
    } else if (el.createTextRange) {
        var rng = el.createTextRange();
        rng.collapse(true);
        rng.moveStart('character', start);
        rng.moveEnd('character', end);
        rng.select();
    }
};

JWB.fn.scrollSelection = function(el, index) { //function to fix scrolling to selection - doesn't do that automatically.
	var newEl = document.createElement('textarea'); //create a new textarea to simulate the same conditions
	var elStyle = getComputedStyle(el);
	newEl.style.height = elStyle.height; //copy over size-influencing styles
	newEl.style.width = elStyle.width;
	newEl.style.lineHeight = elStyle.lineHeight;
	newEl.style.fontSize = elStyle.fontSize;
	newEl.value = el.value.substr(0,index);
	document.body.appendChild(newEl); //needs to be added to the HTML for the scrollHeight and clientHeight to work.
	if (newEl.scrollHeight != newEl.clientHeight) {
		el.scrollTop = newEl.scrollHeight - 2;
	} else {
		el.scrollTop = 0;
	}
	newEl.remove(); //clean up the mess I've made
};

//i18n function
JWB.msg = function(message) {
	var args = arguments;
	var lang = JWB.lang;
	if (typeof message === 'object') {
		lang = message[1];
		message = message[0];
	}
	if (lang == 'qqx') return message;
	if (!JWB.messages || !JWB.messages.en) return '\u29FC'+message+'\u29FD'; // same surrounding <> as used in mw.msg();
	var msg;
	if (JWB.messages.hasOwnProperty(lang) && JWB.messages[lang].hasOwnProperty(message)) {
		msg = JWB.messages[lang][message];
	} else {
		msg = (JWB.messages.en.hasOwnProperty(message)) ? JWB.messages.en[message] : '\u29FC'+message+'\u29FD';
	}
	msg = msg.replace(/\$(\d+)/g, function(match, num) {
		return args[+num] || match;
	});
	return msg;
};

/***** Init *****/

JWB.init = function() {
	console.log(JWB.messages.en, !!JWB.messages.en);
	JWB.setup.load();
	JWB.worker.init();
	JWB.fn.clearAllTimeouts();

	var findreplace = '<div class="replaces">'+
		'<label style="display:block;">'+JWB.msg('label-replace')+' <input type="text" class="replaceText"/></label>'+
		'<label style="display:block;">'+JWB.msg('label-rwith')+' <input type="text" class="replaceWith"/></label>'+
		'<div class="regexswitch">'+
			'<label><input type="checkbox" class="useRegex"> '+JWB.msg('label-useregex')+'</label>'+
			'<a class="re101" href="http://regex101.com/#javascript" target="_blank">?</a>'+
			'<label class="divisor" title="'+JWB.msg('tip-regex-flags')+'" style="display:none;">'+
				JWB.msg('label-regex-flags')+' <input type="text" class="regexFlags" value="g"/>'+ //default: global replacement
			'</label>'+
			'<br/>'+
		'</div>'+
		'<label title="'+JWB.msg('tip-ignore-comment')+'">'+
			'<input type="checkbox" class="ignoreNowiki"> '+JWB.msg('label-ignore-comment')+
		'</label>'+
	'</div>';
	
	var NSList = '<select multiple name="namespace" id="namespacelist">';
	for (var i in JWB.ns) {
		if (parseInt(i) < 0) continue; //No Special: or Media: in the list
		NSList += '<option value="'+JWB.ns[i].id+'" selected>'+(JWB.ns[i]['*'] || '('+JWB.msg('namespace-main')+')')+'</option>';
	}
	NSList += '</select>';
	
	/***** Interface *****/
	
	document.title = 'AutoWikiBrowser Script'+(document.title.split('-')[1] ? ' -'+document.title.split('-')[1] : '');
	$('body').html(
		'<article id="resultWindow"></article>'+
		'<main id="inputsWindow">'+
			'<div id="inputsBox">'+
				'<aside id="articleBox">'+
					'<b>'+JWB.msg('pagelist-caption')+'</b>'+
					'<textarea id="articleList"></textarea>'+
				'</aside>'+
				'<section id="tabs">'+
					'<nav class="tabholder">'+
						'<span class="JWBtab" data-tab="1">'+JWB.msg('tab-setup')+'</span> '+
						'<span class="JWBtab active" data-tab="2">'+JWB.msg('tab-editing')+'</span> '+
						'<span class="JWBtab" data-tab="3">'+JWB.msg('tab-skip')+'</span> '+
						(JWB.sysop?'<span class="JWBtab" data-tab="4">'+JWB.msg('tab-other')+'</span> ':'')+
						' <span class="JWBtab log" data-tab="5">'+JWB.msg('tab-log')+'</span> '+
					'</nav>'+
					'<section class="JWBtabc" data-tab="1"></section>'+
					'<section class="JWBtabc active" data-tab="2"></section>'+
					'<section class="JWBtabc" data-tab="3"></section>'+
					(JWB.sysop?'<section class="JWBtabc" data-tab="4"></section>':'')+
					'<section class="JWBtabc log" data-tab="5"></section>'+
					'<footer id="status">done</footer>'+
				'</section>'+
				'<aside id="editBox">'+
					'<b>'+JWB.msg('editbox-caption')+' - <span id="currentpage">'+JWB.msg('editbox-currentpage', ' ', ' ')+'</span></b>'+
					'<textarea id="editBoxArea"></textarea>'+
				'</aside>'+
			'</div>'+
		'</main>'+
		'<footer id="stats">'+
			JWB.msg('stat-pages')+' <span id="totPages">0</span>;&emsp;'+
			JWB.msg('stat-save')+' <span id="pagesSaved">0</span>;&emsp;'+
			JWB.msg('stat-null')+' <span id="nullEdits">0</span>;&emsp;'+
			JWB.msg('stat-skip')+' <span id="pagesSkipped">0</span>;&emsp;'+
			JWB.msg('stat-other')+' <span id="otherActions">0</span>;&emsp;'+
		'</footer>'+
		'<div id="overlay" style="display:none;"></div>'+
		'<section class="JWBpopup" id="replacesPopup" style="display:none;">'+
			'<button id="moreReplaces">'+JWB.msg('button-more-fields')+'</button>'+
			'<br>'+findreplace+
		'</section>'+
		'<section class="JWBpopup" id="pagelistPopup" style="display:none;">'+
			'<form action="#" id="pl-form"></form>'+
		'</section>'
	);
	
	$('.JWBtabc[data-tab="1"]').html(
		'<fieldset id="pagelist">'+
			'<legend>'+JWB.msg('label-pagelist')+'</legend>'+
			'<button id="removeDupes">'+JWB.msg('button-remove-dupes')+'</button> '+
			'<button id="sortArticles">'+JWB.msg('button-sort')+'</button>'+
			'<br>'+
			'<label title="'+JWB.msg('tip-preparse')+'">'+
				'<input type="checkbox" id="preparse"> '+JWB.msg('preparse')+
			'</label>'+
			'<span class="divisor"></span>'+
			'<button id="preparse-reset" title="'+JWB.msg('tip-preparse-reset')+'">'+JWB.msg('preparse-reset')+'</button>'+
			'<br>'+
			'<button id="pagelistButton">'+JWB.msg('pagelist-generate')+'</button>'+
		'</fieldset>'+
		'<fieldset id="settings">'+
			'<legend>'+JWB.msg('label-settings')+'</legend>'+
			'<button id="saveAs" title="'+JWB.msg('tip-store-setup')+'">'+JWB.msg('store-setup')+'</button>'+
			'<br>'+
			'<label>'+
				JWB.msg('load-settings') + ' '+
				'<select id="loadSettings">'+
					'<option value="default" selected>default</option>'+
					'<option value="_blank">'+JWB.msg('blank-setup')+'</option>'+
				'</select>'+
			'</label>'+
			'<span class="divisor"></span>'+
			'<button id="deleteSetup" title="'+JWB.msg('tip-delete-setup')+'">'+JWB.msg('delete-setup')+'</button>'+
			'<hr>'+
			'<button id="saveToWiki">'+JWB.msg('save-setup')+'</button>'+
			'<span class="divisor"></span>'+
			'<button id="download">'+JWB.msg('download-setup')+'</button>'+
			'<hr>'+
			'<label class="button" id="importLabel" title="'+JWB.msg('tip-import-setup')+'">'+
				'<input type="file" id="import" accept=".json">'+
				JWB.msg('import-setup')+
			'</label>'+
			'<span class="divisor"></span>'+
			'<button id="updateSetups" title="'+JWB.msg('tip-update-setup', JWB.settingspage)+'">'+JWB.msg('update-setup')+'</button>'+
			'<div id="downloads">'+
				'<a download="JWB-settings.json" target="_blank" id="download-anchor"></a>'+
				'<iframe id="download-iframe"></iframe>'+
			'</div>'+
		'</fieldset>'+
		'<fieldset id="limits">'+
			'<legend>'+JWB.msg('label-limits')+'</legend>'+
			'<label class="timelimit-label" title="'+JWB.msg('tip-time-limit')+'">'+
				JWB.msg('time-limit')+
				'<input type="number" id="timelimit" value="3000" min="0">'+
			'</label>'+
			'<label title="'+JWB.msg('tip-diff-size-limit')+'">'+
				JWB.msg('diff-size-limit')+
				'<input type="number" id="sizelimit" value="0" min="0">'+
			'</label>'+
		'</fieldset>'
	);
	$('.JWBtabc[data-tab="2"]').html(
		'<label class="minorEdit"><input type="checkbox" id="minorEdit" checked> '+JWB.msg('minor-edit')+'</label>'+
		'<label class="editSummary viaJWB">'+JWB.msg('edit-summary')+'<br/> <input class="fullwidth" type="text" id="summary" maxlength="500"></label>'+
		' <input type="checkbox" id="viaJWB" checked title="'+JWB.msg('tip-via-JWB')+'">'+
		'<select id="watchPage">'+
			'<option value="watch">'+JWB.msg('watch-watch')+'</option>'+
			'<option value="unwatch">'+JWB.msg('watch-unwatch')+'</option>'+
			'<option value="nochange" selected>'+JWB.msg('watch-nochange')+'</option>'+
			'<option value="preferences">'+JWB.msg('watch-preferences')+'</option>'+
		'</select>'+
		'<span class="divisor"></span>'+
		'<button id="watchNow" disabled accesskey="w">'+
			JWB.msg('watch-add')+
		'</button>'+
		'<br>'+
		(JWB.bot?
			'<label><input type="checkbox" id="autosave"> '+JWB.msg('auto-save')+'</label>'+
			'<label title="'+JWB.msg('tip-save-interval')+'" class="divisor">'+
				JWB.msg('save-interval', '<input type="number" min="0" value="0" style="width:50px" id="throttle" disabled>')+
			'</label>'+
			'<br>'
		:'')+
		'<span id="startstop">'+
			'<button id="startbutton" accesskey="a">'+JWB.msg('editbutton-start')+'</button>'+
			'<br>'+
			'<button id="stopbutton" disabled accesskey="q">'+JWB.msg('editbutton-stop')+'</button> '+
		'</span>'+
		'<button class="editbutton" id="skipButton" disabled accesskey="n">'+JWB.msg('editbutton-skip')+'</button>'+
		'<button class="editbutton" id="submitButton" disabled accesskey="s">'+JWB.msg('editbutton-save')+'</button>'+
		'<br>'+
		'<button class="editbutton" id="previewButton" disabled accesskey="p">'+JWB.msg('editbutton-preview')+'</button>'+
		'<button class="editbutton" id="diffButton" disabled accesskey="d">'+JWB.msg('editbutton-diff')+'</button>'+
		'<button id="replacesButton">'+JWB.msg('button-open-popup')+'</button>'+
		findreplace+
		'<hr>'+
		'<label><input type="checkbox" id="enableRETF"> '+
			JWB.msg('label-enable-RETF', 
				'<a href="/wiki/Project:AutoWikiBrowser/Typos" target="_blank">'+
					JWB.msg('label-RETF')+
				'</a>')+
		'</label>'+
		' <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Gnome-view-refresh.svg/20px-Gnome-view-refresh.svg.png"'+
		'id="refreshRETF" title="'+JWB.msg('tip-refresh-RETF')+'">'+
		'<br/>'+
		'<button id="skipRETF" title="'+JWB.msg('tip-skip-RETF')+'" disabled>'+JWB.msg('skip-RETF')+'</button>'
	);
	$('.JWBtabc[data-tab="3"]').html(
		'<fieldset>'+
			'<legend>'+JWB.msg('label-redirects')+'</legend>'+
			'<label title="'+JWB.msg('tip-redirects-follow')+'">'+
				'<input type="radio" class="redirects" value="follow" name="redir" id="redir-follow"> '+JWB.msg('redirects-follow')+' '+
			'</label>'+
			'<label title="'+JWB.msg('tip-redirects-skip')+'">'+
				 '<input type="radio" class="redirects" value="skip" name="redir" id="redir-skip"> '+JWB.msg('redirects-skip')+' '+
			'</label>'+
			'<label title="'+JWB.msg('tip-redirects-edit')+'">'+
				'<input type="radio" class="redirects" value="edit" name="redir" id="redir-edit" checked> '+JWB.msg('redirects-edit')+''+
			'</label>'+
		'</fieldset>'+
		'<fieldset>'+
			'<legend>'+JWB.msg('label-skip-when')+'</legend>'+
			'<label><input type="checkbox" id="skipNoChange"> '+JWB.msg('skip-no-change')+'</label>'+
			'<br>'+
			'<label><input type="radio" id="exists-yes" name="exists" value="yes"> '+JWB.msg('skip-exists-yes')+'</label>'+
			'<label><input type="radio" id="exists-no" name="exists" value="no" checked> '+JWB.msg('skip-exists-no')+'</label>'+
			'<label><input type="radio" id="exists-neither" name="exists" value="neither">'+JWB.msg('skip-exists-neither')+'</label>'+
			(JWB.sysop?'<br><label><input type="checkbox" id="skipAfterAction" checked> '+JWB.msg('skip-after-action')+'</label>':'')+
			'<hr/>'+
		'<label>'+JWB.msg('skip-contains')+' <input class="fullwidth" type="text" id="skipContains"></label>'+
		'<label>'+JWB.msg('skip-not-contains')+' <input class="fullwidth" type="text" id="skipNotContains"></label>'+
		'<div class="regexswitch">'+
			'<label><input type="checkbox" id="containRegex"> '+JWB.msg('label-useregex')+'</label>'+
			'<a class="re101" href="http://regex101.com/#javascript" target="_blank">?</a>'+
			'<label class="divisor" title="'+JWB.msg('tip-regex-flags')+'" style="display:none;">'+
				JWB.msg('label-regex-flags')+' <input type="text" id="containFlags"/>'+
			'</label>'+
		'</div>'+
		'<hr/>'+
		'<label title="'+JWB.msg('skip-cg-prefix')+'">'+JWB.msg('skip-category')+' <input class="fullwidth" type="text" id="skipCategories"></label>'+
		'</fieldset>'
	);
	if (JWB.sysop) $('.JWBtabc[data-tab="4"]').html(
		'<fieldset>'+
			'<legend>'+JWB.msg('move-header')+'</legend>'+
			'<label><input type="checkbox" id="suppressRedir"> '+JWB.msg('move-redir-suppress')+'</label>'+
			'<br>'+
			JWB.msg('move-also')+' '+
			'<label><input type="checkbox" id="movetalk"> '+JWB.msg('move-talk-page')+'</label> '+
			'<label><input type="checkbox" id="movesubpage"> '+JWB.msg('move-subpage')+'</label>'+
			'<br>'+
			'<label>'+JWB.msg('move-new-name')+' <input type="text" id="moveTo"></label>'+
		'</fieldset>'+
		'<fieldset>'+
		'<legend>'+JWB.msg('protect-header')+'</legend>'+
			JWB.msg('protect-edit')+
			' <select id="editProt">'+
				'<option value="all" selected>'+JWB.msg('protect-none')+'</option>'+
				'<option value="autoconfirmed">'+JWB.msg('protect-autoconf')+'</option>'+
				'<option value="sysop">'+JWB.msg('protect-sysop')+'</option>'+
			'</select> '+
			'<br>'+
			JWB.msg('protect-move')+
			' <select id="moveProt">'+
				'<option value="" selected>('+JWB.msg('protect-like-edit')+')</option>'+
				'<option value="all">'+JWB.msg('protect-none')+'</option>'+
				'<option value="autoconfirmed">'+JWB.msg('protect-autoconf')+'</option>'+
				'<option value="sysop">'+JWB.msg('protect-sysop')+'</option>'+
			'</select> '+
			'<br>'+
			JWB.msg('protect-upload')+
			' <select id="uploadProt">'+
				'<option value="" selected>('+JWB.msg('protect-like-edit')+')</option>'+
				'<option value="all">'+JWB.msg('protect-none')+'</option>'+
				'<option value="autoconfirmed">'+JWB.msg('protect-autoconf')+'</option>'+
				'<option value="sysop">'+JWB.msg('protect-sysop')+'</option>'+
			'</select> '+
			'<br>'+
			'<label>'+JWB.msg('protect-expiry')+' <input type="text" id="protectExpiry"/></label>'+
		'</fieldset>'+
		'<button id="movePage" disabled accesskey="m">'+JWB.msg('editbutton-move')+'</button> '+
		'<button id="deletePage" disabled accesskey="x">'+JWB.msg('editbutton-delete')+'</button> '+
		'<button id="protectPage" disabled accesskey="z">'+JWB.msg('editbutton-protect')+'</button> '+
		'<button id="skipPage" disabled title="['+JWB.tooltip+'n]">'+JWB.msg('editbutton-skip')+'</button>'
	);
	$('.JWBtabc[data-tab="5"]').html('<table id="actionlog"><tbody></tbody></table>');
	$('#pagelistPopup form').html(
		'<div id="ns-filter" title="'+JWB.msg('tip-ns-select')+'">' + JWB.msg('label-ns-select') + NSList + '</div>'+
		'<fieldset>'+
			'<legend><label><input type="checkbox" id="categorymembers" name="categorymembers" value="cm"> '+JWB.msg('legend-cm')+'</label></legend>'+
			'<label title="'+JWB.msg('tip-cm')+'">'+JWB.msg('label-cm')+' <input type="text" name="cmtitle" id="cmtitle"></label>'+
			'<div>'+JWB.msg('cm-include')+' '+
				'<label><input type="checkbox" id="cmtype-page" name="cmtype" value="page" checked> '+JWB.msg('cm-include-pages')+'</label>'+
				'<label><input type="checkbox" id="cmtype-subcg" name="cmtype" value="subcat" checked> '+JWB.msg('cm-include-subcgs')+'</label>'+
				'<label><input type="checkbox" id="cmtype-file" name="cmtype" value="file" checked> '+JWB.msg('cm-include-files')+'</label>'+
			'</div>'+
		'</fieldset>'+
		'<fieldset>'+
			'<legend><label><input type="checkbox" name="linksto" id="linksto"> '+JWB.msg('legend-linksto')+'</label></legend>'+
			'<label>'+JWB.msg('label-linksto')+' <input type="text" name="title" id="linksto-title"></label>'+
			'<div>'+JWB.msg('links-include')+' '+
				'<label><input type="checkbox" id="backlinks" name="backlinks" value="bl" checked> '+JWB.msg('links-include-links')+'</label>'+
				'<label><input type="checkbox" id="embeddedin" name="embeddedin" value="ei"> '+JWB.msg('links-include-templ')+'</label>'+
				'<label><input type="checkbox" id="imageusage" name="imageusage" value="iu"> '+JWB.msg('links-include-files')+'</label>'+
			'</div>'+
			'<div>'+JWB.msg('links-redir')+' '+
				'<label><input type="radio" id="rfilter-redir" name="filterredir" value="redirects"> '+JWB.msg('links-redir-redirs')+'</label>'+
				'<label><input type="radio" id="rfilter-nonredir" name="filterredir" value="nonredirects"> '+JWB.msg('links-redir-noredirs')+'</label>'+
				'<label><input type="radio" id="rfilter-all" name="filterredir" value="all" checked> '+JWB.msg('links-redir-all')+'</label>'+
			'</div>'+
			'<label title="'+JWB.msg('tip-link-redir')+'">'+
				'<input type="checkbox" name="redirect" value="true" checked id="linksto-redir"> '+JWB.msg('label-link-redir')+
			'</label>'+
		'</fieldset>'+
		'<fieldset>'+
			'<legend><label><input type="checkbox" id="prefixsearch" name="prefixsearch" value="ps"> '+JWB.msg('legend-ps')+'</label></legend>'+
			'<label>'+JWB.msg('label-ps')+' <input type="text" name="pssearch" id="pssearch"></label>'+
			'<label title="'+JWB.msg('tip-ps-strict')+'"><input type="checkbox" name="allpages" value="ap" id="psstrict" checked> '+JWB.msg('label-ps-strict')+'</label>'+
		'</fieldset>'+
		'<fieldset>'+
			'<legend><label><input type="checkbox" id="watchlistraw" name="watchlistraw" value="wr"> '+JWB.msg('legend-wr')+'</label></legend>'+
			JWB.msg('label-wr')+
		'</fieldset>'+
		'<fieldset>'+
			'<legend><label><input type="checkbox" id="proplinks" name="links" value="pl"> '+JWB.msg('legend-pl')+'</label></legend>'+
			'<label title="'+JWB.msg('tip-pl')+'">'+JWB.msg('label-pl')+' <input type="text" id="titles" name="titles"></label>'+
		'</fieldset>'+
		'<fieldset>'+
			'<legend><label><input type="checkbox" id="proplinks" name="search" value="sr"> '+JWB.msg('legend-sr')+'</label></legend>'+
			'<label title="'+JWB.msg('tip-sr')+'\n'+JWB.msg('placeholder-sr', 'insource:', 'intitle:')+'">'+
				JWB.msg('label-sr')+' <input type="text" id="srsearch" name="srsearch" placeholder="'+JWB.msg('placeholder-sr', 'insource:', 'intitle:')+'">'+
			'</label>'+
		'</fieldset>'+
		'<fieldset class="listSMW">'+
			'<legend><label><input type="checkbox" id="smwask" name="smwask" value="smw"> '+JWB.msg('legend-smw', JWB.msg('smw-slow'))+'</label></legend>'+
			'<textarea id="smwquery" name="smwquery" placeholder="'+JWB.msg('label-smw', '\n|limit=500')+'"></textarea>'+
		'</fieldset>'+
		'<button type="submit">'+JWB.msg('pagelist-generate')+'</button>'
	);
	if (JWB.hasSMW) {
		$('#pagelistPopup').addClass('hasSMW')
	}
	$('body').addClass('AutoWikiBrowser'); //allow easier custom styling of JWB.
	$('[accesskey]').each(function() {
		let lbl = this.accessKeyLabel || this.accessKey; // few browsers support accessKeyLabel, so fallback to accessKey.
		$(this).attr('title', '['+lbl+']');
	});
	
	/***** Setup *****/
	JWB.setup.save('_blank'); //default setup
	if (JWB.settings.hasOwnProperty('default')) {
		JWB.setup.apply();
	} else if (JWB.setup.initialised) {
		// If we already initialised, create the default settings profile.
		JWB.setup.save('default');
	}
	JWB.setup.extend({});

	/***** Event handlers *****/
	
	//Alert user when leaving the tab, to prevent accidental closing.
	onbeforeunload = function() {
		return "Closing this tab will cause you to lose all progress.";
	};
	ondragover = function(e) {
		e.preventDefault();
	};
	document.addEventListener("securitypolicyviolation", function(e) {
		console.log('violated CSP:', e);
		if (e.blockedURI == 'blob') {
			JWB.worker.supported = false; // tell the next JWB.worker.init() that it shouldn't even try.
		} else if (JWB && JWB.msg) {
			alert(JWB.msg('csp-error', e.violatedDirective))
		}
	})
	
	$('.JWBtab').click(function() {
		$('.active').removeClass('active');
		$(this).addClass('active');
		$('.JWBtabc[data-tab="'+$(this).attr('data-tab')+'"]').addClass('active');
	});
	
	function showRegexFlags() {
		// >>this<< is the element that's triggered
		$(this).parent().nextAll('label').toggle(this.checked);
	}
	$('body').on('change', '#useRegex, #containRegex, .useRegex', showRegexFlags);
	
	$('#preparse-reset').click(function() {
		$('#articleList').val($('#articleList').val().replace(/#PRE-PARSE-STOP/g,'').replace(/\n\n/g, '\n'));
	});
	$('#saveAs').click(function() {
		JWB.setup.save();
	});
	$('#loadSettings').change(function() {
		JWB.setup.apply(this.value);
	});
	$('#download').click(JWB.setup.download);
	$('#saveToWiki').click(JWB.setup.submit);
	$('#import').change(JWB.setup.import);
	ondrop = JWB.setup.import;
	$('#updateSetups').click(JWB.setup.load);
	$('#deleteSetup').click(JWB.setup.del);
	
	if (window.RETF) {
		$('#refreshRETF').click(RETF.load);
		$('#skipRETF').click(JWB.skipRETF)
		$('#enableRETF').change(function() {
			$('#skipRETF').css('visibility', this.checked ? 'visible' : 'hidden');
		});
	}

	$('#replacesButton, #pagelistButton').click(function() {
		var popup = this.id.slice(0, -6); //omits the 'Button' in the id by cutting off the last 6 characters
		$('#'+popup+'Popup, #overlay').show();
	});
	$('#overlay').click(function() {
		$('#replacesPopup, #pagelistPopup, #overlay').hide();
		JWB.pl.done = true;
		JWB.pl.stop();
	});
	$('#moreReplaces').click(function() {
		$('#replacesPopup').append(findreplace);
	});
	$('#replacesPopup').on('keydown', '.replaces:last', function(e) {
		if (e.which === 9) $('#moreReplaces')[0].click();
	});
	
	$('#pl-form').submit(function(e) {
		e.preventDefault();
		JWB.pl.generate();
		return false;
	});
	$('#pagelistPopup legend input').change(function() {
		//remove disabled attr when checked, add when not.
		$(this).parents('fieldset').find('input').not('legend input').prop('disabled', !this.checked);
		$(this).parents('fieldset').prop('disabled', !this.checked);
	}).trigger('change');
	$('#psstrict').change(function() {
		if (this.checked) {
			$('#pssearch').attr('name', 'apprefix');
		} else {
			$('#pssearch').attr('name', 'pssearch');
		}
	}).trigger('change');
	
	$('#resultWindow').on('click', 'tr[data-line]:not(.lineheader) *', function(e) {
		var line = +$(e.target).closest('tr[data-line]').data('line');
		var index = $('#editBoxArea').val().split('\n').slice(0, line-1).join('\n').length;
		$('#editBoxArea')[0].focus();
		JWB.fn.setSelection($('#editBoxArea')[0], index+1);
		JWB.fn.scrollSelection($('#editBoxArea')[0], index);
	});
	
	$('#removeDupes').click(function() {
		$('#articleList').val(JWB.fn.uniques($('#articleList').val().split('\n')).join('\n'));
		JWB.pageCount();
	});
	$('#sortArticles').click(function() {
		$('#articleList').val($('#articleList').val().split('\n').sort().join('\n'));
		JWB.pageCount();
	});
	
	$('#watchNow').click(JWB.api.watch);
	$('#autosave').change(function() {
		$('#throttle').prop('disabled', !this.checked);
	});
	
	$('#viaJWB').change(function() {
		$('#summary').parent('label')
			.toggleClass('viaJWB', this.checked)
			.attr('maxlength', 500 - this.checked*JWB.summarySuffix.length); // Change the max size of the allowed summary according to having a suffix or not.
	});
	$('#startbutton').click(JWB.start);
	$('#stopbutton').click(JWB.stop);
	$('#submitButton').click(JWB.api.submit);
	$('#previewButton').click(JWB.api.preview);
	$('#diffButton').click(JWB.api.diff);
	
	$('#skipButton, #skipPage').click(function() {
		JWB.log('skip', JWB.list[0].split('|')[0]);
		JWB.next();
	});
	
	if (JWB.sysop) {
		$('#movePage').click(function() {
			if ($('#moveTo').val().length === 0) {
				return alert(JWB.msg('alert-no-move'));
			}
			JWB.api.move();
		});
		$('#protectPage').click(JWB.api.protect);
		$('#deletePage').click(JWB.api.del);
	}
};

//Disable JWB altogether when it's loaded on a page other than Project:AutoWikiBrowser/Script. This script shouldn't be loaded on any other page in the first place.
if (JWB.allowed === false) JWB = false;