มอดูล:IPblock

-- Calculate the minimum-sized blocks of IP addresses that cover each
-- IPv4 or IPv6 address entered in the arguments.
local bit32 = require('bit32')
local Collection  -- a table to hold items
Collection = {
	add = function (self, item)
		if item ~= nil then
			self.n = self.n + 1
			selfself.n = item
		end
	end,
	join = function (self, sep)
		return table.concat(self, sep)
	end,
	remove = function (self, pos)
		if self.n > 0 and (pos == nil or (0  limit then
			d = limit  -- good enough to ensure date is valid
		end
		timestamps'y-m-d' .. months = string.format('%d-%02d-%02d', y, m, d)
		timestamps'ymdHMS' .. months = string.format('%d%02d%02d000000', y, m, d)
	end
	return timestampskey or 
end
local note_text = {
	range = '*Links for ranges show the contributions in the previous %s.',
	gadget = =
*Contributions links for IPv6 ranges need the "Allow /16, /24 and /27 – /32 CIDR ranges on Special:Contributions forms" gadget enabled in Special:Preferences, and scripting enabled in the browser.=,
}
local function make_note(strings, key)
	-- Record the fact that a particular note is needed, and return
	-- wikitext for a link to the note or  if no link needed.
	if not strings.nonote then
		strings.notes = strings.notes or {}
		if not strings.noteskey then
			if key == 'gadget' then
				strings.noteskey = note_textkey
			elseif key == 'range' then
				local when = 'month'
				if strings.months > 1 then
					when = strings.months .. ' months'
				end
				strings.noteskey = string.format(note_text.range, when)
			else
				error('make_note: unexpected key')
			end
		end
		if key == 'gadget' then
			return ' #need-gadget|note'
		end
	end
	return 
end
local function describe_total(total, isalloc)
	-- Return text describing given number of addresses or /64 allocations.
	if total = alloc then
			return describe_total(total / alloc, true) .. ' /64'
		end
	end
	total = total/1024
	local suffix = 'K'
	if total >= 1024 then
		total = total/1024
		suffix = 'M'
		if total >= 1024 then
			total = total/1024
			suffix = 'G'
			if total > 64 then
				return '>64G'
			end
		end
	end
	return string.format('%.0f', total) .. suffix
end
local function describe_size(ipsize, size)
	-- Return text describing how many IPs are in a range with size = prefix length.
	local function numtext(n)
		if n 64G'
	end
	local host = ipsize - size
	if host  1 then
				if not z1 or zcount > z2 - z1 + 1 then
					z1 = zstart
					z2 = zstart + zcount - 1
				end
			end
			zstart = nil
			zcount = nil
		end
	end
	local parts = Collection.new()
	for i = 1, 8 do
		if z1 and z1  0 and s:match('^0') then
						-- A redundant leading zero is an error because it is for an IP in octal.
						return nil
					end
					partsi = num
				else
					return nil
				end
			else
				return nil
			end
		end
		local result = Collection.new()
		for i = 1, 3, 2 do
			result:add(partsi * 256 + partsi+1)
		end
		return setmetatable(result, ipmt)
	end
	return nil
end
local function ipv6_address(ip_str)
	-- Return a collection of eight 16-bit words (numbers) equivalent
	-- to the IPv6 address given as a colon-delimited string, or
	-- return nil if invalid.
	local _, n = ip_str:gsub(':', ':')
	if n  0 then
			if length >= 16 then
				resulti = ipi
				length = length - 16
			else
				resulti = bit32.band(ipi, bit32.arshift(0xffff8000, length - 1))
				length = 0
			end
		else
			resulti = 0
		end
	end
	return setmetatable(result, ipmt)
end
local function ip_incremented(ip)
	-- Return a new IP equal to ip + 1.
	-- Will wraparound (255.255.255.255 + 1 = 0.0.0.0)!
	local result = { n = ip.n }
	local carry = 1
	for i = ip.n, 1, -1 do
		local sum = ipi + carry
		if sum >= 0x10000 then
			carry = 1
			sum = sum - 0x10000
		else
			carry = 0
		end
		resulti = sum
	end
	return setmetatable(result, ipmt)
end
local function is_next_ip(ip1, ip2)
	-- Return true if ip2 is the next IP after ip1 (ip2 == ip1 + 1).
	-- IPs are sorted and unique so ip1 = 0x10000 then
				carry = 1
				sum = sum - 0x10000
			else
				carry = 0
			end
			if sum ~= ip2i then
				return false
			end
		end
		return true
	end
end
-- Each IP in a range except for the last IP has a 'common' field which is
-- a number specifying how many bits are common between the prefixes of this
-- IP and the next IP (0 if this IP starts with 0 and the next starts with 1).
-- Each non-empty range has exactly one "minimum common", that is, its value
-- of common is smaller than all others. That there is only one minimum common
-- follows from the fact that the IPs are unique and sorted.
local function make_range(iplist, ifirst, ilast)
	-- Return a table for the range of IPs from iplistifirst to iplistilast inclusive.
	local imin, vmin, done
	if ifirst  common then
					vmin = common
					imin = i
				end
			else
				vmin = common
				imin = i
			end
		end
	else
		vmin = iplist.ipsize
		imin = ifirst
		done = true
	end
	if vmin > iplist.allocation then
		-- For IPv6, the default allocation is /64 and there is no point having
		-- more precise ranges as they add unnecessary complexity.
		-- However, using results=all sets allocation = 128 so vmin is not changed.
		vmin = iplist.allocation
	end
	return {
		ifirst = ifirst,  -- index of first IP
		ilast = ilast,    -- index of last IP
		imin = imin,      -- index of IP with minimum common
		size = vmin,      -- number of common bits in prefix (the minimum)
		prefix = ip_prefix(iplistimin, vmin),  -- IP table of the base IP
		done = done,      -- true if know that this range cannot be improved
	}
end
local function split_range(iplist, range, depth)
	-- Return a table of two or more ranges that more precisely target
	-- the IPs in range, or return nothing if unable to improve range.
	depth = depth and depth + 1 or 0
	if depth  pointless or r2.size > pointless then
			return { r1, r2 }
		end
		local result = Collection.new()
		local function store_split(range)
			local split = split_range(iplist, range, depth)
			if split then
				for _, r in ipairs(split) do
					result:add(r)
				end
				return true
			else
				result:add(range)
			end
		end
		local improved1 = store_split(r1)
		local improved2 = store_split(r2)
		if improved1 or improved2 then
			return result
		end
	end
	range.done = true
end
local function better_summary(iplist, summary)
	-- Return a better summary that more precisely targets the specified IPs,
	-- or return nil if unable to improve the summary.
	local better = Collection.new()
	local improved
	for _, range in ipairs(summary) do
		local split = split_range(iplist, range)
		if split then
			improved = true
			for _, r in ipairs(split) do
				better:add(r)
			end
		else
			better:add(range)
		end
	end
	return improved and better
end
local function make_summaries(iplist)
	-- Return a collection where each item is a summary.
	-- A summary is a table of one or more ranges.
	-- A summary covers all the given IPs and probably more.
	-- A range is a table representing a CIDR block such as 1.2.248.0/21.
	-- The first summary found is a single range; each subsequent summary
	-- (if any) uses more ranges to better target the given IPs.
	-- The result omits any summary with a range size that is too small (too many IPs).
	local function good_size(summary)
		for _, range in ipairs(summary) do
			if range.size  0 then
		for i = 1, iplist.n - 1 do
			-- Set length of prefixes common between each pair of IPs.
			iplisti.common = common_prefix_length(iplisti, iplisti+1)
		end
		local summary = { make_range(iplist, 1, iplist.n) }
		while summary and summaries.n = 3 then
				store(hit)
			end
		end
	end
end
local function extract_ipv6(result, omitted, line)
	-- Extract any IPv6 addresses from given line or throw error.
	-- Addresses must be delimited with whitespace to reduce false positives.
	-- Want to accept all valid IPv6 despite the fact that contributors will
	-- not have an address starting with ':'.
	-- Also want to be able to parse arbitrary wikitext which might use colons
	-- for indenting. To achieve that, if an address at the start of a line
	-- is valid, use it; otherwise strip any leading colons and try again.
	for pos, hit in line:gmatch('()(%S+)') do
		local ipstr, length = hit:match('^(:%x+)(/?%d*)$')
		if ipstr then
			local _, n = ipstr:gsub(':', ':')
			if n >= 2 then
				local ip = ipv6_address(ipstr)
				if not ip and pos == 1 then
					ipstr, n = ipstr:gsub('^:+', )
					if n > 0 then
						ip = ipv6_address(ipstr)
					end
				end
				if ip then
					if length and #length > 0 then
						error('CIDR /n not accepted for IPv6: ' .. hit, 0)
					end
					result:add(ip)
				else
					omitted:add(hit)
				end
			end
		end
	end
end
local function contribs(address, strings, ipbase, size)
	-- Return a URL or wikilink to list the contributions for an IP or IP range,
	-- or return an empty string if cannot do anything useful.
	-- The given address is a string of either a single IP or a CIDR range.
	-- If using old system:
	--   For IPv6 CIDR, return a Special:Contributions link using an asterisk
	--   wildcard which should work if the user has enabled the gadget
	--   "Allow /16, /24 and /27 – /32 CIDR ranges on Special:Contributions".
	local encoded, count = address:gsub('/', '%%2F')
	if strings.want_old and count > 0 then
		make_note(strings, 'range')
		if address:find(':', 1, true) then
			if ipbase and size then
				local digits = math.floor(size / 4)
				if digits = 4 then
						partsi = hex
						digits = digits - 4
						if digits 
Total       Affected     Given        Range=,
footer = ,
sumfirst = =
----------------------------------------------------------
%s%-12s %-12s %-11d %s%s=,
-- %s = empty string (dummy for compatibility)
-- %s = total affected
-- %s = affected
-- %d = given (number of addresses given in input covered by this range)
-- %s = IP address range
-- %s = empty string
sumnext = =
%-12s %-11d %s%s=,
-- %s = affected
-- %d = given
-- %s = IP address range
-- %s = empty string
}
-- Strings for results using a table in wikitext.
local wikitable = {
header = =
',
sumfirst = =%s=,
-- %s = string of number of ranges in summary (number of rows)
-- %s = total affected
-- %s = affected
-- %d = given
-- %s = IP address range
-- %s = contributions link
sumnext = =%s=,
-- %s = affected
-- %d = given
-- %s = IP address range
-- %s = contributions link
}
local function show_summary(lines, strings, iplist, summary)
	-- Show the summary by adding table wikitext or plain text to lines.
	local want_plain = iplist.want_plain
	local total = 0
	for _, range in ipairs(summary) do
		-- A number is a double which easily handles 2^128 = 3.4e38.
		total = total + 2^(iplist.ipsize - range.size)
	end
	for i, range in ipairs(summary) do
		local prefix = ip_string(range.prefix)
		local size = range.size
		local affected = describe_size(iplist.ipsize, size)
		local given = range.ilast - range.ifirst + 1
		local address
		local link = 
		if size == iplist.ipsize then
			address = prefix
			if not want_plain then
				link = contribs(address, strings)
			end
		else
			address = prefix .. '/' .. size
			if not want_plain then
				link = contribs(address, strings, range.prefix, size)
			end
		end
		local s
		if i == 1 then
			s = string.format(strings.sumfirst,
					want_plain and  or tostring(#summary),
					describe_total(total),
					affected, given, address, link)
		else
			s = string.format(strings.sumnext,
					affected, given, address, link)
		end
		-- Pre tags returned by a module are html tags, not like wikitext ....
		lines:add(want_plain and mw.text.nowiki(s) or s)
	end
end
local function process_ips(lines, iplist, omitted)
	-- Process a list of IP addresses, adding text of results to lines.
	-- The list should contain either all IPv4 addresses, or all IPv6 (not a mixture).
	local seq1, seq2, seqmany
	local function show_sequence()
		if seq1 and seq2 then
			local text = ip_string(seq1)
			if seqmany then
				seqmany = false
				text = text .. ' – ' .. ip_string(seq2)
			end
			seq1 = nil
			seq2 = nil
			local markup = text:sub(1, 1) == ':' and ':' or ':'
			lines:add(markup .. text)
		end
	end
	local function show_ip(ip)
		-- Show IP or record it to be included in a "from to" sequence of IPs.
		if is_next_ip(seq2, ip) then
			seq2 = ip
			seqmany = true
		else
			show_sequence()
			seq1 = ip
			seq2 = ip
			seqmany = false
		end
	end
	if iplist.n  0 then
		lines:add()
	end
	if omitted.n > 0 then
		lines:add('Warning, omitted as invalid: ' .. omitted:join(' '))
		lines:add()
	end
	local heading_line
	if not iplist.nolist then
		lines:add()  -- this blank line is replaced with a heading
		heading_line = lines.n
	end
	local duplicates = Collection.new()
	local previous
	iplist:sort()
	-- Check for duplicates which can interfere with method to get ranges.
	for i, ip in ipairs(iplist) do
		if previous == ip then
			duplicates:add(i)  -- index to omit duplicate later
		elseif not iplist.nolist then
			show_ip(ip)
		end
		previous = ip
	end
	show_sequence()
	local duplicate_text = 
	if duplicates.n > 0 then
		duplicate_text = ' (after omitting some duplicates)'
		for i = duplicates.n, 1, -1 do
			iplist:remove(duplicatesi)
		end
	end
	local heading_text = string.format('เรียง %d เลขที่อยู่  %s ',
		iplist.n,
		iplist.ipname,
		iplist.n == 1 and  or 'es'
		)
	if heading_line then
		linesheading_line = heading_text .. duplicate_text .. ':'
	end
	local strings = iplist.want_plain and plaintext or wikitable
	strings.notes = nil  -- needed when module is kept loaded for multiple tests
	strings.want_old = iplist.want_old
	strings.nonote = iplist.nonote
	strings.months = iplist.months
	lines:add(strings.header)
	local upto = lines.n
	for _, summary in ipairs(make_summaries(iplist)) do
		show_summary(lines, strings, iplist, summary)
	end
	lines:add(strings.footer)
	if upto + 1 == lines.n then
		-- Show message in the very unlikely event that no results are found.
		lines:add('----')
		lines:add('No suitable ranges found; use |results=all to see all ranges.')
	end
	if strings.notes then
		lines:add()
		lines:add("Notes")
		for _, key in ipairs({'range', 'gadget'}) do
			if strings.noteskey then
				lines:add(strings.noteskey)
			end
		end
	end
end
local function make_options(args)
	-- Return table of options from validated args or throw error.
	local options = {}
	if not empty(args.comment) then
		options.comment = args.comment
	end
	-- Parameter 'months' is only used if 'old' is also used.
	local months = math.floor(tonumber(args.months) or tonumber(args.month) or 1)
	if months  12 then
		months = 12
	end
	options.months = months  -- silently ignore invalid input
	local allocation
	if not empty(args.allocation) then
		allocation = tonumber(args.allocation)
		if not (allocation and 48 ?%_
							:gsub('\226\128\142', ' ')  -- replace LTR marks (U+200E)
						extract_ipv4(v4list, v4omitted, line)
						extract_ipv6(v6list, v6omitted, line)
					end
				end
			end
		end
	end
	if v4list.n Error: ' .. msg .. 
end
local function sha1(frame)
	-- Return SHA-1 hash of first parameter.
	-- This is for use at User:Johnuniq/Security to generate hash of a password.
	local text = (frame.args1 or ):match("^%s*(.-)%s*$")
	if text ~=  then
		return 'SHA-1 hash after removing any leading or trailing whitespace is ' .. mw.hash.hashValue('sha1', text) .. 
	end
	return 'Usage:  to display the SHA-1 hash of text.'
end
return {
	IPblock = IPblock,
	_IPblock = _IPblock,
	sha1 = sha1,
}