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('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}`;
}
});