Content deleted Content added
minor fixes |
refactor IP code into IPAddress class |
||
Line 1:
class IPAddress {
static from(input) {
if (typeof input !== 'string') return null;
try {
const parsed = IPAddress.#parse(input);
return parsed ? new IPAddress(parsed) : null;
} catch {
return null;
}
}
constructor({ version, ip, mask }) {
this.version = version;
this.ip = ip;
this.mask = mask;
this.effectiveMask = mask ?? (version === 4 ? 32 : 128);
}
equals(other) {
return other instanceof IPAddress &&
this.version === other.version &&
this.ip === other.ip &&
this.effectiveMask === other.effectiveMask;
}
masked(prefixLength) {
const size = this.version === 4 ? 32 : 128;
const mask = (1n << BigInt(size - prefixLength)) - 1n;
const maskedIP = this.ip & ~mask;
return new IPAddress({
ip: maskedIP,
mask: prefixLength,
version: this.version
});
}
toString(uppercase = true, compress = false) {
let ipString = this.version === 4
? IPAddress.#bigIntToIPv4(this.ip)
: IPAddress.#bigIntToIPv6(this.ip);
if (compress && this.version === 6) {
ipString = IPAddress.#compressIPv6(ipString);
}
if (this.mask !== null) {
ipString += `/${this.mask}`;
}
return uppercase ? ipString.toUpperCase() : ipString;
}
static #parse(input) {
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}:){7}[\dA-Fa-f]{1,4}|(?:[\dA-Fa-f]{1,4}:){1,7}:|(?:[\dA-Fa-f]{1,4}:){1,6}:[\dA-Fa-f]{1,4}|(?:[\dA-Fa-f]{1,4}:){1,5}(?:\:[\dA-Fa-f]{1,4}){1,2}|(?:[\dA-Fa-f]{1,4}:){1,4}(?:\:[\dA-Fa-f]{1,4}){1,3}|(?:[\dA-Fa-f]{1,4}:){1,3}(?:\:[\dA-Fa-f]{1,4}){1,4}|(?:[\dA-Fa-f]{1,4}:){1,2}(?:\:[\dA-Fa-f]{1,4}){1,5}|[\dA-Fa-f]{1,4}:(?:(?:\:[\dA-Fa-f]{1,4}){1,6}))(?:\/(19|[2-9]\d|1[01]\d|12[0-8]))?$/; // based on https://stackoverflow.com/a/17871737
const match = IPV4REGEX.exec(input) || IPV6REGEX.exec(input);
if (match) {
const version = match[1].includes(':') ? 6 : 4;
const ip = version === 4 ? IPAddress.#ipv4ToBigInt(match[1]) : IPAddress.#ipv6ToBigInt(match[1]);
const mask = match[2] ? parseInt(match[2], 10) : null;
return { version, ip, mask };
}
return null;
}
static #ipv4ToBigInt(ipv4) {
const octets = ipv4.split('.').map(BigInt);
return (octets[0] << 24n) | (octets[1] << 16n) | (octets[2] << 8n) | octets[3];
}
static #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'));
}
static #ipv6ToBigInt(ipv6) {
const segments = ipv6.split(':');
let bigIntValue = 0n;
const expanded = IPAddress.#expandIPv6(segments);
expanded.forEach(segment => {
bigIntValue = (bigIntValue << 16n) + BigInt(parseInt(segment, 16));
});
return bigIntValue;
}
static #bigIntToIPv4(bigIntValue) {
return [
(bigIntValue >> 24n) & 255n,
(bigIntValue >> 16n) & 255n,
(bigIntValue >> 8n) & 255n,
bigIntValue & 255n,
].join('.');
}
static #bigIntToIPv6(bigIntValue) {
const segments = [];
for (let i = 0; i < 8; i++) {
const segment = (bigIntValue >> BigInt((7 - i) * 16)) & 0xffffn;
segments.push(segment.toString(16));
}
return segments.join(':')
}
static #compressIPv6(ipv6) {
const zeroBlocks = ipv6.match(/\b0(?:\:0)+\b/g);
if (!zeroBlocks) return ipv6;
const longestZeroBlock = zeroBlocks.reduce((longest, current) => {
return current.length > longest.length ? current : longest;
}, "");
if (longestZeroBlock) {
return ipv6.replace(new RegExp(`:?\\b${longestZeroBlock}\\b:?`), '::');
}
return ipv6;
}
}
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter']).then(function() {
// state variables
Line 11 ⟶ 135:
const userToolsBDI = document.querySelector('.mw-contributions-user-tools bdi');
const userName = userToolsBDI ? userToolsBDI.textContent.trim() : '';
const ip =
if (ip) {
addContributionsLinks(ip
}
} else if (specialPage === 'Blankpage') {
const match = pageName.match(/^Special:BlankPage\/(\w+)(?:\/(.+))?$/);
if (!match)
}
} else if (match[1] === 'RangeCalculator') {
displayRangeCalculator();
}
} else if (pageName === 'Special:Log/block') {
Line 31 ⟶ 154:
if (pageParam) {
const match = pageParam.match(/^User:(.+)$/);
if (!match)
mw.util.addPortletLink('p-tb', `/wiki/Special:BlankPage/RangeBlocks/${ip}`, "Find range blocks");
}
}
Line 46 ⟶ 164:
// adds links to user tools
function addContributionsLinks(ip
const userToolsContainer = document.querySelector('.mw-contributions-user-tools .mw-changeslist-links');
if (!userToolsContainer) return;
Line 52 ⟶ 170:
if (blockLogLink) {
const rangeLogLink = document.createElement('a');
const rangeLogPage = `Special:BlankPage/RangeBlocks/${ip}`;
rangeLogLink.href = `/wiki/${rangeLogPage}`;
rangeLogLink.textContent = '(ranges)';
Line 75 ⟶ 189:
}
if (!insertBefore) return;
const steps = ip.effectiveMask >= 64 ? 16 : 8;
for (let mask = floor; mask <= ceiling; mask += steps) {
const contribsLink = document.createElement('a');
contribsLink.href = `/wiki/Special:Contributions/${ip.masked(mask)}`;
contribsLink.textContent = `/${mask}`;
contribsLink.className = 'mw-contributions-link-range-suggestion';
Line 92 ⟶ 201:
userToolsContainer.insertBefore(span, insertBefore);
}
if (ip.mask
mw.util.addPortletLink('p-tb', '/wiki/Special:BlankPage/RangeCalculator', 'Range calculator');
mw.util.addPortletLink('p-tb', '#', 'Range selector')
Line 124 ⟶ 233:
return '<span style="color:red;">Mixed IPv4 and IPv6 addresses are not supported.</span>';
}
const masks = firstVersion ===
const bestMask = masks.findLast(m => {
const base =
return ips.every(ip =>
});
if (!bestMask) {
return '<span style="color:red;">No common range found.</span>';
}
const resultRange =
const contribsLink = `<a href="/wiki/Special:Contributions/${resultRange}" target="_blank">${resultRange}</a>`;
const blockLink = `<a href="/wiki/Special:Block/${resultRange}" target="_blank">block</a>`;
Line 169 ⟶ 278:
const input = document.getElementById('range-input').value;
const ipRegex = /\b(?:\d{1,3}(?:\.\d{1,3}){3})\b|\b(?:[\dA-Fa-f]{1,4}:){4,}[\dA-Fa-f:]+/g;
const ips = [];
input.matchAll(ipRegex)
results.innerHTML = computeCommonRange(ips);
});
Line 201 ⟶ 307:
checkbox.addEventListener('change', () => {
const ipText = link.textContent.trim();
const ip =
if (!ip) return;
if (checkbox.checked) {
Line 218 ⟶ 324:
api = new mw.Api();
formatTimeAndDate = mw.loader.require('mediawiki.DateFormatter').formatTimeAndDate;
document.title = `Range blocks for ${ip}`;
const heading = document.querySelector('#firstHeading');
if (heading) {
heading.innerHTML = `Range blocks for ${
}
const contentContainer = document.querySelector('#mw-content-text');
Line 228 ⟶ 333:
contentContainer.innerHTML = '';
const statusMessage = document.createElement('p');
statusMessage.innerHTML = `Querying logs for IP blocks affecting <a href="/wiki/Special:Contributions/${
contentContainer.appendChild(statusMessage);
const resultsList = document.createElement('ul');
contentContainer.appendChild(resultsList);
const masks = ip.version ===
if (!masks.includes(ip.
masks.push(ip.
}
const blocks = [];
const blockPromises = masks.map(mask => {
const range =
return getBlockLogs(api, range).then(async (blockLogs) => {
for (const block of blockLogs) {
Line 258 ⟶ 363:
statusMessage.innerHTML = '<span style="color:red;">No blocks found.</span>';
} else {
statusMessage.innerHTML = `Range blocks for <a href="/wiki/Special:Contributions/${
}
mw.hook('wikipage.content').fire($(contentContainer));
Line 280 ⟶ 385:
params: event.params || {},
url: mw.util.getUrl('Special:Log', { logid: event.logid }),
range: range
}));
}
Line 306 ⟶ 397:
// add IP to array
function ipListAdd(ipList, ip) {
if (!ipList.some(i => i.equals(ip)))
}
// remove IP from array
function ipListRemove(ipList, ip) {
const index = ipList.findIndex(i => i.equals(ip
if (index !== -1)
}
|