MediaWiki:Gadget-twinkleblock.js: Difference between revisions

Content deleted Content added
Repo at 65e9dea: Fix bug in block conversion display; Add additional partial blocking templates; disable partial block preset menu
deploy #2180: soft redirect to Special:Block for multi-blocked targets
 
(45 intermediate revisions by 5 users not shown)
Line 1:
// <nowiki>
 
(function() {
 
const api = new mw.Api();
(function($) {
let relevantUserName, blockedUserName, blockWindow;
 
const menuFormattedNamespaces = $.extend({}, mw.config.get('wgFormattedNamespaces'));
var api = new mw.Api(), relevantUserName;
var menuFormattedNamespaces = mw.config.get('wgFormattedNamespaces');
menuFormattedNamespaces[0] = '(Article)';
 
Line 17:
 
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.userIsInGroup('sysop') && mw.config.get('wgRelevantUserName')) {
if (Morebits.userIsSysop && relevantUserName && (!Morebits.ip.isRange(relevantUserName) || Morebits.ip.validCIDR(relevantUserName))) {
Twinkle.addPortletLink(Twinkle.block.callback, 'Block', 'tw-block', 'Block relevant user');
}
Line 24 ⟶ 26:
 
Twinkle.block.callback = function twinkleblockCallback() {
if (mw.config.get('wgRelevantUserName')relevantUserName === mw.config.get('wgUserName') &&
!confirm('You are about to block yourself! Are you sure you want to proceed?')) {
return;
}
 
var Window = new Morebits.simpleWindow(650, 530);
// need to be verbose about who we're blocking
Window.setTitle('Block or issue block template to ' + mw.config.get('wgRelevantUserName'));
Window.setScriptName('Twinkle');
Window.addFooterLink('Block templates', 'Template:Uw-block/doc/Block_templates');
Window.addFooterLink('Block policy', 'WP:BLOCK');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#block');
 
Twinkle.block.currentBlockInfo = undefined;
Line 41 ⟶ 35:
Twinkle.block.field_template_options = {};
 
var formblockWindow = new Morebits.quickFormSimpleWindow(Twinkle.block.callback.evaluate650, 530);
// need to be verbose about who we're blocking
var actionfield = form.append({
blockWindow.setTitle('Block or issue block template to ' + relevantUserName);
blockWindow.setScriptName('Twinkle');
blockWindow.addFooterLink('Block templates', 'Template:Uw-block/doc/Block_templates');
blockWindow.addFooterLink('Block policy', 'WP:BLOCK');
blockWindow.addFooterLink('Block prefs', 'WP:TW/PREF#block');
blockWindow.addFooterLink('Twinkle help', 'WP:TW/DOC#block');
blockWindow.addFooterLink('Give feedback', 'WT:TW');
 
// Always added, hidden later if actual user not blocked
blockWindow.addFooterLink('Unblock this user', 'Special:Unblock/' + relevantUserName, true);
 
const form = new Morebits.QuickForm(Twinkle.block.callback.evaluate);
const actionfield = form.append({
type: 'field',
label: 'Type of action'
Line 61 ⟶ 68:
value: 'partial',
tooltip: 'Enable partial blocks and partial block templates.',
checked: Twinkle.getPref('defaultToPartialBlocks') // Overridden if already blocked
},
{
Line 67 ⟶ 74:
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: true
checked: !Morebits.ip.isRange(relevantUserName),
disabled: Morebits.ip.isRange(relevantUserName)
}
]
});
 
/*
Add option for IPv6 ranges smaller than /64 to upgrade to the 64
CIDR ([[WP:/64]]). This is one of the few places where we want
wgRelevantUserName since this depends entirely on the original user.
In theory, we shouldn't use Morebits.ip.get64 here since since we want
to exclude functionally-equivalent /64s. That'd be:
// 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.
*/
const sixtyFour = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
if (sixtyFour && sixtyFour !== mw.config.get('wgRelevantUserName')) {
const 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')
}]
});
}
 
form.append({ type: 'field', label: 'Preset', name: 'field_preset' });
Line 78 ⟶ 125:
form.append({ type: 'submit' });
 
varconst result = form.render();
WindowblockWindow.setContent(result);
WindowblockWindow.display();
result.root = result;
 
Twinkle.block.fetchUserInfo(function() => {
// Toggle initial partial state depending on prior block type,
// will override the defaultToPartialBlocks pref
if (blockedUserName === relevantUserName) {
$(result).find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
 
// clean up preset data (defaults, etc.), done exactly once, must be before Twinkle.block.callback.change_action is called
Twinkle.block.transformBlockPresets();
 
// init the controls after user and block info have been fetched
varconst evt = document.createEvent('Event');
evt.initEvent('change', true, true);
 
result.actiontype[0].dispatchEvent(evt);
if (result.block64 && result.block64.checked) {
// Calls the same change_action event once finished
result.block64.dispatchEvent(evt);
} else {
result.actiontype[0].dispatchEvent(evt);
}
});
};
 
// Store fetched user data, only relevant if switching IPv6 to a /64
Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) {
Twinkle.block.fetchedData = {};
// Processes the data from a query response, separated from
// Twinkle.block.fetchUserInfo to allow reprocessing of already-fetched data
Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) {
let blockinfo = data.query.blocks[0];
// Soft redirect to Special:Block if the user is multi-blocked (#2178)
if (blockinfo && data.query.blocks.length > 1) {
// Remove submission buttons.
$(blockWindow.content).dialog('widget').find('.morebits-dialog-buttons').empty();
Morebits.Status.init(blockWindow.content.querySelector('form'));
Morebits.Status.warn(
`This target has ${data.query.blocks.length} active blocks`,
`Multiblocks is not supported by Twinkle. Use [[Special:Block/${relevantUserName}]] instead.`
);
return;
}
const 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];
}
// Cache response, used when toggling /64 blocks
Twinkle.block.fetchedData[userinfo.name] = data;
 
Twinkle.block.isRegistered = !!userinfo.userid;
api.get({
if (Twinkle.block.isRegistered) {
Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map((e) => e.group).includes('bot');
} else {
Twinkle.block.userIsBot = false;
}
 
if (blockinfo) {
// handle frustrating system of inverted boolean values
blockinfo.disabletalk = blockinfo.allowusertalk === undefined;
blockinfo.hardblock = blockinfo.anononly === undefined;
}
// will undefine if no blocks present
Twinkle.block.currentBlockInfo = blockinfo;
blockedUserName = Twinkle.block.currentBlockInfo && Twinkle.block.currentBlockInfo.user;
 
// Toggle unblock link if not the user in question; always first
const unblockLink = document.querySelector('.morebits-dialog-footerlinks a');
if (blockedUserName !== relevantUserName) {
unblockLink.hidden = true;
unblockLink.nextSibling.hidden = true; // link+trailing bullet
} else {
unblockLink.hidden = false;
unblockLink.nextSibling.hidden = false; // link+trailing bullet
}
 
// Semi-busted on ranges, see [[phab:T270737]] and [[phab:T146628]].
// Basically, logevents doesn't treat functionally-equivalent ranges
// as equivalent, meaning any functionally-equivalent IP range is
// misinterpreted by the log throughout. Without logevents
// redirecting (like Special:Block does) we would need a function to
// parse ranges, which is a pain. IPUtils has the code, but it'd be a
// lot of cruft for one purpose.
Twinkle.block.hasBlockLog = !!data.query.logevents.length;
Twinkle.block.blockLog = Twinkle.block.hasBlockLog && data.query.logevents;
// Used later to check if block status changed while filling out the form
Twinkle.block.blockLogId = Twinkle.block.hasBlockLog ? data.query.logevents[0].logid : false;
 
if (typeof fn === 'function') {
return fn();
}
};
 
Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) {
const query = {
format: 'json',
action: 'query',
Line 102 ⟶ 230:
letype: 'block',
lelimit: 1,
letitle: 'User:' + relevantUserName,
bkusers: mw.config.get('wgRelevantUserName'),
bkprop: 'expiry|reason|flags|restrictions|range|user',
ususers: mw.config.get('wgRelevantUserName'),
ususers: relevantUserName
letitle: 'User:' + mw.config.get('wgRelevantUserName')
});
.then(function(data) {
var blockinfo = data.query.blocks[0],
userinfo = data.query.users[0];
 
// bkusers doesn't catch single IPs blocked as part of a range block
Twinkle.block.isRegistered = !!userinfo.userid;
if (mw.util.isIPAddress(relevantUserName, true)) {
relevantUserName = Twinkle.block.isRegistered ? 'User:' + mw.config.get('wgRelevantUserName') : mw.config.get('wgRelevantUserName');
query.bkip = relevantUserName;
} else {
query.bkusers = relevantUserName;
// groupmemberships only relevant for registered users
query.usprop = 'groupmemberships';
}
 
api.get(query).then((data) => {
if (blockinfo) {
Twinkle.block.processUserInfo(data, fn);
// handle frustrating system of inverted boolean values
}, (msg) => {
blockinfo.disabletalk = blockinfo.allowusertalk === undefined;
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
blockinfo.hardblock = blockinfo.anononly === undefined;
Morebits.Status.warn('Error fetching user info', msg);
Twinkle.block.currentBlockInfo = blockinfo;
});
}
 
Twinkle.block.hasBlockLog = !!data.query.logevents.length;
 
if (typeof fn === 'function') {
return fn();
}
}, function(msg) {
Morebits.status.init($('div[name="currentblock"] span').last()[0]);
Morebits.status.warn('Error fetching user info', msg);
});
};
 
Twinkle.block.callback.saveFieldset = function twinkleblockCallbacksaveFieldset(fieldset) {
Twinkle.block[$(fieldset).prop('name')] = {};
$(fieldset).serializeArray().forEach(function(el) => {
// namespaces and pages for partial blocks are overwritten
// here, but we're handling them elsewhere so that's fine
Twinkle.block[$(fieldset).prop('name')][el.name] = el.value;
});
};
 
Twinkle.block.callback.change_block64 = function twinkleblockCallbackChangeBlock64(e) {
const $form = $(e.target.form), $block64 = $form.find('[name=block64]');
 
// Show/hide block64 button
// Single IPv6, or IPv6 range smaller than a /64
const priorName = relevantUserName;
if ($block64.is(':checked')) {
relevantUserName = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
} else {
relevantUserName = mw.config.get('wgRelevantUserName');
}
// No templates for ranges, but if the original user is a single IP, offer the option
// (done separately in Twinkle.block.callback.issue_template)
const originalIsRange = Morebits.ip.isRange(mw.config.get('wgRelevantUserName'));
$form.find('[name=actiontype][value=template]').prop('disabled', originalIsRange).prop('checked', !originalIsRange);
 
// Refetch/reprocess user info then regenerate the main content
const regenerateForm = function() {
// Tweak titlebar text. In theory, we could save the dialog
// at initialization and then use `.setTitle` or
// `dialog('option', 'title')`, but in practice that swallows
// the scriptName and requires `.display`ing, which jumps the
// window. It's just a line of text, so this is fine.
const titleBar = document.querySelector('.ui-dialog-title').firstChild.nextSibling;
titleBar.nodeValue = titleBar.nodeValue.replace(priorName, relevantUserName);
// Tweak unblock link
const unblockLink = document.querySelector('.morebits-dialog-footerlinks a');
unblockLink.href = unblockLink.href.replace(priorName, relevantUserName);
unblockLink.title = unblockLink.title.replace(priorName, relevantUserName);
 
// 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 === '');
}
 
// Set content appropriately
Twinkle.block.callback.change_action(e);
};
 
if (Twinkle.block.fetchedData[relevantUserName]) {
Twinkle.block.processUserInfo(Twinkle.block.fetchedData[relevantUserName], regenerateForm);
} else {
Twinkle.block.fetchUserInfo(regenerateForm);
}
};
 
Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction(e) {
varlet field_preset, field_template_options, field_block_options,;
const $form = $(e.target.form);
// Make ifs shorter
varconst blockBox = $form.find('[name=actiontype][value=block]').is(':checked');
varconst templateBox = $form.find('[name=actiontype][value=template]').is(':checked');
varconst $partial = $form.find('[name=actiontype][value=partial]');
varconst partialBox = $partial.is(':checked');
varlet blockGroup = partialBox ? Twinkle.block.blockGroupsPartial : Twinkle.block.blockGroups;
 
$partial.prop('disabled', !blockBox && !templateBox);
 
// Add current block parameters as default preset
const prior = { label: 'Prior block' };
if (blockedUserName === relevantUserName) {
Twinkle.block.blockPresetsInfo.prior = Twinkle.block.currentBlockInfo;
// value not a valid template selection, chosen below by setting templateName
prior.list = [{ label: 'Prior block settings', value: 'prior', selected: true }];
 
// Arrays of objects are annoying to check
if (!blockGroup.some((bg) => bg.label === prior.label)) {
blockGroup.push(prior);
}
 
// Always ensure proper template exists/is selected when switching modes
if (partialBox) {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-pblockindef' : 'uw-pblock';
} else {
if (!Twinkle.block.isRegistered) {
Twinkle.block.blockPresetsInfo.prior.templateName = 'uw-ablock';
} 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((bg) => bg.label !== prior.label);
}
 
// Can be in preset or template field, so the old one in the template
// field will linger. No need to keep the old value around, so just
// remove it; saves trouble when hiding/evaluating
$form.find('[name=dstopic]').parent().remove();
 
Twinkle.block.callback.saveFieldset($('[name=field_block_options]'));
Line 155 ⟶ 356:
 
if (blockBox) {
field_preset = new Morebits.quickFormQuickForm.elementElement({ type: 'field', label: 'Preset', name: 'field_preset' });
field_preset.append({
type: 'select',
Line 164 ⟶ 365:
});
 
field_block_options = new Morebits.quickFormQuickForm.elementElement({ type: 'field', label: 'Block options', name: 'field_block_options' });
field_block_options.append({ type: 'div', name: 'hasblocklog', label: ' ' });
field_block_options.append({ type: 'div', name: 'currentblock', label: ' ' });
field_block_options.append({ type: 'div', name: 'hasblocklog', label: ' ' });
field_block_options.append({
type: 'select',
Line 210 ⟶ 411:
tooltip: '10 page max.'
});
varconst ns = field_block_options.append({
type: 'select',
multiple: true,
Line 218 ⟶ 419:
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) {
Line 226 ⟶ 427:
}
 
varconst blockoptions = [
{
checked: Twinkle.block.field_block_options.nocreate,
Line 280 ⟶ 481:
label: 'Reason (for block log):',
name: 'reason',
tooltip: 'Consider adding helpful details to the default message.',
value: Twinkle.block.field_block_options.reason
});
Line 317 ⟶ 519:
});
 
// Yet-another-logevents-doesn't-handle-ranges-well
if (Twinkle.block.currentBlockInfo) {
if (blockedUserName === relevantUserName) {
field_block_options.append({ type: 'hidden', name: 'reblock', value: '1' });
}
}
 
// grab discretionary sanctions list from en-wiki
Twinkle.block.dsinfo = Morebits.wiki.getCachedJson('Template:Ds/topics.json');
 
Twinkle.block.dsinfo.then((dsinfo) => {
const $select = $('[name="dstopic"]');
const $options = $.map(dsinfo, (value, key) => $('<option>').val(value.code).text(key).prop('label', key));
$select.append($options);
});
 
// DS selection visible in either the template field set or preset,
// joint settings saved here
const dsSelectSettings = {
type: 'select',
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
};
if (templateBox) {
field_template_options = new Morebits.quickFormQuickForm.elementElement({ type: 'field', label: 'Template options', name: 'field_template_options' });
field_template_options.append({
type: 'select',
Line 332 ⟶ 554:
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',
display: 'none',
label: 'Linked page',
value: '',
Line 345 ⟶ 570:
type: 'input',
name: 'area',
display: 'none',
label: 'Area blocked from',
value: '',
Line 355 ⟶ 579:
type: 'input',
name: 'template_expiry',
displaylabel: 'nonePeriod of blocking:',
label: 'Period of blocking: ',
value: '',
tooltip: 'The period the blocking is due for, for example 24 hours, 2 weeks, indefinite etc...'
Line 364 ⟶ 587:
type: 'input',
name: 'block_reason',
label: '"You have been blocked for ..." ',
display: 'none',
tooltip: 'An optional reason, to replace the default generic reason. Only available for the generic block templates.',
value: Twinkle.block.field_template_options.block_reason
Line 408 ⟶ 630:
}
 
varconst $previewlink = $('<a id="twinkleblock-preivewpreview-link">Preview</a>');
$previewlink.off('click').on('click', function() => {
Twinkle.block.callback.preview($form[0]);
});
Line 415 ⟶ 637:
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);
}
 
varlet oldfield;
if (field_preset) {
oldfield = $form.find('fieldset[name="field_preset"]')[0];
Line 427 ⟶ 652:
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({
theme: 'default select2-morebits',
width: '100%',
placeholder: 'Select pages to block user from',
Line 444 ⟶ 670:
delay: 100,
data: function(params) {
varconst title = mw.Title.newFromText(params.term);
if (!title) {
return;
}
return {
'action': 'query',
'format': 'json',
'list': 'allpages',
'apfrom': title.title,
'apnamespace': title.namespace,
'aplimit': '10'
};
},
processResults: function(data) {
return {
results: data.query.allpages.map(function(page) => {
varconst title = mw.Title.newFromText(page.title, page.ns).toText();
return {
id: title,
Line 476 ⟶ 702:
}
});
 
 
$form.find('[name=namespacerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matchers.wordBeginning,
Line 489 ⟶ 715:
 
mw.util.addCSS(
// prevent dropdown from appearing behind the dialog, just in case
'.select2-container { z-index: 10000; }' +
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
Line 503 ⟶ 727:
} else {
$form.find('fieldset[name="field_block_options"]').hide();
$form.find('fieldset[name="field_64"]').hide();
// Clear select2 options
$form.find('[name=pagerestrictions]').val(null).trigger('change');
$form.find('[name=namespacerestrictions]').val(null).trigger('change');
}
 
if (field_template_options) {
oldfield = $form.find('fieldset[name="field_template_options"]')[0];
oldfield.parentNode.replaceChild(field_template_options.render(), oldfield);
e.target.form.root.previewer = new Morebits.wiki.previewPreview($(e.target.form.root).find('#twinkleblock-previewbox').last()[0]);
} else {
$form.find('fieldset[name="field_template_options"]').hide();
}
 
// Any block, including ranges
if (Twinkle.block.hasBlockLog) {
if (Twinkle.block.currentBlockInfo) {
var $blockloglink = $('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: mw.config.get('wgRelevantUserName'), type: 'block'}) + '">block log</a>)');
// false for an ip covered by a range or a smaller range within a larger range;
// true for a user, single ip block, or the exact range for a range block
const sameUser = blockedUserName === relevantUserName;
 
Morebits.statusStatus.init($('div[name="hasblocklogcurrentblock"] span').last()[0]);
let statusStr = relevantUserName + ' is ' + (Twinkle.block.currentBlockInfo.partial === '' ? 'partially blocked' : 'blocked sitewide');
Morebits.status.warn('This user has been blocked in the past', $blockloglink[0]);
}
 
// Range blocked
if (Twinkle.block.currentBlockInfo) {
if (Twinkle.block.currentBlockInfo.rangestart !== Twinkle.block.currentBlockInfo.rangeend) {
Morebits.status.init($('div[name="currentblock"] span').last()[0]);
if (sameUser) {
// list=blocks without bkprops (as we do in fetchUerInfo)
statusStr += ' as a rangeblock';
// returns partial: '' if the user is partially blocked
} else {
var statusStr = relevantUserName + ' is ' + (Twinkle.block.currentBlockInfo.partial === '' ? 'partially blocked' : 'blocked sitewide');
statusStr += ' within a' + (Morebits.ip.get64(relevantUserName) === blockedUserName ? ' /64' : '') + ' rangeblock';
var infoStr = 'Submit query to reblock with supplied options';
// Link to the full range
if (Twinkle.block.currentBlockInfo.partial === undefined && partialBox) {
const $rangeblockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: blockedUserName, type: 'block'}) + '">' + blockedUserName + '</a>)'));
infoStr += ' and convert to a partial block';
statusStr += ' (' + $rangeblockloglink.html() + ')';
} else if (Twinkle.block.currentBlockInfo.partial === '' && !partialBox) {
}
infoStr += ' and convert to a sitewide block';
}
 
Morebits.status.warn(statusStr, infoStr);
if (Twinkle.block.callbackcurrentBlockInfo.update_form(e,expiry Twinkle.block.currentBlockInfo=== 'infinity'); {
statusStr += ' (indefinite)';
} else if (templateBox) {
} else if (new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).isValid()) {
// make sure all the fields are correct based on defaults
statusStr += ' (expires ' + new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).calendar('utc') + ')';
if (blockBox) {
}
Twinkle.block.callback.change_preset(e);
 
let 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.';
Twinkle.block.callback.change_template(e);
}
 
Morebits.Status.warn(statusStr, infoStr);
 
// Default to the current block conditions on intial form generation
Twinkle.block.callback.update_form(e, Twinkle.block.currentBlockInfo);
}
 
// This is where T146628 really comes into play: a rangeblock will
// only return the correct block log if wgRelevantUserName is the
// exact range, not merely a funtional equivalent
if (Twinkle.block.hasBlockLog) {
const $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) {
const 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);
}
};
Line 554 ⟶ 822:
* disabletalk: <disable user from editing their own talk page while blocked>
* expiry: <string - expiry timestamp, can include relative times like "5 months", "2 weeks" etc>
* forAnonOnlyforUnregisteredOnly: <show block option in the interface only if the relevant user is an IP>
* forRegisteredOnly: <show block option in the interface only if the relevant user is registered>
* label: <string - label for the option of the dropdown in the interface (keep brief)>
Line 560 ⟶ 828:
* pageParam: <set if the associated block template accepts a page parameter>
* prependReason: <string - prepends the value of 'reason' to the end of the existing reason, namely for when revoking talk page access>
* nocreate: <block account creation from the user's IP (for anonymousunregistered users only)>
* nonstandard: <template does not conform to stewardship of WikiProject User Warnings and may not accept standard parameters>
* reason: <string - block rationale, as would appear in the block log,
Line 576 ⟶ 844:
*/
Twinkle.block.blockPresetsInfo = {
'anonblock': {
expiry: '31 hours',
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
nonstandard: true,
Line 586 ⟶ 854:
'anonblock - school': {
expiry: '36 hours',
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
nonstandard: true,
Line 595 ⟶ 863:
'blocked proxy': {
expiry: '1 year',
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
nonstandard: true,
Line 604 ⟶ 872:
'CheckUser block': {
expiry: '1 week',
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
nonstandard: true,
Line 620 ⟶ 888:
},
'checkuserblock-wide': {
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
nonstandard: true,
Line 626 ⟶ 894:
sig: '~~~~'
},
'colocationwebhost': {
expiry: '1 year',
forAnonOnlyforUnregisteredOnly: true,
nonstandard: true,
reason: '{{colocationwebhost}}',
sig: null
},
'oversightblock': {
autoblock: true,
expiry: 'infinity',
Line 642 ⟶ 910:
},
'school block': {
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
nonstandard: true,
Line 648 ⟶ 916:
sig: '~~~~'
},
'spamblacklistblock': {
forAnonOnlyforUnregisteredOnly: true,
expiry: '1 month',
disabletalk: true,
Line 655 ⟶ 923:
reason: '{{spamblacklistblock}} <!-- editor only attempts to add blacklisted links, see [[Special:Log/spamblacklist]] -->'
},
rangeblock: {
// Placeholder for when we add support for rangeblocks
// reason: '{{rangeblock}}' : {,
nocreate: true,
// reason: '{{rangeblock}}',
// nocreate nonstandard: true,
// nonstandard forUnregisteredOnly: true,
sig: '~~~~'
// forAnonOnly: true,
},
// sig: '~~~~'
//tor: },{
'tor': {
expiry: '1 year',
forAnonOnlyforUnregisteredOnly: true,
nonstandard: true,
reason: '{{Tor}}',
sig: null
},
'webhostblock': {
expiry: '1 year',
forAnonOnlyforUnregisteredOnly: true,
nonstandard: true,
reason: '{{webhostblock}}',
Line 689 ⟶ 956:
autoblock: true,
expiry: '31 hours',
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
pageParam: true,
Line 709 ⟶ 976:
reason: '[[WP:Arbitration enforcement|Arbitration enforcement]]',
reasonParam: true,
summary: 'You have been blocked from editing for violating an [[WP:Arbitration|arbitration decision]] with your edits'
},
'uw-bioblock': {
Line 756 ⟶ 1,023:
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': {
Line 773 ⟶ 1,048:
'uw-copyrightblock': {
autoblock: true,
expiry: '24 hoursinfinity',
nocreate: true,
pageParam: true,
Line 795 ⟶ 1,070:
autoblock: true,
nocreate: true,
reason: 'DeliberatelyRepeatedly triggering the [[WP:Edit filter|Edit filter]]',
summary: 'You have been blocked from editing for making disruptive edits that repeatedly triggered the [[WP:EF|edit filter]]'
},
'uw-ewblock': {
Line 814 ⟶ 1,089:
},
'uw-ipevadeblock': {
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
reason: '[[WP:Blocking policy#Evasion of blocks|Block evasion]]',
Line 947 ⟶ 1,222:
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-ublockuhblock-double}} <!-- UsernameAttempted closelyimpersonation resemblesof 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': {
Line 957 ⟶ 1,241:
pageParam: true,
reason: '{{uw-vaublock}} <!-- Username violation, vandalism-only account -->',
summary: 'You have been indefinitely blocked from editing because your account is being [[WP:VOADISRUPTONLY|used only for vandalism]] and your username is a blatant violation of the [[WP:U|username policy]]'
},
'uw-vblock': {
Line 965 ⟶ 1,249:
pageParam: true,
reason: '[[WP:Vandalism|Vandalism]]',
summary: 'You have been blocked from editing forto persistentprevent further [[WP:VAND|vandalism]]'
},
'uw-voablock': {
Line 973 ⟶ 1,257:
nocreate: true,
pageParam: true,
reason: '[[WP:Vandalism-only accountDISRUPTONLY|Vandalism-only account]]',
summary: 'You have been indefinitely blocked from editing because your account is being [[WP:VOADISRUPTONLY|used only for vandalism]]'
},
'zombie proxy': {
expiry: '1 month',
forAnonOnlyforUnregisteredOnly: true,
nocreate: true,
nonstandard: true,
Line 986 ⟶ 1,270:
 
// Begin partial block templates, accessed in Twinkle.block.blockGroupsPartial
'uw-pblockacpblock': {
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,
expiry: '24 hours',
nocreate: false,
pageParam: false,
reason: '[[WP:Arbitration enforcement|Arbitration enforcement]]',
reasonParam: true,
summary: 'You have been [[WP:PB|partially blocked]] from certainediting areasfor ofviolating thean encyclopedia[[WP:Arbitration|arbitration decision]]'
},
'uw-pblockindefepblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
noemail: true,
pageParam: false,
reasonParam: true,
reason: 'Email [[WP:Harassment|harassment]]',
summary: 'You have been indefinitely [[WP:PB|partially blocked]] from certain areas of the encyclopedia'
summary: 'You have been [[WP:PB|blocked from emailing]] other editors for [[WP:Harassment|harassment]]'
},
'uw-ewpblock': {
Line 1,010 ⟶ 1,315:
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-acpblockpblock': {
autoblock: true,
expiry: '4824 hours',
nocreate: truefalse,
pageParam: false,
reasonParam: true,
summary: 'You have been [[WP:PB|partially blocked]] from certain areas of the encyclopedia'
reason: 'Misusing [[WP:Sock puppetry|multiple accounts]]',
summary: 'You have been [[WP:PB|blocked]] from creating accounts for misusing [[WP:SOCK|multiple accounts]]'
},
'uw-acpblockindefpblockindef': {
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-epblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
noemail: true,
pageParam: false,
reasonParam: true,
summary: 'You have been indefinitely [[WP:PB|partially blocked]] from certain areas of the encyclopedia'
reason: 'Email [[WP:Harassment|harassment]]',
summary: 'You have been [[WP:PB|blocked]] from emailing other editors for [[WP:Harassment|harassment]]'
}
};
Line 1,046 ⟶ 1,338:
Twinkle.block.transformBlockPresets = function twinkleblockTransformBlockPresets() {
// supply sensible defaults
$.each(Twinkle.block.blockPresetsInfo, function(preset, settings) => {
settings.summary = settings.summary || settings.reason;
settings.sig = settings.sig !== undefined ? settings.sig : 'yes';
settings.indefinite = settings.indefinite || Morebits.string.isInfinity(settings.expiry);
// despite this it's preferred that you use 'infinity' as the value for expiry
settings.indefinite = settings.indefinite || settings.expiry === 'infinity' || settings.expiry === 'infinite' || settings.expiry === 'indefinite' || settings.expiry === 'never';
 
if (!Twinkle.block.isRegistered && settings.indefinite) {
Line 1,106 ⟶ 1,397:
{ 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' }
]
Line 1,112 ⟶ 1,404:
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' },
Line 1,133 ⟶ 1,426:
{ label: 'colocationwebhost', value: 'colocationwebhost' },
{ label: 'oversightblock', value: 'oversightblock' },
// { label: 'rangeblock', value: 'rangeblock' }, // placeholderOnly for whenIP weranges, add supportselected for rangeblocksnon-/64 ranges in filtered_block_groups
{ label: 'spamblacklistblock', value: 'spamblacklistblock' },
{ label: 'tor', value: 'tor' },
Line 1,144 ⟶ 1,437:
Twinkle.block.blockGroupsPartial = [
{
label: 'PartialCommon partial block reasons',
list: [
{ label: 'Generic partial block (custom reason)', value: 'uw-pblock', selected: true },
{ label: 'Generic partial block (custom reason) - indefinite', value: 'uw-pblockindef' },
{ label: 'Edit warring', value: 'uw-ewpblock' },
]
},
{
label: 'Extended partial block reasons',
list: [
{ label: 'Arbitration enforcement', value: 'uw-aepblock' },
{ label: 'Email harassment', value: 'uw-epblock' },
{ label: 'Misusing multiple accounts', value: 'uw-acpblock' },
{ label: 'Misusing multiple accounts - indefinite', value: 'uw-acpblockindef' },
{ label: 'Email harassment', value: 'uw-epblock' }
]
}
];
 
 
Twinkle.block.callback.filtered_block_groups = function twinkleblockCallbackFilteredBlockGroups(group, show_template) {
return $.map(group, function(blockGroup) => {
varconst list = $.map(blockGroup.list, function(blockPreset) => {
switch (blockPreset.value) {
// only show uw-talkrevoked if reblocking
if (!Twinkle.block.currentBlockInfo && blockPreset.value === case 'uw-talkrevoked') {:
if (blockedUserName !== relevantUserName) {
return;
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('suppress')) {
return;
}
break;
default:
break;
}
 
const blockSettings = Twinkle.block.blockPresetsInfo[blockPreset.value];
 
let registrationRestrict;
if (blockSettings.forRegisteredOnly) {
registrationRestrict = Twinkle.block.isRegistered;
} else if (blockSettings.forUnregisteredOnly) {
registrationRestrict = !Twinkle.block.isRegistered;
} else {
registrationRestrict = true;
}
 
var blockSettings = Twinkle.block.blockPresetsInfo[blockPreset.value];
var registrationRestrict = blockSettings.forRegisteredOnly ? Twinkle.block.isRegistered : blockSettings.forAnonOnly ? !Twinkle.block.isRegistered : true;
if (!(blockSettings.templateName && show_template) && registrationRestrict) {
varconst templateName = blockSettings.templateName || blockPreset.value;
return {
label: (show_template ? '{{' + templateName + '}}: ' : '') + blockPreset.label,
Line 1,176 ⟶ 1,506:
value: templateName
}],
selected: !!blockPreset.selected,
disabled: !!blockPreset.disabled
};
}
Line 1,190 ⟶ 1,521:
 
Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset(e) {
varconst keyform = e.target.form, key = form.preset.value;
if (!key) {
return;
}
 
e.target.form.template.value = Twinkle.block.blockPresetsInfo[key].templateName || key;
Twinkle.block.callback.update_form(e, Twinkle.block.blockPresetsInfo[key]);
if (form.template) {
Twinkle.block.callback.change_template(e);
form.template.value = Twinkle.block.blockPresetsInfo[key].templateName || key;
Twinkle.block.callback.change_template(e);
} else {
Morebits.QuickForm.setElementVisibility(form.dstopic.parentNode, key === 'uw-aeblock' || key === 'uw-aepblock');
}
};
 
Twinkle.block.callback.change_expiry = function twinkleblockCallbackChangeExpiry(e) {
varconst expiry = e.target.form.expiry;
if (e.target.value === 'custom') {
Morebits.quickFormQuickForm.setElementVisibility(expiry.parentNode, true);
} else {
Morebits.quickFormQuickForm.setElementVisibility(expiry.parentNode, false);
expiry.value = e.target.value;
}
Line 1,212 ⟶ 1,547:
Twinkle.block.seeAlsos = [];
Twinkle.block.callback.toggle_see_alsos = function twinkleblockCallbackToggleSeeAlso() {
varconst reason = this.form.reason.value.replace(
new RegExp('( <!--|;) ' + 'see also ' + Twinkle.block.seeAlsos.join(' and ') + '( -->)?'), ''
);
 
Twinkle.block.seeAlsos = Twinkle.block.seeAlsos.filter(function(el) {=> el !== this.value);
return el !== this.value;
}.bind(this));
 
if (this.checked) {
Twinkle.block.seeAlsos.push(this.value);
}
varconst seeAlsoMessage = Twinkle.block.seeAlsos.join(' and ');
 
if (!Twinkle.block.seeAlsos.length) {
this.form.reason.value = reason;
} else if (reason.indexOfincludes('{{') !== -1) {
this.form.reason.value = reason + ' <!-- see also ' + seeAlsoMessage + ' -->';
} else {
this.form.reason.value = reason + '; see also ' + seeAlsoMessage;
}
};
 
Twinkle.block.dsReason = '';
Twinkle.block.callback.toggle_ds_reason = function twinkleblockCallbackToggleDSReason() {
const reason = this.form.reason.value.replace(
new RegExp(' ?\\(\\[\\[' + Twinkle.block.dsReason + '\\]\\]\\)'), ''
);
 
Twinkle.block.dsinfo.then((dsinfo) => {
const sanctionCode = this.selectedIndex;
const sanctionName = this.options[sanctionCode].label;
Twinkle.block.dsReason = dsinfo[sanctionName].page;
if (!this.value) {
this.form.reason.value = reason;
} else {
this.form.reason.value = reason + ' ([[' + Twinkle.block.dsReason + ']])';
}
});
};
 
Twinkle.block.callback.update_form = function twinkleblockCallbackUpdateForm(e, data) {
varconst form = e.target.form,;
let expiry = data.expiry;
 
// don't override original expiry if useInitialOptions is set
Line 1,248 ⟶ 1,600:
form.expiry.value = expiry;
if (form.expiry_preset.value === 'custom') {
Morebits.quickFormQuickForm.setElementVisibility(form.expiry.parentNode, true);
} else {
Morebits.quickFormQuickForm.setElementVisibility(form.expiry.parentNode, false);
}
}
Line 1,259 ⟶ 1,611:
 
// disable autoblock if blocking a bot
if (Twinkle.block.isRegistereduserIsBot &&|| relevantUserName.search(/bot$\b/i.test(relevantUserName) > 0) {
data.autoblock = false;
}
 
$(form).find('[name=field_block_options]').find(':checkbox').each(function(i, el) => {
// don't override original options if useInitialOptions is set
if (data.useInitialOptions && data[el.name] === undefined) {
Line 1,269 ⟶ 1,621:
}
 
varconst check = data[el.name] === '' || !!data[el.name];
$(el).prop('checked', check);
});
Line 1,277 ⟶ 1,629:
} else {
form.reason.value = data.reason || '';
}
 
// Clear and/or set any partial page or namespace restrictions
if (form.pagerestrictions) {
const $pageSelect = $(form).find('[name=pagerestrictions]');
const $namespaceSelect = $(form).find('[name=namespacerestrictions]');
 
// Respect useInitialOptions by clearing data when switching presets
// In practice, this will always clear, since no partial presets use it
if (!data.useInitialOptions) {
$pageSelect.val(null).trigger('change');
$namespaceSelect.val(null).trigger('change');
}
 
// Add any preset options; in practice, just used for prior block settings
if (data.restrictions) {
if (data.restrictions.pages && !$pageSelect.val().length) {
const pages = data.restrictions.pages.map((pr) => pr.title);
// since page restrictions use an ajax source, we
// short-circuit that and just add a new option
pages.forEach((page) => {
if (!$pageSelect.find("option[value='" + $.escapeSelector(page) + "']").length) {
const newOption = new Option(page, page, true, true);
$pageSelect.append(newOption);
}
});
$pageSelect.val($pageSelect.val().concat(pages)).trigger('change');
}
if (data.restrictions.namespaces) {
$namespaceSelect.val($namespaceSelect.val().concat(data.restrictions.namespaces)).trigger('change');
}
}
}
};
 
Twinkle.block.callback.change_template = function twinkleblockcallbackChangeTemplate(e) {
varconst form = e.target.form, value = form.template.value, settings = Twinkle.block.blockPresetsInfo[value];
 
if (!$(form).find('[name=actiontype][value=block]').is(':checked')) {
const blockBox = $(form).find('[name=actiontype][value=block]').is(':checked');
const partialBox = $(form).find('[name=actiontype][value=partial]').is(':checked');
const templateBox = $(form).find('[name=actiontype][value=template]').is(':checked');
 
// Block form is not present
if (!blockBox) {
if (settings.indefinite || settings.nonstandard) {
if (Twinkle.block.prev_template_expiry === null) {
Line 1,288 ⟶ 1,678:
}
form.template_expiry.parentNode.style.display = 'none';
form.template_expiry.value = 'indefiniteinfinity';
} else if (form.template_expiry.parentNode.style.display === 'none') {
if (Twinkle.block.prev_template_expiry !== null) {
Line 1,299 ⟶ 1,689:
form.expiry.value = Twinkle.block.prev_template_expiry;
}
Morebits.quickFormQuickForm.setElementVisibility(form.notalk.parentNode, !settings.nonstandard);
// Partial
Morebits.quickForm.setElementVisibility(form.noemail_template.parentNode, $(form).find('[name=actiontype][value=partial]').is(':checked') && !$(form).find('[name=actiontype][value=block]').is(':checked'));
Morebits.quickFormQuickForm.setElementVisibility(form.nocreate_templatenoemail_template.parentNode, $(form).find('[name=actiontype][value=partial]').is(':checked') && !$(form).find('[name=actiontype][value=block]').is(':checked')partialBox);
Morebits.QuickForm.setElementVisibility(form.nocreate_template.parentNode, partialBox);
} else {
} else if (templateBox) { // Only present if block && template forms both visible
Morebits.quickForm.setElementVisibility(
Morebits.QuickForm.setElementVisibility(
form.blank_duration.parentNode,
!settings.indefinite && !settings.nonstandard
);
}
 
Morebits.quickForm.setElementVisibility(form.article.parentNode, !!settings.pageParam);
Morebits.quickFormQuickForm.setElementVisibility(form.block_reasondstopic.parentNode, !!settings.reasonParamvalue === '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.quickFormQuickForm.setElementVisibility(form.area.parentNode, $(form).find('[name=actiontype][value=partial]').is(':checked')partialBox && !$(form).find('[name=actiontype][value=block]').is(':checked')blockBox);
 
form.root.previewer.closePreview();
};
Twinkle.block.prev_template_expiry = null;
Twinkle.block.prev_block_reason = null;
Twinkle.block.prev_article = null;
Twinkle.block.prev_reason = null;
 
Twinkle.block.callback.preview = function twinkleblockcallbackPreview(form) {
varconst params = {
article: form.article.value,
blank_duration: form.blank_duration ? form.blank_duration.checked : false,
Line 1,328 ⟶ 1,720:
expiry: form.template_expiry ? form.template_expiry.value : form.expiry.value,
hardblock: Twinkle.block.isRegistered ? form.autoblock.checked : form.hardblock.checked,
indefinite: (/indef|infinit|never|\*|max/)Morebits.teststring.isInfinity(form.template_expiry ? form.template_expiry.value : form.expiry.value),
reason: form.block_reason.value,
template: form.template.value,
dstopic: form.dstopic.value,
partial: $(form).find('[name=actiontype][value=partial]').is(':checked'),
pagerestrictions: $(form.pagerestrictions).val() || [],
Line 1,339 ⟶ 1,732:
};
 
varconst templateText = Twinkle.block.callback.getBlockNoticeWikitext(params);
 
form.previewer.beginRender(templateText, 'User_talk:' + mw.config.get('wgRelevantUserName')relevantUserName); // Force wikitext/correct username
};
 
Twinkle.block.callback.evaluate = function twinkleblockCallbackEvaluate(e) {
varconst $form = $(e.target),
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'),;
let blockoptions = {}, templateoptions = {};
 
Twinkle.block.callback.saveFieldset($form.find('[name=field_block_options]'));
Line 1,357 ⟶ 1,750:
 
templateoptions = Twinkle.block.field_template_options;
 
templateoptions.disabletalk = !!(templateoptions.disabletalk || blockoptions.disabletalk);
templateoptions.hardblock = !!blockoptions.hardblock;
 
delete blockoptions.expiry_preset; // remove extraneous
 
Line 1,377 ⟶ 1,772:
if (toBlock) {
if (blockoptions.partial) {
if (blockoptions.disabletalk && !blockoptions.namespacerestrictions.indexOfincludes('3') === -1) {
return alert('Partial blocks cannot prevent talk page access unless also restricting them from editing User talk space!');
}
Line 1,383 ⟶ 1,778:
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') &&
} else if (!confirm('You are about to block with no restrictions on page or namespace editing, are you sure you want to proceed?')) {
// 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;
}
Line 1,390 ⟶ 1,787:
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) {
Line 1,395 ⟶ 1,794:
}
 
Morebits.simpleWindowSimpleWindow.setButtonsEnabled(false);
Morebits.statusStatus.init(e.target);
varconst statusElement = new Morebits.statusStatus('Executing block');
blockoptions.action = 'block';
 
blockoptions.user = mw.config.get('wgRelevantUserName');
blockoptions.user = relevantUserName;
 
// boolean-flipped options
Line 1,405 ⟶ 1,805:
blockoptions.allowusertalk = blockoptions.disabletalk ? undefined : true;
 
/*
// execute block
Check if block status changed while processing the form.
api.getToken('block').then(function(token) {
 
statusElement.status('Processing...');
There's a lot to consider here. list=blocks provides the
blockoptions.token = token;
current block status, but there are at least two issues with
var mbApi = new Morebits.wiki.api('Executing block', blockoptions, function() {
relying on it. First, the id doesn't update on a reblock,
meaning the individual parameters need to be compared. This
can be done roughly with JSON.stringify - we can thankfully
rely on order from the server, although sorting would be
fine if not - but falsey values are problematic and is
non-ideal. More importantly, list=blocks won't indicate if a
non-blocked user is blocked then unblocked. This should be
exceedingy rare, but regardless, we thus need to check
list=logevents, which has a nicely updating logid
parameter. We can't rely just on that, though, since it
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).
*/
const 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((data) => {
let 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];
}
const logevents = data.query.logevents[0];
const logid = data.query.logevents.length ? logevents.logid : false;
 
if (logid !== Twinkle.block.blockLogId || !!block !== !!Twinkle.block.currentBlockInfo) {
let message = 'The block status of ' + blockoptions.user + ' has changed. ';
if (block) {
message += 'New status: ';
} else {
message += 'Last entry: ';
}
 
let logExpiry = '';
if (logevents.params.duration) {
if (logevents.params.duration === 'infinity') {
logExpiry = 'indefinitely';
} else {
const expiryDate = new Morebits.Date(logevents.params.expiry);
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?';
 
if (!confirm(message)) {
Morebits.Status.info('Executing block', 'Canceled by user');
return;
}
blockoptions.reblock = 1; // Writing over a block will fail otherwise
}
 
// execute block
blockoptions.tags = Twinkle.changeTags;
blockoptions.token = mw.user.tokens.get('csrfToken');
const mbApi = new Morebits.wiki.Api('Executing block', blockoptions, (() => {
statusElement.info('Completed');
if (toWarn) {
Twinkle.block.callback.issue_template(templateoptions);
}
}));
mbApi.post();
}, function() {
statusElement.error('Unable to fetch block token');
});
} else if (toWarn) {
Morebits.simpleWindowSimpleWindow.setButtonsEnabled(false);
 
Morebits.statusStatus.init(e.target);
Twinkle.block.callback.issue_template(templateoptions);
} else {
Line 1,430 ⟶ 1,910:
 
Twinkle.block.callback.issue_template = function twinkleblockCallbackIssueTemplate(formData) {
// Use wgRelevantUserName to ensure the block template goes to a single IP and not to the
var userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
// "talk page" of an IP range (which does not exist)
const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
 
const params = Twinkle.block.combineFormDataAndFieldTemplateOptions(
var params = $.extend(formData, {
messageData: formData,
Twinkle.block.blockPresetsInfo[formData.template],
reason: Twinkle.block.field_template_options.block_reason,
disabletalk: Twinkle.block.field_template_options.notalk,
noemail: Twinkle.block.field_template_options.noemail_template,
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';
 
varconst wikipedia_page = new Morebits.wiki.pagePage(userTalkPage, 'User talk page modification');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.setFollowRedirect(true);
wikipedia_page.load(Twinkle.block.callback.main);
};
 
Twinkle.block.combineFormDataAndFieldTemplateOptions = function(formData, messageData, reason, disabletalk, noemail, nocreate) {
return $.extend(formData, {
messageData: messageData,
reason: reason,
disabletalk: disabletalk,
noemail: noemail,
nocreate: nocreate
});
};
 
Twinkle.block.callback.getBlockNoticeWikitext = function(params) {
varlet text = '{{',;
const settings = Twinkle.block.blockPresetsInfo[params.template];
if (!settings.nonstandard) {
text += 'subst:' + params.template;
if (params.article && settings.pageParam) {
text += '|page=' + params.article;
}
if (params.dstopic) {
text += '|topic=' + params.dstopic;
}
 
Line 1,460 ⟶ 1,956:
if (params.indefinite) {
text += '|indef=yes';
} else if (!params.blank_duration && !new Morebits.Date(params.expiry).isValid()) {
// Block template wants a duration, not date
text += '|time=' + params.expiry;
}
Line 1,480 ⟶ 1,977:
if (params.partial) {
if (params.pagerestrictions.length || params.namespacerestrictions.length) {
varconst makeSentence = function (array) {
if (array.length < 3) {
return array.join(' and ');
}
varconst last = array.pop();
return array.join(', ') + ', and ' + last;
 
};
text += '|area=' + (params.indefinite ? 'certain ' : 'from certain ');
if (params.pagerestrictions.length) {
text += 'pages (' + makeSentence(params.pagerestrictions.map(function(p) {=> '[[:' + p + ']]'));
return '[[:' + p + ']]';
}));
text += params.namespacerestrictions.length ? ') and certain ' : ')';
}
if (params.namespacerestrictions.length) {
// 1 => Talk, 2 => User, etc.
varconst namespaceNames = params.namespacerestrictions.map(function(id) {=> menuFormattedNamespaces[id]);
return menuFormattedNamespaces[id];
});
text += '[[Wikipedia:Namespace|namespaces]] (' + makeSentence(namespaceNames) + ')';
}
Line 1,524 ⟶ 2,017:
 
Twinkle.block.callback.main = function twinkleblockcallbackMain(pageobj) {
varconst textparams = pageobj.getPageTextgetCallbackParameters(),
paramsdate = new Morebits.Date(pageobj.getCallbackParametersgetLoadTime()),
messageData = params.messageData,;
let text;
date = new Date(pageobj.getLoadTime());
 
params.indefinite = Morebits.string.isInfinity(params.expiry);
var dateHeaderRegex = new RegExp('^==+\\s*(?:' + date.getUTCMonthName() + '|' + date.getUTCMonthNameAbbrev() +
')\\s+' + date.getUTCFullYear() + '\\s*==+', 'mg');
var dateHeaderRegexLast, dateHeaderRegexResult;
while ((dateHeaderRegexLast = dateHeaderRegex.exec(text)) !== null) {
dateHeaderRegexResult = dateHeaderRegexLast;
}
// 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
// returns -1 in this case, so lastHeaderIndex gets set to 0 as desired.
var lastHeaderIndex = text.lastIndexOf('\n==') + 1;
 
if (Twinkle.getPref('blankTalkpageOnIndefBlock') && params.template !== 'uw-lblock' && params.indefinite) {
if (text.length > 0) {
Morebits.Status.info('Info', 'Blanking talk page per preferences and creating a new talk page section for this month');
text += '\n\n';
text = date.monthHeader() + '\n';
}
} else {
text = pageobj.getPageText();
 
const dateHeaderRegex = date.monthHeaderRegex();
params.indefinite = (/indef|infinit|never|\*|max/).test(params.expiry);
let dateHeaderRegexLast, dateHeaderRegexResult;
while ((dateHeaderRegexLast = dateHeaderRegex.exec(text)) !== null) {
dateHeaderRegexResult = dateHeaderRegexLast;
}
// 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
// returns -1 in this case, so lastHeaderIndex gets set to 0 as desired.
const lastHeaderIndex = text.lastIndexOf('\n==') + 1;
 
if (text.length > 0) {
if (Twinkle.getPref('blankTalkpageOnIndefBlock') && params.template !== 'uw-lblock' && params.indefinite) {
text += '\n\n';
Morebits.status.info('Info', 'Blanking talk page per preferences and creating a new level 2 heading for the date');
}
text = '== ' + date.getUTCMonthName() + ' ' + date.getUTCFullYear() + ' ==\n';
 
} else if (!dateHeaderRegexResult || dateHeaderRegexResult.index !== lastHeaderIndex) {
if (!dateHeaderRegexResult || dateHeaderRegexResult.index !== lastHeaderIndex) {
Morebits.status.info('Info', 'Will create a new level 2 heading for the date, as none was found for this month');
Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found');
text += '== ' + date.getUTCMonthName() + ' ' + date.getUTCFullYear() + ' ==\n';
text += date.monthHeader() + '\n';
}
}
 
Line 1,559 ⟶ 2,055:
 
// build the edit summary
varlet summary = messageData.summary;
if (messageData.suppressArticleInSummary !== true && params.article) {
summary += ' on [[:' + params.article + ']]';
}
summary += '.' + Twinkle.getPref('summaryAd');
 
pageobj.setPageText(text);
pageobj.setEditSummary(summary);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('watchWarnings'));
pageobj.save();
};
 
Twinkle.addInitCallback(Twinkle.block, 'block');
})(jQuery);
}());
 
 
// </nowiki>