User:Daniel Quinlan/Scripts/RangeHelper.js: Difference between revisions

Content deleted Content added
query block log asynchronously, improve block entry formatting
add range calculation tools
Line 6:
 
// activate on relevant special pages
ifconst pageName = (mw.config.get('wgCanonicalSpecialPageNamewgPageName') === 'Contributions') {;
const specialPage = mw.config.get('wgCanonicalSpecialPageName');
if (specialPage === 'Contributions') {
const userToolsBDI = document.querySelector('.mw-contributions-user-tools bdi');
const userName = userToolsBDI ? userToolsBDI.textContent.trim() : '';
Line 12 ⟶ 14:
if (ip) {
addContributionsLinks(ip);
if (ip.mask !== (ip.version === 6 ? 128 : 32)) {
mw.util.addPortletLink('p-tb', '/wiki/Special:BlankPage/RangeCalculator', 'Range calculator');
mw.util.addPortletLink('p-tb', '#', 'Range selector')
.addEventListener('click', event => {
event.preventDefault();
startRangeSelection();
});
}
}
} else if (mw.config.get('wgCanonicalSpecialPageName')specialPage === 'Blankpage') {
const match = pageName.match(/^Special:BlankPage\/(\w+)(?:\/(.+))?$/);
const pageName = mw.config.get('wgPageName') || '';
const match = pageName.match(/^Special:BlankPage\/RangeBlocks\/(.+)$/);
if (match) {
constif ip = extractIP(match[1] === 'RangeBlocks'); {
const ip = extractIP(match[2] || '');
if (ip) {
addRangeBlocksif (ip); {
displayRangeBlocks(ip);
}
} else if (match[1] === 'RangeCalculator') {
displayRangeCalculator();
}
}
} else if (mw.config.get('wgPageName')pageName === 'Special:Log/block') {
const pageParam = mw.util.getParamValue('page');
if (pageParam) {
Line 73 ⟶ 86:
span.appendChild(link);
userToolsContainer.insertBefore(span, insertBefore);
});
}
 
// generate styled div for IP range calculation results
function createRangeDisplay() {
const display = document.createElement('div');
display.id = 'range-display';
display.style.fontWeight = 'bold';
display.style.border = '1px solid var(--border-color-base, #a2a9b1)';
display.style.borderRadius = '2px';
display.style.padding = '16px';
display.style.fontSize = '1rem';
display.style.margin = '1em 0';
return display;
}
 
// compute common IP range for IP list
function computeCommonRange(ips) {
if (!ips.length) {
return '<span style="color:red;">No valid IPs found.</span>';
}
const firstVersion = ips[0].version;
if (!ips.every(ip => ip.version === firstVersion)) {
return '<span style="color:red;">Mixed IPv4 and IPv6 addresses are not supported.</span>';
}
const masks = firstVersion === 6 ? sequence(19, 64) : sequence(16, 32);
const bestMask = masks.findLast(m => {
const base = maskedIP(ips[0], m);
return ips.every(ip => maskedIP(ip, m) === base);
});
if (!bestMask) {
return '<span style="color:red;">No common range found.</span>';
}
const resultRange = maskedIP(ips[0], bestMask);
const contribsLink = `<a href="/wiki/Special:Contributions/${resultRange}" target="_blank">${resultRange}</a>`;
const blockLink = `<a href="/wiki/Special:Block/${resultRange}" target="_blank">(block)</a>`;
return `<span>${ips.length} unique IP${ips.length !== 1 ? 's' : ''}: ${contribsLink} ${blockLink}</span>`;
}
 
// standalone range calculator
function displayRangeCalculator() {
document.title = 'Range calculator';
const heading = document.querySelector('#firstHeading');
if (heading) {
heading.innerHTML = 'Range calculator';
}
const contentContainer = document.querySelector('#mw-content-text');
if (!contentContainer) return;
contentContainer.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.innerHTML = `
<p>Calculate the smallest range that encompasses a given list of IP addresses.</p>
<fieldset>
<legend>Enter IP addresses (one per line or space-separated)</legend>
<textarea id="range-input" rows="10" style="width: 100%"></textarea>
</fieldset>
<div style="margin-top:10px;">
<button id="range-calculate">Calculate Range</button>
</div>
`;
contentContainer.appendChild(wrapper);
document.getElementById('range-calculate').addEventListener('click', event => {
event.preventDefault();
let results = document.getElementById('range-display');
if (!results) {
results = createRangeDisplay();
wrapper.appendChild(results);
}
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 matches = [...input.matchAll(ipRegex)].map(m => m[0]);
const ips = [];
for (const match of matches) {
const ip = extractIP(match);
if (ip) {
ipListAdd(ips, ip);
}
}
results.innerHTML = computeCommonRange(ips);
});
}
 
// select IPs to compute common IP range
function startRangeSelection() {
function updateRangeDisplay() {
if (!selectedIPs.length) {
display.innerHTML = 'No IPs selected.';
} else {
display.innerHTML = computeCommonRange(selectedIPs);
}
}
const selectedIPs = [];
const display = createRangeDisplay();
updateRangeDisplay();
document.querySelector('#mw-content-text')?.prepend(display);
document.querySelectorAll('a.mw-anonuserlink').forEach(link => {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.marginLeft = '0.5em';
checkbox.addEventListener('change', () => {
const ipText = link.textContent.trim();
const ip = extractIP(ipText);
if (!ip) return;
if (checkbox.checked) {
ipListAdd(selectedIPs, ip);
} else {
ipListRemove(selectedIPs, ip);
}
updateRangeDisplay();
});
link.parentNode?.insertBefore(checkbox, link.nextSibling);
});
}
 
// find range blocks
async function addRangeBlocksdisplayRangeBlocks(ip) {
api = new mw.Api();
formatTimeAndDate = mw.loader.require('mediawiki.DateFormatter').formatTimeAndDate;
document.title = `Range blocks for ${ip.ip}`;
const heading = document.querySelector('#firstHeading');
Line 86 ⟶ 212:
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);
}
api = new mw.Api();
const statusMessage = document.createElement('p');
statusMessage.textContentinnerHTML = '`Querying logs for relevant IP range blocks affecting <a href="/wiki/Special:Contributions/${ip.ip}">${ip.ip}</a>...'`;
contentContainer.appendChild(statusMessage);
const resultsList = document.createElement('ul');
contentContainer.appendChild(resultsList);
const masks = ip.version === 6 ? sequence(19, 64) : sequence(16, 31);
formatTimeAndDate = mw.loader.require('mediawiki.DateFormatter').formatTimeAndDate;
const blocks = [];
const blockPromises = masks.map(mask => {
const range = ip.version === 6 ? maskedIPv6maskedIP(ip.ip, mask) : maskedIPv4(ip.ip, mask);
return getBlockLogs(api, range).then(async (blockLogs) => {
for (const block of blockLogs) {
Line 118 ⟶ 237:
resultsList.appendChild(li);
});
if (!blocks.length) {
statusMessage.textContent = blocks.length ? 'Range blocks:' : 'No blocks found.';
statusMessage.innerHTML = '<span style="color:red;">No blocks found.</span>';
} else {
statusMessage.innerHTML = `Range blocks for <a href="/wiki/Special:Contributions/${ip.ip}">${ip.ip}</a>`;
}
mw.hook('wikipage.content').fire($(contentContainer));
}
Line 155 ⟶ 278:
}
return null;
}
 
// generate sequence of numbers
function sequence(n, m, step = 1) {
for (var i = n, r = []; i <= m; i += step) r.push(i);
return r;
}
 
// add IP to array
function ipListAdd(ipList, ip) {
if (!ipList.some(i => i.ip === ip.ip && i.mask === ip.mask && i.version === ip.version)) {
ipList.push(ip);
}
}
 
// remove IP from array
function ipListRemove(ipList, ip) {
const index = ipList.findIndex(i => i.ip === ip.ip && i.mask === ip.mask && i.version === ip.version);
if (index !== -1) {
ipList.splice(index, 1);
}
}
 
Line 201 ⟶ 345:
const ipv6 = segments.join(':').replace(/(^|:)0(:0)+(:|$)/, '::');
return ipv6 + (prefixLength < 128 ? `/${prefixLength}` : '');
}
 
// generate masked IP range
function maskedIP(ip, prefixLength) {
return ip.version === 6 ? maskedIPv6(ip.ip, prefixLength) : maskedIPv4(ip.ip, prefixLength);
}
 
Line 206 ⟶ 355:
function maskedIPv6(ipv6, prefixLength) {
const bigIntValue = ipv6ToBigInt(ipv6);
const maskedBigIntshift = applyMask(bigIntValue,128 - prefixLength);
const mask = (1n << BigInt(128 - shift)) - 1n;
return bigIntToIPv6(maskedBigInt, prefixLength);
const masked = bigIntValue & (mask << BigInt(shift));
return bigIntToIPv6(masked, prefixLength);
}
 
// generate masked IPv4 range
function maskedIPv4(ipv4, prefixLength) {
const segmentsoctets = ipv4.split('.').map(Number);
const ipIntintValue = (segmentsoctets[0] << 24) | (segmentsoctets[1] << 16) | (segmentsoctets[2] << 8) | segmentsoctets[3];
const mask = (1 << (32 - prefixLength)) - 1;
const maskedIpIntmasked = ipIntintValue & ~mask;
return [
(maskedIpIntmasked >>> 24) & 0xff,
(maskedIpIntmasked >>> 16) & 0xff,
(maskedIpIntmasked >>> 8) & 0xff,
maskedIpIntmasked & 0xff
].join('.') + `/${prefixLength}`;
}