Jump to content

User:Daniel Quinlan/Scripts/RangeHelper.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Daniel Quinlan (talk | contribs) at 06:17, 13 November 2024 (create script). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
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}`;
	}
});