mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function() {
if (mw.config.get('wgCanonicalSpecialPageName') === 'Contributions') {
const userToolsBDI = document.querySelector('.mw-contributions-user-tools bdi');
const userName = userToolsBDI ? userToolsBDI.textContent.trim() : '';
const ip = extractIP(userName);
if (ip) {
addContributionsLinks(ip);
}
} else if (mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage') {
const pageName = mw.config.get('wgPageName') || '';
const match = pageName.match(/^Special:BlankPage\/RangeBlocks\/(.+)$/);
if (match) {
const ip = extractIP(match[1]);
if (ip) {
addRangeBlocks(ip);
}
}
} else if (mw.config.get('wgPageName') === 'Special:Log/block') {
const pageParam = mw.util.getParamValue('page');
if (pageParam) {
const match = pageParam.match(/^User:(.+)$/);
if (match) {
const ip = extractIP(match[1]);
if (ip) {
mw.util.addPortletLink('p-tb', `/wiki/Special:BlankPage/RangeBlocks/${ip.ip}`, "Find range blocks");
}
}
}
}
return;
// adds links to user tools
function addContributionsLinks(ip) {
const userToolsContainer = document.querySelector('.mw-contributions-user-tools .mw-changeslist-links');
if (!userToolsContainer) return;
const spans = userToolsContainer.querySelectorAll('span');
let insertBefore = null;
for (const span of spans) {
if (span.textContent.toLowerCase().includes('global')) {
insertBefore = span;
break;
}
}
if (!insertBefore) return;
let floor = 16, ceiling = 24, steps = 8;
if (ip.version === 6) {
floor = 32;
ceiling = 64;
if (ip.mask >= 64)
steps = 16;
}
let links = [];
const rangeBlockLink = document.createElement('a');
rangeBlockLink.href = `/wiki/Special:BlankPage/RangeBlocks/${ip.ip}`;
rangeBlockLink.textContent = 'range block log';
rangeBlockLink.className = 'mw-link-range-blocks';
links.push(rangeBlockLink);
for (let mask = floor; mask <= ceiling && mask < ip.mask; mask += steps) {
const contribsLink = document.createElement('a');
contribsLink.href = `/wiki/Special:Contributions/${ip.ip}/${mask}`;
contribsLink.textContent = `/${mask}`;
contribsLink.className = 'mw-contributions-link-range-suggestion';
links.push(contribsLink)
}
links.forEach(link => {
const span = document.createElement('span');
span.appendChild(link);
userToolsContainer.insertBefore(span, insertBefore);
});
}
// find range blocks
async function addRangeBlocks(ip) {
document.title = `Range blocks for ${ip.ip}`;
const heading = document.querySelector('#firstHeading');
if (heading) {
heading.innerHTML = `Range blocks for <a href="/wiki/Special:Contributions/${ip.ip}">${ip.ip}</a>`;
}
const contentContainer = document.querySelector('#mw-content-text');
if (!contentContainer) return;
contentContainer.innerHTML = '';
let masks;
if (ip.version === 6) {
masks = Array.from({ length: 46 }, (_, i) => 64 - i);
} else {
masks = Array.from({ length: 16 }, (_, i) => 31 - i);
}
const api = new mw.Api();
const statusMessage = document.createElement('p');
statusMessage.textContent = 'Querying logs for relevant IP range blocks...';
contentContainer.appendChild(statusMessage);
const resultsList = document.createElement('ul');
contentContainer.appendChild(resultsList);
mw.hook('wikipage.content').fire($(contentContainer));
let foundBlocks = false;
const allBlocks = {}
for (const mask of masks) {
const range = ip.version === 6 ? maskedIPv6(ip.ip, mask) : maskedIPv4(ip.ip, mask);
const blocks = await getBlockLogs(api, range);
if (Object.keys(blocks).length > 0) {
foundBlocks = true;
Object.assign(allBlocks, blocks);
resultsList.innerHTML = '';
const sortedBlocks = Object.values(allBlocks).sort((a, b) => b.timestamp > a.timestamp ? 1 : -1);
for (const block of sortedBlocks) {
const li = document.createElement('li');
li.innerHTML = `<a href="${block.url}">${block.timestamp}</a> <a href="/wiki/User:${encodeURIComponent(block.user)}">${block.user}</a> blocked <a href="/wiki/Special:Contributions/${encodeURIComponent(block.range)}">${block.range}</a> (${block.expiry})`;
resultsList.appendChild(li);
}
}
mw.hook('wikipage.content').fire($(resultsList));
}
statusMessage.textContent = foundBlocks ? 'Range blocks:' : 'No blocks found.';
mw.hook('wikipage.content').fire($(contentContainer));
}
// query API for blocks
async function getBlockLogs(api, range) {
const response = await api.get({
action: 'query',
list: 'logevents',
letype: 'block',
letitle: `User:${range}`,
format: 'json'
});
const blocksObj = {};
response.query.logevents.forEach(event => {
blocksObj[event.logid] = {
timestamp: event.timestamp,
user: event.user,
expiry: event.params.duration || 'indefinite',
url: mw.util.getUrl('Special:Log', { logid: event.logid }),
range: range,
};
});
return blocksObj;
}
// extract IP address
function extractIP(userName) {
const IPV4REGEX = /^((?:1?\d\d?|2[0-2]\d)\b(?:\.(?:1?\d\d?|2[0-4]\d|25[0-5])){3})(?:\/(1[6-9]|2\d|3[0-2]))?$/;
const IPV6REGEX = /^((?:[\dA-Fa-f]{1,4}:){2,7}(?:[\dA-Fa-f]{1,4}|:)?)(?:\/(19|[2-9]\d|1[01]\d|12[0-8]))?$/;
let match;
if ((match = IPV4REGEX.exec(userName) || IPV6REGEX.exec(userName))) {
const version = match[1].includes(':') ? 6 : 4;
const ip = match[1];
const mask = parseInt(match[2] || (version == 6 ? '128' : '32'), 10);
return { version, ip, mask };
}
return null;
}
// convert full IPv6 address to BigInt
function ipv6ToBigInt(ipv6) {
const segments = ipv6.split(':');
let bigIntValue = 0n;
const expanded = expandIPv6(segments);
expanded.forEach(segment => {
bigIntValue = (bigIntValue << 16n) + BigInt(parseInt(segment, 16));
});
return bigIntValue;
}
// expand shorthand IPv6 (e.g., '::1' to '0:0:0:0:0:0:0:1')
function expandIPv6(segments) {
const expanded = [];
let hasEmpty = false;
segments.forEach(segment => {
if (segment === '' && !hasEmpty) {
expanded.push(...Array(8 - segments.filter(s => s).length).fill('0'));
hasEmpty = true;
} else if (segment === '') {
expanded.push('0');
} else {
expanded.push(segment);
}
});
return expanded.map(seg => seg.padStart(4, '0'));
}
// apply mask to BigInt for IPv6
function applyMask(bigIntValue, prefixLength) {
const maskBits = 128 - prefixLength;
const mask = (1n << BigInt(128 - maskBits)) - 1n;
return bigIntValue & (mask << BigInt(maskBits));
}
// convert BigInt back to IPv6 string
function bigIntToIPv6(bigIntValue, prefixLength = 128) {
const segments = [];
for (let i = 0; i < 8; i++) {
const segment = (bigIntValue >> BigInt((7 - i) * 16)) & 0xffffn;
segments.push(segment.toString(16));
}
const ipv6 = segments.join(':').replace(/(^|:)0(:0)+(:|$)/, '::');
return ipv6 + (prefixLength < 128 ? `/${prefixLength}` : '');
}
// generate masked IPv6 range
function maskedIPv6(ipv6, prefixLength) {
const bigIntValue = ipv6ToBigInt(ipv6);
const maskedBigInt = applyMask(bigIntValue, prefixLength);
return bigIntToIPv6(maskedBigInt, prefixLength);
}
// generate masked IPv4 range
function maskedIPv4(ipv4, prefixLength) {
const segments = ipv4.split('.').map(Number);
const ipInt = (segments[0] << 24) | (segments[1] << 16) | (segments[2] << 8) | segments[3];
const mask = (1 << (32 - prefixLength)) - 1;
const maskedIpInt = ipInt & ~mask;
return [
(maskedIpInt >>> 24) & 0xff,
(maskedIpInt >>> 16) & 0xff,
(maskedIpInt >>> 8) & 0xff,
maskedIpInt & 0xff
].join('.') + `/${prefixLength}`;
}
});