MediaWiki:Gadget-twinkleblock.js: Difference between revisions

Content deleted Content added
Repo at f27c7b2: Change reference to "level 2 heading" to "talk page section"
Repo at ab124a59: Only select {{rangeblock}} for IPv6 ranges larger than /64; Leave template on the page of the current IP even when blocking a /64; Hide rather than disable CUOS options; Add Morebits.ip.isRange to simplify use of mw.util.isIPAddress; Reorganize IP utilities into Morebits.ip object; Add checkbox to just block the /64; Enable for IP ranges, improve handling of rangeblocks; Show the length of the last block if it expired naturally
Line 4:
(function($) {
 
var api = new mw.Api(), relevantUserName, blockedUserName;
var menuFormattedNamespaces = $.extend({}, 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.userIsSysop && 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;
Line 35 ⟶ 37:
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')relevantUserName);
Window.setScriptName('Twinkle');
Window.addFooterLink('Block templates', 'Template:Uw-block/doc/Block_templates');
Line 42 ⟶ 44:
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#block');
Window.addFooterLink('Give feedback', 'WT:TW');
 
// Always added, hidden later if actual user not blocked
Window.addFooterLink('Unblock this user', 'Special:Unblock/' + relevantUserName, true);
 
var form = new Morebits.quickForm(Twinkle.block.callback.evaluate);
Line 69 ⟶ 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.
*/
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: relevantUserName !== mw.config.get('wgRelevantUserName'), // In case the user closes and reopens the form
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 88 ⟶ 133:
// Toggle initial partial state depending on prior block type,
// will override the defaultToPartialBlocks pref
if (blockedUserName === relevantUserName) {
if (Twinkle.block.currentBlockInfo) {
$(result).find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
Line 94 ⟶ 139:
// clean up preset data (defaults, etc.), done exactly once, must be before Twinkle.block.callback.change_action is called
Twinkle.block.transformBlockPresets();
if (Twinkle.block.currentBlockInfo) {
Window.addFooterLink('Unblock this user', 'Special:Unblock/' + mw.config.get('wgRelevantUserName'), true);
}
 
// init the controls after user and block info have been fetched
Line 103 ⟶ 145:
result.actiontype[0].dispatchEvent(evt);
});
};
 
// Store fetched user data, only relevant if switching IPv6 to a /64
Twinkle.block.fetchedData = {};
// Processes the data from a a query response, separated from
// Twinkle.block.fetchUserInfo to allow reprocessing of already-fetched data
Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) {
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];
}
// Cache response, used when toggling /64 blocks
Twinkle.block.fetchedData[userinfo.name] = data;
 
Twinkle.block.isRegistered = !!userinfo.userid;
if (Twinkle.block.isRegistered) {
Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map(function(e) {
return e.group;
}).indexOf('bot') !== -1;
} 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
var 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) {
var query = {
api.get({
format: 'json',
action: 'query',
Line 112 ⟶ 213:
letype: 'block',
lelimit: 1,
letitle: 'User:' + relevantUserName,
bkusers: mw.config.get('wgRelevantUserName'),
bkprop: 'expiry|reason|flags|restrictions|range|user',
ususers: mw.config.get('wgRelevantUserName'),relevantUserName
};
usprop: 'groupmemberships',
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)) {
if (Twinkle.block.isRegistered) {
query.bkip = relevantUserName;
relevantUserName = 'User:' + mw.config.get('wgRelevantUserName');
} else {
Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map(function(e) {
query.bkusers = relevantUserName;
return e.group;
// groupmemberships only relevant for registered users
}).indexOf('bot') !== -1;
query.usprop = 'groupmemberships';
} else {
}
relevantUserName = mw.config.get('wgRelevantUserName');
Twinkle.block.userIsBot = false;
}
 
if (blockinfo) {
// handle frustrating system of inverted boolean values
blockinfo.disabletalk = blockinfo.allowusertalk === undefined;
blockinfo.hardblock = blockinfo.anononly === undefined;
Twinkle.block.currentBlockInfo = blockinfo;
}
 
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 api.get(query).then(typeof fn === 'function'(data) {
Twinkle.block.processUserInfo(data, fn);
return fn();
}, function(msg) {
}
Morebits.status.init($('div[name="currentblock"] span').last()[0]);
}, function(msg) {
Morebits.status.init($warn('div[name="currentblock"]Error spanfetching user info').last()[0], msg);
});
Morebits.status.warn('Error fetching user info', msg);
});
};
 
Line 161 ⟶ 242:
Twinkle.block[$(fieldset).prop('name')][el.name] = el.value;
});
};
 
Twinkle.block.callback.change_block64 = function twinkleblockCallbackChangeBlock64(e) {
var $form = $(e.target.form), $block64 = $form.find('[name=block64]');
 
// Show/hide block64 button
// Single IPv6, or IPv6 range smaller than a /64
var 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)
var 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
var 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.
var titleBar = document.querySelector('.ui-dialog-title').firstChild.nextSibling;
titleBar.nodeValue = titleBar.nodeValue.replace(priorName, relevantUserName);
// Tweak unblock link
var 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);
}
};
 
Line 168 ⟶ 296:
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;
 
$partial.prop('disabled', !blockBox && !templateBox);
 
// Add current block parameters as default preset
var prior = { label: 'Prior block' };
if (Twinkle.block.currentBlockInfo) {
if (blockedUserName === relevantUserName) {
Twinkle.block.blockPresetsInfo.prior = Twinkle.block.currentBlockInfo;
// value not a valid template selection, chosen below by setting templateName
var prior = {
prior.list = [{ label: 'Prior block settings', value: 'prior', selected: true }];
 
// value not a valid template selection, chosen below by setting templateName
list: [{ label: 'Prior block settings', value: 'prior', selected: true }]
};
// Arrays of objects are annoying to check
if (!blockGroup.some(function(bg) {
Line 199 ⟶ 326:
}
}
} else {
// But first remove any prior prior
blockGroup = blockGroup.filter(function(bg) {
return bg.label !== prior.label;
});
}
 
Line 373 ⟶ 505:
});
 
// 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' });
}
Line 485 ⟶ 618:
field_template_options.append({ type: 'div', id: 'twinkleblock-previewbox', style: 'display: none' });
} else if (field_preset) {
// Only visible for aeblockarbitration and aepblockenforcement, toggled in change_templatechange_preset
field_preset.append(dsSelectSettings);
}
Line 499 ⟶ 632:
oldfield = $form.find('fieldset[name="field_block_options"]')[0];
oldfield.parentNode.replaceChild(field_block_options.render(), oldfield);
$form.find('fieldset[name="field_64"]').show();
 
 
Line 572 ⟶ 706:
} 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');
Line 585 ⟶ 720:
}
 
// Any block, including ranges
if (Twinkle.block.currentBlockInfo) {
// 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
var sameUser = blockedUserName === relevantUserName;
 
Morebits.status.init($('div[name="currentblock"] span').last()[0]);
var statusStr = relevantUserName + ' is ' + (Twinkle.block.currentBlockInfo.partial === '' ? 'partially blocked' : 'blocked sitewide');
 
// Range blocked
if (Twinkle.block.currentBlockInfo.rangestart !== Twinkle.block.currentBlockInfo.rangeend) {
if (sameUser) {
statusStr += ' as a rangeblock';
} else {
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)';
Line 593 ⟶ 746:
statusStr += ' (expires ' + new Morebits.date(Twinkle.block.currentBlockInfo.expiry).calendar('utc') + ')';
}
 
var infoStr = 'This form will change that block';
 
if (Twinkle.block.currentBlockInfo.partial === undefined && partialBox) {
var infoStr += ', converting it to aThis partialform blockwill';
if (sameUser) {
} else if (Twinkle.block.currentBlockInfo.partial === '' && !partialBox) {
infoStr += ', convertingchange it to a sitewidethat 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.';
}
 
infoStr += '.';
Morebits.status.warn(statusStr, infoStr);
 
Line 606 ⟶ 767:
}
 
// 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) {
var $blockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: mw.config.get('wgRelevantUserName')relevantUserName, type: 'block'}) + '">block log</a>)'));
if (!Twinkle.block.currentBlockInfo) {
var lastBlockAction = Twinkle.block.blockLog[0];
Line 613 ⟶ 777:
$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]);
}
 
Line 739 ⟶ 903:
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 forAnonOnly: true,
sig: '~~~~'
// forAnonOnly: true,
},
// sig: '~~~~'
// },
'tor': {
expiry: '1 year',
Line 1,374 ⟶ 1,537:
list: [
{ label: 'blocked proxy', value: 'blocked proxy' },
{ label: 'CheckUser block', value: 'CheckUser block', disabled: !Morebits.userIsInGroup('checkuser') },
{ label: 'checkuserblock-account', value: 'checkuserblock-account', disabled: !Morebits.userIsInGroup('checkuser') },
{ label: 'checkuserblock-wide', value: 'checkuserblock-wide', disabled: !Morebits.userIsInGroup('checkuser') },
{ label: 'colocationwebhost', value: 'colocationwebhost' },
{ label: 'oversightblock', value: 'oversightblock', disabled: !Morebits.userIsInGroup('oversight') },
// { 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,412 ⟶ 1,575:
return $.map(group, function(blockGroup) {
var 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('oversight')) {
return;
}
break;
default:
break;
}
 
Line 1,443 ⟶ 1,629:
 
Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset(e) {
var 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');
}
};
 
Line 1,651 ⟶ 1,841:
var templateText = Twinkle.block.callback.getBlockNoticeWikitext(params);
 
form.previewer.beginRender(templateText, 'User_talk:' + mw.config.get('wgRelevantUserName')relevantUserName); // Force wikitext/correct username
};
 
Line 1,670 ⟶ 1,860:
templateoptions.disabletalk = !!(templateoptions.disabletalk || blockoptions.disabletalk);
templateoptions.hardblock = !!blockoptions.hardblock;
 
delete blockoptions.expiry_preset; // remove extraneous
 
Line 1,714 ⟶ 1,905:
var statusElement = new Morebits.status('Executing block');
blockoptions.action = 'block';
 
blockoptions.user = mw.config.get('wgRelevantUserName');
blockoptions.user = relevantUserName;
 
// boolean-flipped options
Line 1,746 ⟶ 1,938:
same block is still active (same status, no confirmation).
*/
var query = {
api.get({
format: 'json',
action: 'query',
Line 1,752 ⟶ 1,944:
letype: 'block',
lelimit: 1,
letitle: 'User:' + blockoptions.user,
};
bkusers: blockoptions.user
// bkusers doesn't catch single IPs blocked as part of a range block
}).then(function(data) {
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];
}
var logevents = data.query.logevents[0];
var logid = data.query.logevents.length ? logevents.logid : false;
 
if (logid !== Twinkle.block.blockLogId || !!block !== !!Twinkle.block.currentBlockInfo) {
var message = 'The block status of ' + mwblockoptions.config.get('wgRelevantUserName')user + ' has changed. ';
if (block) {
message += 'New status: ';
Line 1,810 ⟶ 2,017:
 
Twinkle.block.callback.issue_template = function twinkleblockCallbackIssueTemplate(formData) {
// Use wgRelevantUserName to ensure the block template goes to a single IP and not to the
// "talk page" of an IP range (which does not exist)
var userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');