User:DatGuy/mark-blocked.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* Gadget-MarkBLocked-DG
* @author Dragoniez
* @author DatGuy
* @link https://www.meta.wikimedia.org/wiki/User:DatGuy/mark-blocked.js
* @link https://www.mediawiki.org/wiki/User:Dragoniez/Gadget-MarkBLocked.css
* @license MIT
* @requires Gadget-MarkBLocked.css
* @description
* This is a script forked from [[mediawiki:User:Dragoniez/Gadget-MarkBLocked.js]]. This script:
* (1) Marks up locally blocked users and single IPs.
* (2) Can mark up single IPs included in locally blocked IP ranges.
* (3) Can mark up globally locked users.
* (4) Can mark up globally blocked single IPs and IP ranges.
* Note that the features in (2)-(4) require quite some API calls and could lead to performance
* issues depending on the browser and computer environments of the editor who uses the script;
* hence disabled by default. You can enable them via the configuration page added by the script,
* namely via [[Special:MarkBLockedPreferences]] (and also [[Special:MBLP]] or [[Special:MBP]]).
*/
// <nowiki>
/* jshint esversion: 8 */
(function(mw, $) { // Wrapper function
// *******************************************************************************************************************
var api;
var metaApi;
/** @readonly */
var MarkBLocked = mw.libs.MarkBLocked = {
// ********************************************** LOCALIZATION SETTINGS **********************************************
/**
* Portletlink configurations
* @static
* @readonly
*/
portletlink: {
position: 'p-tb',
text: 'MarkBLocked Preferences',
id: 't-mblp',
tooltip: 'Configure MarkBLocked',
accesskey: null,
nextnode: null
},
/**
* Register all local page names for [[Special:Contributions]] and [[Special:CentralAuth]] (without the namespace prefix).
* 'contribs', 'contributions', 'ca', and 'centralauth' are registered by default: No need to register them. Note that the
* items are case-insensitive, compatible both with " " and "_" for spaces, and should NEVER be URI-encoded. If nothing
* needs to be registered, leave the array empty.
* @static
* @readonly
*/
contribs_CA: ['投稿記録', 'アカウント統一管理'], // Example setting for jawiki
/**
* Texts to show on [[Special:MarkBLockedPreferences]]
* @static
* @readonly
*/
configpage: {
heading: 'MarkBLocked Preferences',
tooltip: 'Tooltip to use. $prev_text will default to the username if undefined',
check: {
localips: 'Check whether single IPs are included in locally-blocked IP ranges',
globalusers: 'Check whether registered users are globally locked',
globalips: 'Check whether IPs are globally blocked'
},
save: {
button: 'Save',
doing: 'Saving preferences',
done: 'Saved preferences',
failed: 'Failed to save preferences',
lastsave: 'Last saved at' // This is FOLLOWED by a space and a timestamp
}
},
/**
* Names of the local user groups that have the 'apihighlimits' user right
* @static
* @readonly
*/
apihighlimits: ['bot', 'sysop'],
// *******************************************************************************************************************
/**
* The keys are namespace numbers. The values are arrays of corresponding aliases.
* ```
* console.log(nsAliases[3]); // ['user_talk'] - Always in lowercase and spaces are represented by underscores.
* ```
* @type {Object.<number, Array<string>>}
* @static
* @readonly
*/
nsAliases: (function() {
/** @type {Object.<string, number>} */
var nsObj = mw.config.get('wgNamespaceIds'); // {"special": -1, "user": 2, ...}
/** @type {Object.<number, Array<string>>} */
var obj = Object.create(null);
return Object.keys(nsObj).reduce(function(acc, alias) {
var nsNumber = nsObj[alias];
if (!acc[nsNumber]) {
acc[nsNumber] = [alias];
} else {
acc[nsNumber].push(alias);
}
return acc;
}, obj);
})(),
/**
* Get all namespace aliases associated with certain numbers. The aliases are in lowercase and spaces are represented by underscores.
* @param {Array<number>} nsNumberArray
* @param {string} [stringifyWith] Join the result array with this delimiter and retun a string if provided
* @returns {Array<string>|string}
*/
getAliases: function(nsNumberArray, stringifyWith) {
/** @type {Array<string>} */
var aliasesArr = [];
nsNumberArray.forEach(function(nsNumber) {
aliasesArr = aliasesArr.concat(MarkBLocked.nsAliases[nsNumber]);
});
return typeof stringifyWith === 'string' ? aliasesArr.join(stringifyWith) : aliasesArr;
},
hasApiHighlimits: false,
prefs: {
tooltip: '$prev_text; $action_type ($duration) by $blocker: $reason ($time ago)',
localips: false,
globalusers: false,
globalips: false
},
/**
* @static
* @readonly
*/
saveOptionName: 'userjs-mbl-preferences',
/**
* @requires mediawiki.user
* @requires mediawiki.util
* @requires mediawiki.api
*/
init: function() {
api = new mw.Api();
metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
// Initialize MarkBLocked.hasApiHighlimits
var userGroups = MarkBLocked.apihighlimits.concat([
'apihighlimits-requestor',
'founder',
'global-bot',
'global-sysop',
'staff',
'steward',
'sysadmin',
'wmf-researcher'
]);
MarkBLocked.hasApiHighlimits = mw.config.get('wgUserGroups').concat(mw.config.get('wgGlobalGroups')).some(function(group) {
return userGroups.indexOf(group) !== -1;
});
// Merge preferences
var prefs = mw.user.options.get(MarkBLocked.saveOptionName);
if (prefs) $.extend(MarkBLocked.prefs, JSON.parse(prefs));
// Are we on the preferences page?
if (mw.config.get('wgNamespaceNumber') === -1 && /^(markblockedpreferences|mbl?p)$/i.test(mw.config.get('wgTitle'))) {
return MarkBLocked.createPreferencesPage();
}
// If not, create a portletlink to the preferences page
mw.util.addPortletLink(
MarkBLocked.portletlink.position,
mw.config.get('wgArticlePath').replace('$1', 'Special:MarkBLockedPreferences'),
MarkBLocked.portletlink.text,
MarkBLocked.portletlink.id,
MarkBLocked.portletlink.tooltip,
MarkBLocked.portletlink.accesskey,
MarkBLocked.portletlink.nextnode
);
// Now prepare for markup on certain conditions
if (mw.config.get('wgAction') !== 'edit' || // Not on an edit page, or
document.querySelector('.mw-logevent-loglines') // There's a notification box for delete, block, etc.
) {
var hookTimeout;
mw.hook('wikipage.content').add(function() {
clearTimeout(hookTimeout); // Prevent hook from being triggered multiple times
hookTimeout = setTimeout(MarkBLocked.markupUserLinks, 100);
// Check if CSS still works
});
}
},
/**
* @static
* @readonly
*/
images: {
loading: '<img src="//upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; height: 1em; border: 0;">',
check: '<img src="//upload.wikimedia.org/wikipedia/commons/f/fb/Yes_check.svg" style="vertical-align: middle; height: 1em; border: 0;">',
cross: '<img src="//upload.wikimedia.org/wikipedia/commons/a/a2/X_mark.svg" style="vertical-align: middle; height: 1em; border: 0;">'
},
createPreferencesPage: function() {
document.title = 'MarkBLockedPreferences - Wikipedia';
var container = document.createElement('div');
container.id = 'mblp-container';
/**
* @param {HTMLElement} appendTo
* @param {string} id
* @param {string} labelText
* @param {boolean} [appendBr]
* @returns {HTMLInputElement} checkbox
*/
var createCheckbox = function(appendTo, id, labelText, appendBr) {
var checkbox = document.createElement('input');
appendTo.appendChild(checkbox);
checkbox.type = 'checkbox';
checkbox.id = id;
checkbox.style.marginRight = '0.5em';
var belowHyphen = id.replace(/^[^-]+-/, '');
if (MarkBLocked.prefs[belowHyphen]) checkbox.checked = MarkBLocked.prefs[belowHyphen];
supplementInput(appendTo, id, labelText, appendBr);
return checkbox;
};
/**
* @param {HTMLElement} appendTo
* @param {string} id
* @param {string} labelText
* @param {boolean} [appendBr]
* @returns {HTMLInputElement} text input
*/
var createTextInput = function(appendTo, id, labelText, appendBr) {
var textInput = document.createElement('input');
appendTo.appendChild(textInput);
textInput.type = 'text';
textInput.id = id;
var belowHyphen = id.replace(/^[^-]+-/, '');
textInput.value = MarkBLocked.prefs[belowHyphen];
supplementInput(appendBr, id, labelText, appendBr);
return textInput;
};
var supplementInput = function(appendTo, id, labelText, appendBr) {
var label = document.createElement('label');
appendTo.appendChild(label);
label.htmlFor = id;
label.appendChild(document.createTextNode(labelText));
if (appendBr) appendTo.appendChild(document.createElement('br'));
};
var bodyDiv = document.createElement('div');
container.appendChild(bodyDiv);
bodyDiv.id = 'mblp-body';
var tooltip = createTextInput(bodyDiv, 'mblp-tooltip', MarkBLocked.configpage.tooltip, true);
var localips = createCheckbox(bodyDiv, 'mblp-localips', MarkBLocked.configpage.check.localips, true);
var globalusers = createCheckbox(bodyDiv, 'mblp-globalusers', MarkBLocked.configpage.check.globalusers, true);
var globalips = createCheckbox(bodyDiv, 'mblp-globalips', MarkBLocked.configpage.check.globalips, true);
var saveBtn = document.createElement('input');
bodyDiv.appendChild(saveBtn);
saveBtn.id = 'mblp-save';
saveBtn.type = 'button';
saveBtn.style.marginTop = '1em';
saveBtn.value = MarkBLocked.configpage.save.button;
/**
* @param {HTMLElement} appendTo
* @param {string} id
* @returns {HTMLParagraphElement}
*/
var createHiddenP = function(appendTo, id) {
var p = document.createElement('p');
appendTo.appendChild(p);
p.id = id;
p.style.display = 'none';
return p;
};
var status = createHiddenP(bodyDiv, 'mblp-status');
var lastsaved = createHiddenP(bodyDiv, 'mblp-lastsaved');
// Replace body content. Easier to just replace mw.util.$content[0].innerHTML, but this would remove #p-cactions etc.
var bodyContent = document.querySelector('.mw-body-content') || mw.util.$content[0];
bodyContent.replaceChildren(container);
var firstHeading = document.querySelector('.mw-first-heading');
if (firstHeading) { // The innerHTML of .mw-body-content was replaced
firstHeading.textContent = MarkBLocked.configpage.heading;
} else { // The innerHTML of mw.util.$content[0] was replaced (in this case the heading is gone)
var h1 = document.createElement('h1');
h1.textContent = MarkBLocked.configpage.heading;
container.prepend(h1);
}
/** @param {boolean} disable */
var toggleDisabled = function(disable) {
[tooltip, localips, globalusers, globalips, saveBtn].forEach(function(el) {
el.disabled = disable;
});
};
var msgTimeout;
saveBtn.addEventListener('click', function() {
clearTimeout(msgTimeout);
toggleDisabled(true);
status.style.display = 'block';
status.innerHTML = MarkBLocked.configpage.save.doing + ' ' + MarkBLocked.images.loading;
$.extend(MarkBLocked.prefs, {
tooltip: tooltip.value,
localips: localips.checked,
globalusers: globalusers.checked,
globalips: globalips.checked
});
var newPrefsStr = JSON.stringify(MarkBLocked.prefs);
// API call to save the preferences
api.saveOption(MarkBLocked.saveOptionName, newPrefsStr)
.then(function() { // Success
status.innerHTML = MarkBLocked.configpage.save.done + ' ' + MarkBLocked.images.check;
lastsaved.style.display = 'block';
lastsaved.textContent = MarkBLocked.configpage.save.lastsave + ' ' + new Date().toJSON().split('.')[0];
mw.user.options.set(MarkBLocked.saveOptionName, newPrefsStr);
}).catch(function(code, err) { // Failure
mw.log.error(err);
status.innerHTML = MarkBLocked.configpage.save.failed + ' ' + MarkBLocked.images.cross;
}).then(function() {
toggleDisabled(false);
msgTimeout = setTimeout(function() { // Hide the progress message after 3.5 seconds
status.style.display = 'none';
status.innerHTML = '';
}, 3500);
});
});
},
/**
* @type {{article: RegExp, script: RegExp, user: RegExp}}
* @private
*/
// @ts-ignore
_regex: {},
/**
* @returns {{article: RegExp, script: RegExp, user: RegExp}}
*/
getRegex: function() {
if ($.isEmptyObject(MarkBLocked._regex)) {
var user = '(?:' + MarkBLocked.getAliases([2, 3], '|') + '):';
var contribs_CA = MarkBLocked.contribs_CA.length === 0 ? '' : '|' + MarkBLocked.contribs_CA.join('|');
contribs_CA = '(?:' + MarkBLocked.getAliases([-1], '|') + '):(?:contrib(?:ution)?s|ca|centralauth' + contribs_CA + ')/';
MarkBLocked._regex = {
article: new RegExp(mw.config.get('wgArticlePath').replace('$1', '([^#?]+)')), // '/wiki/PAGENAME'
script: new RegExp(mw.config.get('wgScript') + '\\?title=([^#&]+)'), // '/w/index.php?title=PAGENAME'
user: new RegExp('^(?:' + user + '|' + contribs_CA + ')([^/#]+|[a-f\\d:\\.]+/\\d\\d)$', 'i')
};
}
return MarkBLocked._regex;
},
/**
* @typedef UserLinks
* @type {Object}
* @property {Array<HTMLAnchorElement>} processed - Links containing the user that are already processed
* @property {Array<HTMLAnchorElement>} all - Links containing the user
*/
/**
* @typedef UserDetails
* @type {Object}
* @property {UserLinks} links - Links containing the user
* @property {UserStatus} status - (B)lock statuses for user
*/
// Memory usage remains a question, but I think it should be fine
/** {'username': {'links': {'processed': [], 'all': [<link1>, ...], 'status': {'rangeblock': {'api': api, 'xhr': xhr}, ...}, ...}, */
/** @type {Map<string, UserDetails>} */
usersMap: new Map(),
/** Function to find all user links that match the RegExps and save them into 'usersMap', and then send API requests to check (b)lock status */
markupUserLinks: async function() {
/** @type {Array<HTMLAnchorElement>} */
var anchors = Array.prototype.slice.call(mw.util.$content[0].getElementsByTagName('a'));
// Additional anchors outside the content body
var contribsToolLinks = document.querySelector('.mw-contributions-user-tools');
var pNamespaces = document.getElementById('p-namespaces');
[contribsToolLinks, pNamespaces].forEach(function(wrapper) {
if (!wrapper) return;
anchors = anchors.concat(Array.prototype.slice.call(wrapper.getElementsByTagName('a')));
});
if (!anchors.length) return;
var regex = MarkBLocked.getRegex();
/** @type {Set<string>} */
const regUsers = new Set();
/** @type {Set<string>} */
const ips = new Set();
const ignoredClasses = /\bmw-changeslist-/;
const ignoredClassesPr = /\bmw-(history|rollback)-|\bautocomment/;
anchors.forEach((a) => { // Loop through all links in the page content
// Ignore some anchors
if (a.type === 'button' || a.role === 'button') return;
var firstParent, secondParent;
if (ignoredClasses.test(a.className) ||
(firstParent = a.parentElement) && ignoredClassesPr.test(firstParent.className) ||
// cur/prev revision links
firstParent && (secondParent = firstParent.parentElement) && secondParent.classList.contains('mw-history-histlinks') && secondParent.classList.contains('mw-changeslist-links')
) {
return;
}
var hrefUrl = a.href;
if (!hrefUrl || hrefUrl[0] === '#') return;
var m, pageTitle;
if ((m = regex.article.exec(hrefUrl))) {
pageTitle = m[1];
} else if ((m = regex.script.exec(hrefUrl))) {
pageTitle = m[1];
} else {
return;
}
pageTitle = decodeURIComponent(pageTitle).replace(/ /g, '_'); // Was previously (/_/g, ' ');
// Extract a username from the page title
if (!(m = regex.user.exec(pageTitle))) return;
var username = m[1].replace(/_/g, ' ');
if (mw.util.isIPAddress(username, true)) {
if (mw.util.isIPv6Address(username, true)) username = username.toUpperCase(); // Alphabets in IPv6s are case-insensitive
ips.add(username);
} else {
// Ensure the username doesn't contain characters that can't be used for usernames (do this here or block status query might fail)
if (/[/@#<>[\]|{}:]|^(\d{1,3}\.){3}\d{1,3}$/.test(username)) {
return;
} else {
username = username.slice(0, 1).toUpperCase() + username.slice(1); // Capitalize 1st letter: required for links like [[Special:Contribs/user]]
regUsers.add(username);
}
}
// Add a class to this anchor and save the anchor into the map
a.classList.add('mbl-userlink');
if (!MarkBLocked.usersMap.has(username)) MarkBLocked.usersMap.set(username, { links: { processed: [], all: [] }, status: {} });
if (!MarkBLocked.usersMap.get(username).links.all.includes(a)) {
MarkBLocked.usersMap.get(username).links.all.push(a);
}
});
if (MarkBLocked.usersMap.size === 0 || (regUsers.size === 0 && ips.size === 0)) return;
const lockedUsersPromise = async (regUsers) => { if (MarkBLocked.prefs.globalusers) return MarkBLocked.markLockedUsers(regUsers); };
const blockedIPsPromise = async (ips) => { if (MarkBLocked.prefs.localips) return MarkBLocked.markBlockedIPs(ips); };
const gBlockedIPsPromise = async (ips) => { if (MarkBLocked.prefs.globalips) return MarkBLocked.markGloballyBlockedIps(ips); };
// Check (b)lock status and mark up the links if needed
await Promise.all([
MarkBLocked.markBlockedUsers(regUsers),
lockedUsersPromise(regUsers),
blockedIPsPromise(ips),
gBlockedIPsPromise(ips)
]).then(() => {
MarkBLocked.usersMap.forEach((userDetails, userName) => {
MarkBLocked.usersMap.get(userName).links = {
processed: [...userDetails.links.all],
all: []
};
});
});
},
/**
* Get the local block status of registered users and single IPs (this can't detect single IPs included in blocked IP ranges)
* @param {Set<string>} referenceUsersSet
* @return {Array<Promise>} apiPromises
*/
markBlockedUsers: async function(referenceUsersSet) {
const parseQueryResponse = (res, xhr) => {
let resBlock;
if (!res || !res.query || !(resBlock = res.query.blocks) || resBlock.length === 0) return;
const serverTime = new Date(xhr.getResponseHeader('Date'));
resBlock.forEach(function(block) {
MarkBLocked.markBlock(undefined, serverTime, block, false, false);
});
};
// Create shallow copy
const usersArray = Array.from(referenceUsersSet);
const bkLimit = MarkBLocked.hasApiHighlimits ? 500 : 50; // Better performance for users with 'apiHighLimits'
/** @type {Array<Promise>} **/
const apiPromises = [];
// Send API calls
while (usersArray.length > 0) {
apiPromises.push(api.post({ // This MUST be a POST request because the parameters can exceed the word count limit of URI
action: 'query',
list: 'blocks',
bklimit: bkLimit,
bkusers: usersArray.splice(0, bkLimit).join('|'),
bkprop: 'user|by|timestamp|expiry|reason|restrictions',
formatversion: '2'
}).done(parseQueryResponse)
.fail(function(code, err) {
mw.log.error(err.error.info);
}));
}
return Promise.all(apiPromises);
},
/**
* Function to check whether single IPs are locally blocked AND whether they are included in any locally-blocked IP range
* @param {Set<string>} ipsSet
* @return {Array<Promise>} apiPromises
*/
markBlockedIPs: async function(ipsSet) {
const parseQueryResponse = (currentIP, res, xhr) => {
let resBlock;
// noinspection JSUnresolvedVariable
if (!res || !res.query || !(resBlock = res.query.blocks) || resBlock.length === 0) return;
const serverTime = new Date(xhr.getResponseHeader('Date'));
resBlock.forEach(function(block) {
MarkBLocked.markBlock(currentIP, serverTime, block, block.rangestart !== block.rangeend, false);
});
};
const ipsArray = [...ipsSet];
const fetchedIPs = ipsArray.filter((currentIP) => 'rangeblock' in MarkBLocked.usersMap.get(currentIP).status);
fetchedIPs.forEach((currentIP) => {
const ipRangeBlockInfo = MarkBLocked.usersMap.get(currentIP).status.rangeblock;
parseQueryResponse(currentIP, ipRangeBlockInfo.api, ipRangeBlockInfo.xhr);
});
return Promise.all(ipsArray.filter((currentIP) => !fetchedIPs.includes(currentIP)).map((currentIP) => {
return api.post({
action: 'query',
list: 'blocks',
bklimit: '10',
bkip: currentIP,
bkprop: 'user|by|timestamp|expiry|range|reason|restrictions',
formatversion: '2'
}).done(function(res, xhr) {
parseQueryResponse(currentIP, res, xhr);
MarkBLocked.usersMap.get(currentIP).status.rangeblock = { api: res, xhr };
}).fail(function(code, err) {
mw.log.error(err.error.info);
});
}));
},
/**
* Get the global lock status of registered users
* @param {Set<string>} regUsersSet
* @return {Array<Promise>} apiPromises
*/
markLockedUsers: async function(regUsersSet) {
const parseQueryResponse = (currentUser, res) => {
// If the length is 0, then we couldn't find the log event
if (res.query.logevents.length > 0) {
const logEvent = res.query.logevents[0];
if (logEvent.params.added && logEvent.params.added.includes('locked')) {
const prettyTimestamp = MarkBLocked.formatTime(new Date(), new Date(logEvent.timestamp));
let tooltip = MarkBLocked.BuildTooltip('locked', '', currentUser, logEvent.user, logEvent.comment, prettyTimestamp);
const userLinks = MarkBLocked.usersMap.get(currentUser).links;
const links = userLinks.all.filter((link) => !userLinks.processed.includes(link));
links.forEach((link) => {
link.classList.add('mbl-globally-locked');
tooltip = tooltip.replace('$prev_text', link.getAttribute('title') || currentUser);
link.setAttribute('title', tooltip);
});
}
}
};
const usersArray = [...regUsersSet];
const fetchedUsers = usersArray.filter((currentUser) => 'lock' in MarkBLocked.usersMap.get(currentUser).status);
fetchedUsers.forEach((currentUser) => {
const userLockInfo = MarkBLocked.usersMap.get(currentUser).status.lock;
parseQueryResponse(currentUser, userLockInfo.api);
});
return Promise.all(usersArray.filter((currentUser) => !fetchedUsers.includes(currentUser)).map((regUser) => {
return metaApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment|details',
leaction: 'globalauth/setstatus',
letitle: `User:${regUser}@global`,
formatversion: '2'
}).done((res) => {
parseQueryResponse(regUser, res);
MarkBLocked.usersMap.get(regUser).status.lock = { api: res };
}).fail((code, err) => {
mw.log.error(err.error.info);
});
}));
},
/**
* Get the global block status of IPs; can check whether single IPs are included in globally-blocked IP ranges
* @param {Set<string>} ipsSet
* @return {Array<Promise>} apiPromises
*/
markGloballyBlockedIps: async function(ipsSet) {
const parseQueryResponse = (currentIP, res, xhr) => {
// noinspection DuplicatedCode
let resBlock;
if (!res || !res.query || !(resBlock = res.query.globalblocks) || resBlock.length === 0) return;
const serverTime = new Date(xhr.getResponseHeader('Date'));
resBlock.forEach(function(block) {
MarkBLocked.markBlock(currentIP, serverTime, block, block.rangestart !== block.rangeend, true);
});
};
const ipsArray = [...ipsSet];
const fetchedIPs = ipsArray.filter((currentIP) => 'globalblock' in MarkBLocked.usersMap.get(currentIP).status);
fetchedIPs.forEach((currentIP) => {
const ipGlobalBlockInfo = MarkBLocked.usersMap.get(currentIP).status.globalblock;
parseQueryResponse(currentIP, ipGlobalBlockInfo.api, ipGlobalBlockInfo.xhr);
});
return Promise.all(ipsArray.filter((currentIP) => !fetchedIPs.includes(currentIP)).map(function(currentIP) {
return api.post({
action: 'query',
list: 'globalblocks',
bglimit: '10',
bgip: currentIP,
bgprop: 'address|by|timestamp|expiry|range|reason',
formatversion: '2'
}).done(function(res, xhr) {
parseQueryResponse(currentIP, res, xhr);
MarkBLocked.usersMap.get(currentIP).status.globalblock = { api: res, xhr };
}).fail(function(code, err) {
mw.log.error(err.error.info);
});
}));
},
/** Marks the link with the block from all the information provided
* @param {string} userLinkName Name of user to mark for
* @param {Date} serverTime Time the server gave us
* @param {object} block
* @param {boolean} rangeblock Whether it's a rangeblock
* @param {boolean} globalBlock Whether it's a global block
*/
markBlock: function(userLinkName, serverTime, block, rangeblock, globalBlock) {
let linkClass;
let blockType = rangeblock ? 'rangeblocked' : 'blocked';
// noinspection JSUnresolvedVariable
const partialBlock = block.restrictions && !Array.isArray(block.restrictions); // Boolean: True if partial block
if (partialBlock) {
blockType = 'partially ' + blockType;
linkClass = 'mbl-blocked-partial';
}
if (globalBlock) blockType = 'globally ' + blockType;
let blockDuration = block.expiry;
const blockTimestamp = new Date(block.timestamp);
if (block.expiry.includes('in')) { // Has the value 'infinity' if blocked indefinitely
if (!partialBlock) linkClass = globalBlock ? 'mbl-globally-blocked-indef' : 'mbl-blocked-indef';
} else {
if (!partialBlock) linkClass = globalBlock ? 'mbl-globally-blocked-temp' : 'mbl-blocked-temp';
blockDuration = MarkBLocked.formatTime(new Date(block.expiry), blockTimestamp);
}
// block.address for globally blocked IPs
const blockTarget = block.user || block.address;
// noinspection JSUnresolvedVariable
let tooltip = MarkBLocked.BuildTooltip(blockType, blockDuration, blockTarget, block.by, block.reason, MarkBLocked.formatTime(serverTime, blockTimestamp));
const userLinks = MarkBLocked.usersMap.get(userLinkName || blockTarget).links; // Get all links related to the user
const links = userLinks.all.filter((link) => !userLinks.processed.includes(link));
links.forEach((link) => {
link.classList.add(linkClass);
tooltip = tooltip.replace('$prev_text', link.getAttribute('title') || blockTarget);
link.setAttribute('title', tooltip);
});
},
/** Builds tooltip from provided values
* @param {string} actionType Type of action, i.e. 'globally blocked', 'rangeblocked', etc.
* @param {string} actionDuration Prettified duration of (b)lock, e.g. 3d
* @param {string} actionTarget (B)lock target
* @param {string} actionPerformer (B)lock performer
* @param {string} actionReason (B)lock reason
* @param {string} actionTimeSince Time since (b)lock occured
* @return {string} A formatted tooltip to display
*/
BuildTooltip: function(actionType, actionDuration, actionTarget, actionPerformer, actionReason, actionTimeSince) {
let tooltip = MarkBLocked.prefs.tooltip;
if (actionDuration === '') {
tooltip = tooltip.replace(' ($duration)', ''); // Bad.
}
return tooltip
.replace('$action_type', actionType)
.replace('$target', actionTarget)
.replace('$duration', actionDuration)
.replace('$blocker', actionPerformer)
.replace('$reason', actionReason)
.replace('$time', actionTimeSince);
},
/**
* Formats time from existingTime to targetTime, e.g. 259200 seconds -> 3d
* @param {Date} startTime Time to start the comparison at
* @param {Date} targetTime Time to end the comparison at
* @returns {string} Prettified time string
*/
formatTime: function(startTime, targetTime) {
const padNumber = (number) => number <= 9 ? '0' + number : number;
const timeDifference = startTime - targetTime;
let minutes = Math.floor(timeDifference / 60000);
if (!minutes) {
return Math.floor(timeDifference / 1000) + 's';
}
let hours = Math.floor(minutes / 60);
minutes %= 60;
const days = Math.floor(hours / 24);
hours %= 24;
if (days) {
return `${days}${(days < 10 ? '.' + padNumber(hours) : '')}d`;
}
return `${hours}:${padNumber(minutes)}`;
}
};
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.user']), $.ready).then(MarkBLocked.init);
// *******************************************************************************************************************
// @ts-ignore "Cannot find name 'mediaWiki'."
})(mediaWiki, jQuery);
//</nowiki>