User:Daniel Quinlan/Scripts/RangeHelper.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | This user script seems to have a documentation page at User:Daniel Quinlan/Scripts/RangeHelper. |
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function() {
if (mw.config.get('wgPageName') !== 'Special:Log/block')
return;
// extract user
const pageParam = mw.util.getParamValue('page');
if (!pageParam || !pageParam.startsWith('User:')) return;
const IPV4REGEX = /^(?:1?\d\d?|2[0-2]\d)\b(?:\.(?:1?\d\d?|2[0-4]\d|25[0-5])){3}$/;
const IPV6REGEX = /^[\dA-Fa-f]{1,4}(?:\:[\dA-Fa-f]{1,4}){7}$/;
const user = pageParam.replace('User:', '');
// test user
const isIPv6 = IPV6REGEX.test(user);
if (!IPV4REGEX.test(user) && !isIPv6) {
return;
}
// add button
const link = mw.util.addPortletLink('p-tb', '#', "Find range blocks", 'ca-find-range-blocks');
link.addEventListener('click', function(event) {
event.preventDefault();
findRangeBlocks();
});
// find range blocks
async function findRangeBlocks() {
let netmasks;
if (isIPv6) {
netmasks = Array.from({ length: 46 }, (_, i) => 64 - i);
} else {
netmasks = Array.from({ length: 16 }, (_, i) => 31 - i);
}
const api = new mw.Api();
const contentContainer = document.querySelector('.mw-content-container');
if (!contentContainer) return;
const outputDiv = document.createElement('div');
outputDiv.style.marginTop = '1em';
const statusMessage = document.createElement('p');
statusMessage.textContent = 'Querying logs for relevant IP range blocks.';
outputDiv.appendChild(statusMessage);
const resultsList = document.createElement('ul');
outputDiv.appendChild(resultsList);
contentContainer.appendChild(outputDiv);
mw.hook('wikipage.content').fire($(outputDiv));
let foundBlocks = false;
for (const mask of netmasks) {
const range = isIPv6 ? maskedIPv6(user, mask) : maskedIPv4(user, mask);
const blocks = await getBlockLogs(api, range);
if (blocks.length) {
foundBlocks = true;
blocks.forEach(block => {
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(range)}">${range}</a> (${block.expiry})`;
resultsList.appendChild(li);
mw.hook('wikipage.content').fire($(resultsList));
});
}
}
statusMessage.textContent = foundBlocks
? 'Blocks affecting this IP:'
: 'No blocks found.';
mw.hook('wikipage.content').fire($(statusMessage));
}
// 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'
});
return response.query.logevents.map(event => ({
timestamp: event.timestamp,
user: event.user,
expiry: event.params.duration || 'indefinite',
url: mw.util.getUrl('Special:Log', { logid: event.logid })
}));
}
// 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}`;
}
});