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:54, 23 April 2025 (move range blocks to separate page, add range links to contributions user tools). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
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('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}`;
	}
});