Модуль:IPblock
Модуль:IPblock — модуль для вычисления минимальных блоков IP-адресов (CIDR-диапазонов), покрывающих заданный список IPv4 или IPv6 адресов. Используется через {{IP range calculator}}
Авторство и лицензия
[править код]- Оригинальный автор: Johnuniq
- Источник: en:Module:IPblock из английской Википедии
- Лицензия: Текст доступен по лицензии Creative Commons Attribution-ShareAlike 3.0; в отдельных случаях могут действовать дополнительные условия.
Использование
[править код]Модуль используется через {{IP range calculator}} для:
- Анализа списков IP-адресов
- Вычисления минимальных CIDR-диапазонов
- Генерации ссылок на вклад и блокировки диапазонов
Технические детали
[править код]Модуль:
- Поддерживает IPv4 (CIDR /16 - /32) и IPv6 (CIDR /19 - /128)
- Автоматически определяет тип адресов
- Обрабатывает дубликаты и невалидные адреса
- Генерирует ссылки на Служебная:Вклад и инструменты XTools
Ограничения
[править код]- Не может блокировать более /16 для IPv4 и /19 для IPv6 (ограничение MediaWiki)
- IPv4 и IPv6 адреса обрабатываются отдельно
- Максимум 1000 диапазонов при
results=all
См. также
[править код]- {{IP range calculator}} — шаблон-обёртка
- Справка по блокировке диапазонов
-- Вычисление минимальных блоков IP-адресов, покрывающих каждый
-- IPv4 или IPv6 адрес, введённый в аргументах.
local bit32 = require('bit32')
local Collection -- таблица для хранения элементов
Collection = {
add = function (self, item)
if item ~= nil then
self.n = self.n + 1
self[self.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 < pos and pos <= self.n)) then
self.n = self.n - 1
return table.remove(self, pos)
end
end,
sort = function (self, comp)
table.sort(self, comp)
end,
new = function ()
return setmetatable({n = 0}, Collection)
end
}
Collection.__index = Collection
local function empty(text)
-- Возвращает true, если text nil или пустой (предполагается строка).
return text == nil or text == ''
end
local timestamps = {} -- кэш
local function start_date(code, months)
-- Возвращает строку временной метки для URL со списком вкладов участников
-- начиная с возвращённой даты.
-- Код определяет нужный формат.
-- Для этого модуля нужны только недавние вклады, поэтому
-- временная метка - это сегодняшняя дата минус заданное количество месяцев (от 1 до 12).
local key = code .. months
if not timestamps[key] then
local date = os.date('!*t') -- сегодняшняя дата UTC
local y, m, d = date.year, date.month, date.day -- полный год, месяц (1-12), день (1-31)
m = m - months
if m <= 0 then
m = m + 12
y = y - 1
end
local limit = m == 2 and 28 or 30
if d > limit then
d = limit -- достаточно для обеспечения валидности даты
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 timestamps[key] or ''
end
local note_text = {
range = '*Ссылки для диапазонов показывают вклад за предыдущий %s.',
gadget = [=[
*<span id="need-gadget"></span>Ссылки на вклад для диапазонов IPv6 требуют включения гаджета "<span style="color:green;">Разрешить диапазоны CIDR /16, /24 и /27 – /32 в форме Служебная:Вклад</span>" в [[Служебная:Настройки#mw-prefsection-gadgets|Служебная:Настройки]], а также включённый JavaScript в браузере.]=],
}
local function make_note(strings, key)
-- Записывает факт необходимости определённого примечания и возвращает
-- викитекст для ссылки на примечание или '', если ссылка не нужна.
if not strings.nonote then
strings.notes = strings.notes or {}
if not strings.notes[key] then
if key == 'gadget' then
strings.notes[key] = note_text[key]
elseif key == 'range' then
local when = 'месяц'
if strings.months > 1 then
when = strings.months .. ' месяца'
if strings.months >= 5 then
when = strings.months .. ' месяцев'
end
end
strings.notes[key] = string.format(note_text.range, when)
else
error('make_note: неожиданный ключ')
end
end
if key == 'gadget' then
return ' [[#need-gadget|<sup>[прим.]</sup>]]'
end
end
return ''
end
local function describe_total(total, isalloc)
-- Возвращает текст, описывающий заданное количество адресов или /64 выделений.
if total <= 9999 then
-- Может быть дробным, если total - количество /64 выделений.
if total < 9 then
return (string.format('%.1f', total):gsub('%.0$', ''))
end
return string.format('%.0f', total)
end
if not isalloc then
local alloc = 2^64
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)
-- Возвращает текст, описывающий количество IP в диапазоне с размером = длина префикса.
local function numtext(n)
if n <= 16 then
return tostring(2^n)
end
if n <= 19 then
return tostring(2^(n - 10)) .. 'K'
end
if n <= 29 then
return tostring(2^(n - 20)) .. 'M'
end
if n <= 36 then
return tostring(2^(n - 30)) .. 'G'
end
return '>64G'
end
local host = ipsize - size
if host <= 32 then
-- IPv4 или IPv6.
return numtext(host)
end
-- Должен быть IPv6.
if host <= 64 then
local s = ({
[64] = '1', [63] = '50%', [62] = '25%', [61] = '12%',
[60] = '6%', [59] = '3%', [58] = '2%'
})[host] or '<1%'
return s .. ' /64'
end
-- IPv6 с размером < 64.
return numtext(host - 64) .. ' /64'
end
local function ipv6_string(ip)
-- Возвращает строку, эквивалентную заданному IPv6 адресу.
local z1, z2 -- индексы последовательности нулей для отображения как "::"
local zstart, zcount
for i = 1, 9 do
-- Находит самое левое вхождение самой длинной последовательности двух или более нулей.
if i < 9 and ip[i] == 0 then
if zstart then
zcount = zcount + 1
else
zstart = i
zcount = 1
end
else
if zcount and zcount > 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 <= i and i <= z2 then
if i == z1 then
if z1 == 1 or z2 == 8 then
if z1 == 1 and z2 == 8 then
return '::'
end
parts:add(':')
else
parts:add('')
end
end
else
parts:add(string.format('%x', ip[i]))
end
end
return parts:join(':')
end
local function ip_string(ip)
-- Возвращает строку, эквивалентную заданному IP адресу (IPv4 или IPv6).
if ip.n == 2 then
-- IPv4.
local parts = {}
for i = 1, 2 do
local w = ip[i]
local q = i == 1 and 1 or 3
parts[q] = math.floor(w / 256)
parts[q+1] = w % 256
end
return table.concat(parts, '.')
end
return ipv6_string(ip)
end
-- Метатаблица для некоторых операций над IP адресами.
local ipmt = {
__eq = function (lhs, rhs)
-- Возвращает true, если значения в нумерованных таблицах совпадают.
if lhs.n == rhs.n then
for i = 1, lhs.n do
if lhs[i] ~= rhs[i] then
return false
end
end
return true
end
return false
end,
__lt = function (lhs, rhs)
-- Возвращает true, если lhs < rhs; для сортировки.
if lhs.n == rhs.n then
for i = 1, lhs.n do
if lhs[i] ~= rhs[i] then
return lhs[i] < rhs[i]
end
end
return false
end
return lhs.n < rhs.n -- сортировать IPv4 перед IPv6, хотя это не требуется
end,
}
local function ipv4_address(ip_str)
-- Возвращает коллекцию из двух 16-битных слов (чисел), эквивалентных
-- IPv4 адресу, заданному как строка с точками, или
-- возвращает nil, если невалидно.
-- Это представление для совместимости с IPv6 адресами.
local parts = Collection.new()
local s = ip_str:match('^%s*(.-)%s*$') .. '.'
for item in s:gmatch('(.-)%.') do
parts:add(item)
end
if parts.n == 4 then
for i, s in ipairs(parts) do
if s:match('^%d+$') then
local num = tonumber(s)
if 0 <= num and num <= 255 then
if num > 0 and s:match('^0') then
-- Избыточный ведущий ноль - ошибка, так как он для IP в восьмеричной системе.
return nil
end
parts[i] = num
else
return nil
end
else
return nil
end
end
local result = Collection.new()
for i = 1, 3, 2 do
result:add(parts[i] * 256 + parts[i+1])
end
return setmetatable(result, ipmt)
end
return nil
end
local function ipv6_address(ip_str)
-- Возвращает коллекцию из восьми 16-битных слов (чисел), эквивалентных
-- IPv6 адресу, заданному как строка с двоеточиями, или
-- возвращает nil, если невалидно.
local _, n = ip_str:gsub(':', ':')
if n < 7 then
ip_str, n = ip_str:gsub('::', string.rep(':', 9 - n))
end
local parts = Collection.new()
for item in (ip_str .. ':'):gmatch('(.-):') do
parts:add(item)
end
if parts.n == 8 then
for i, s in ipairs(parts) do
if s == '' then
parts[i] = 0
else
local num = tonumber('0x' .. s)
if num and 0 <= num and num <= 65535 then
parts[i] = num
else
return nil
end
end
end
return setmetatable(parts, ipmt)
end
return nil
end
local function common_length(num1, num2, nr_bits)
-- Возвращает количество префиксных битов, общих для двух целых чисел.
-- Количество битов в каждом числе nr_bits = 16, 8, 4, 2 или 1.
if nr_bits <= 1 then
return num1 == num2 and 1 or 0
end
local half = nr_bits / 2
local splitter = 2^half
local upper1, lower1 = math.modf(num1 / splitter)
local upper2, lower2 = math.modf(num2 / splitter)
if upper1 == upper2 then
lower1 = math.floor(lower1 * splitter + 0.5)
lower2 = math.floor(lower2 * splitter + 0.5)
return half + common_length(lower1, lower2, half)
end
return common_length(upper1, upper2, half)
end
local function common_prefix_length(ip1, ip2)
-- Возвращает количество префиксных битов, общих для двух IP.
-- Вызывающая сторона гарантирует, что оба IP либо IPv4, либо IPv6.
local size = 0
for i = 1, ip1.n do
local w1, w2 = ip1[i], ip2[i]
if w1 == w2 then
size = size + 16
else
return size + common_length(w1, w2, 16)
end
end
return size
end
local function ip_prefix(ip, length)
-- Возвращает копию ip, маскированную для содержания только префикса заданной длины.
local result = { n = ip.n }
for i = 1, ip.n do
if length > 0 then
if length >= 16 then
result[i] = ip[i]
length = length - 16
else
result[i] = bit32.band(ip[i], bit32.arshift(0xffff8000, length - 1))
length = 0
end
else
result[i] = 0
end
end
return setmetatable(result, ipmt)
end
local function ip_incremented(ip)
-- Возвращает новый IP, равный ip + 1.
-- Будет переполнение (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 = ip[i] + carry
if sum >= 0x10000 then
carry = 1
sum = sum - 0x10000
else
carry = 0
end
result[i] = sum
end
return setmetatable(result, ipmt)
end
local function is_next_ip(ip1, ip2)
-- Возвращает true, если ip2 - следующий IP после ip1 (ip2 == ip1 + 1).
-- IP отсортированы и уникальны, поэтому ip1 < ip2 и можно игнорировать переполнение в ноль.
-- Это менее затратно, чем создание нового инкрементированного IP и сравнение.
if ip1 and ip2 then
local carry = 1
for i = ip1.n, 1, -1 do
local sum = ip1[i] + carry
if sum >= 0x10000 then
carry = 1
sum = sum - 0x10000
else
carry = 0
end
if sum ~= ip2[i] then
return false
end
end
return true
end
end
-- Каждый IP в диапазоне, кроме последнего, имеет поле 'common', которое
-- является числом, указывающим, сколько битов общие между префиксами этого
-- IP и следующего IP (0, если этот IP начинается с 0, а следующий с 1).
-- Каждый непустой диапазон имеет ровно один "минимальный общий", то есть его значение
-- common меньше всех остальных. То, что есть только один минимальный общий,
-- следует из того факта, что IP уникальны и отсортированы.
local function make_range(iplist, ifirst, ilast)
-- Возвращает таблицу для диапазона IP от iplist[ifirst] до iplist[ilast] включительно.
local imin, vmin, done
if ifirst < ilast then
for i = ifirst, ilast - 1 do
-- Находит (уникальный) минимум общих длин.
local common = iplist[i].common
if vmin then
if vmin > 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
-- Для IPv6 выделение по умолчанию /64, и нет смысла иметь
-- более точные диапазоны, так как они добавляют ненужную сложность.
-- Однако использование results=all устанавливает allocation = 128, поэтому vmin не изменяется.
vmin = iplist.allocation
end
return {
ifirst = ifirst, -- индекс первого IP
ilast = ilast, -- индекс последнего IP
imin = imin, -- индекс IP с минимальным общим
size = vmin, -- количество общих битов в префиксе (минимум)
prefix = ip_prefix(iplist[imin], vmin), -- таблица IP базового IP
done = done, -- true, если известно, что этот диапазон нельзя улучшить
}
end
local function split_range(iplist, range, depth)
-- Возвращает таблицу из двух или более диапазонов, которые более точно нацелены
-- на IP в диапазоне, или ничего не возвращает, если не удаётся улучшить диапазон.
depth = depth and depth + 1 or 0
if depth <= 20 and -- 20 проверяет 1M смежных адресов вплоть до отдельных IP
not range.done and
range.size < iplist.allocation and
range.ifirst < range.ilast then
local imin = range.imin
assert(imin and range.ifirst <= imin and imin < range.ilast)
local r1 = make_range(iplist, range.ifirst, range.imin)
local r2 = make_range(iplist, range.imin + 1, range.ilast)
local pointless = range.size + 1
if r1.size > 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)
-- Возвращает улучшенную сводку, которая более точно нацелена на указанные IP,
-- или возвращает nil, если не удаётся улучшить сводку.
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)
-- Возвращает коллекцию, где каждый элемент - это сводка.
-- Сводка - это таблица из одного или более диапазонов.
-- Сводка покрывает все заданные IP и, вероятно, больше.
-- Диапазон - это таблица, представляющая блок CIDR, например 1.2.248.0/21.
-- Первая найденная сводка - это один диапазон; каждая последующая сводка
-- (если есть) использует больше диапазонов для лучшего нацеливания на заданные IP.
-- Результат опускает любую сводку с размером диапазона, который слишком мал (слишком много IP).
local function good_size(summary)
for _, range in ipairs(summary) do
if range.size < iplist.minsize then
return false
end
end
return true
end
local summaries = Collection.new()
if iplist.n > 0 then
for i = 1, iplist.n - 1 do
-- Устанавливает длину общих префиксов между каждой парой IP.
iplist[i].common = common_prefix_length(iplist[i], iplist[i+1])
end
local summary = { make_range(iplist, 1, iplist.n) }
while summary and summaries.n < iplist.maxresults do
if good_size(summary) then
summaries:add(summary)
end
summary = better_summary(iplist, summary)
end
end
return summaries
end
local function extract_ipv4(result, omitted, line)
-- Извлекает любые IPv4 адреса из заданной строки или выдаёт ошибку.
-- Принимает CIDR /n для указания диапазона (принимает только от 16 до 32).
-- Адреса должны быть разделены пробелами для уменьшения ложных срабатываний.
local function store(hit)
local n = 32
local lhs, rhs = hit:match('^(.-)/(%d+)$')
if lhs then
hit = lhs
n = tonumber(rhs)
if not (n and 16 <= n and n <= 32) then
error('CIDR /n принимает только n = 16-32, недействительно: ' .. lhs .. '/' .. rhs, 0)
end
end
local ip = ipv4_address(hit)
if ip then
if n == 32 then
result:add(ip)
else
if ip ~= ip_prefix(ip, n) then
error('Недействительный базовый адрес (биты хоста должны быть нулевыми): ' .. hit, 0)
end
for _ = 1, 2^(32 - n) do
result:add(ip)
ip = ip_incremented(ip)
end
end
else
omitted:add(hit)
end
end
line = line:gsub(':', ' ') -- чтобы викитекстовые отступы или другие двоеточия не скрывали IPv4 адрес
for hit in line:gmatch('%S+') do
if hit:match('^%d+%.%d+[%.%d/]+$') then
local _, n = hit:gsub('%.', '.')
if n >= 3 then
store(hit)
end
end
end
end
local function extract_ipv6(result, omitted, line)
-- Извлекает любые IPv6 адреса из заданной строки или выдаёт ошибку.
-- Адреса должны быть разделены пробелами для уменьшения ложных срабатываний.
-- Хотим принимать все валидные IPv6, несмотря на то, что у участников
-- не будет адреса, начинающегося с ':'.
-- Также хотим иметь возможность парсить произвольный викитекст, который может использовать двоеточия
-- для отступов. Для этого, если адрес в начале строки
-- валиден, используем его; иначе убираем любые ведущие двоеточия и пробуем снова.
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 не принимается для IPv6: ' .. hit, 0)
end
result:add(ip)
else
omitted:add(hit)
end
end
end
end
end
local function contribs(address, strings, ipbase, size)
-- Возвращает URL или викиссылку для списка вкладов для IP или диапазона IP,
-- или возвращает пустую строку, если не может сделать ничего полезного.
-- Заданный адрес - это строка либо одного IP, либо диапазона CIDR.
-- Если используется старая система:
-- Для IPv6 CIDR возвращает ссылку Служебная:Вклад, используя звёздочку
-- как подстановочный знак, который должен работать, если у пользователя включён гаджет
-- "Разрешить диапазоны CIDR /16, /24 и /27 – /32 в Служебная:Вклад".
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 < 3 then
digits = 3
end
local wildcard = digits % 4 == 0 and ':*' or '*'
local parts = {}
for i = 1, 8 do
local hex = string.format('%X', ipbase[i]) -- должен быть в верхнем регистре
if digits >= 4 then
parts[i] = hex
digits = digits - 4
if digits <= 0 then
break
end
else
local nz -- количество ведущих нулей в этой группе из четырёх цифр
if hex == '0' then
nz = 4
else
nz = 4 - #hex
end
if digits <= nz then
-- Не можем правильно обработать этот случай; должны опустить группу
-- потому что "0" никогда не встречается как первая цифра.
wildcard = ':*'
else
hex = string.rep('0', nz) .. hex -- четыре цифры
parts[i] = hex:sub(1, digits)
end
break
end
end
address = table.concat(parts, ':') .. wildcard
local url = '[https://ru.wikipedia.org/wiki/Special:Contributions/%s?ucstart=%s вклад]'
-- %s = IPv6 префикс адрес в верхнем регистре с подстановочным знаком '*' в конце
-- %s = Дата начала в формате 'yyyymmdd000000'
return string.format(url, address, start_date('ymdHMS', strings.months)) .. make_note(strings, 'gadget')
end
return '' -- ссылка на вклад недоступна
end
local url = '[https://tools.wmflabs.org/xtools/rangecontribs/?project=ru.wikipedia.org&namespace=all&limit=50&text=%s&begin=%s вклад]'
-- %s = IPv4 CIDR диапазон с '/' изменённым на '%2F'
-- %s = Дата начала в формате 'yyyy-mm-dd'
return string.format(url, encoded, start_date('y-m-d', strings.months))
end
return '[[Служебная:Вклад/' .. address .. '|вклад]]'
end
-- Строки для результатов, использующих простой текст.
-- Используемые теги pre - это html, которые не предоставляют "nowiki",
-- но это не требуется для используемого текста.
local plaintext = {
header = [=[
<pre>
Всего Затронуто Указано Диапазон]=],
footer = '</pre>',
sumfirst = [=[
----------------------------------------------------------
%s%-12s %-12s %-11d %s%s]=],
-- %s = пустая строка (заглушка для совместимости)
-- %s = всего затронуто
-- %s = затронуто
-- %d = указано (количество адресов, указанных во входных данных, покрываемых этим диапазоном)
-- %s = диапазон IP адресов
-- %s = пустая строка
sumnext = [=[
%-12s %-11d %s%s]=],
-- %s = затронуто
-- %d = указано
-- %s = диапазон IP адресов
-- %s = пустая строка
}
-- Строки для результатов, использующих таблицу в викитексте.
local wikitable = {
header = [=[
{| class="wikitable"
! Всего<br />затронуто !! Затронуто<br />адресов !! Указано<br />адресов !! Диапазон !! Вклад]=],
footer = '|}',
sumfirst = [=[
|- style="vertical-align: top; border-top: 10px solid #a9a9a9;"
|rowspan="%s" |%s ||%s ||%d ||%s ||%s]=],
-- %s = строка с количеством диапазонов в сводке (количество строк)
-- %s = всего затронуто
-- %s = затронуто
-- %d = указано
-- %s = диапазон IP адресов
-- %s = ссылка на вклад
sumnext = [=[
|-
|%s ||%d ||%s ||%s]=],
-- %s = затронуто
-- %d = указано
-- %s = диапазон IP адресов
-- %s = ссылка на вклад
}
local function show_summary(lines, strings, iplist, summary)
-- Показывает сводку, добавляя викитекст таблицы или простой текст в lines.
local want_plain = iplist.want_plain
local total = 0
for _, range in ipairs(summary) do
-- Число - это double, который легко обрабатывает 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, возвращаемые модулем, являются html тегами, а не как викитекст <pre>...</pre>.
lines:add(want_plain and mw.text.nowiki(s) or s)
end
end
local function process_ips(lines, iplist, omitted)
-- Обрабатывает список IP адресов, добавляя текст результатов в lines.
-- Список должен содержать либо все IPv4 адреса, либо все IPv6 (не смесь).
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 ':<nowiki/>' or ':'
lines:add(markup .. text)
end
end
local function show_ip(ip)
-- Показывает IP или записывает его для включения в последовательность "от до" IP.
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 < 1 then
return
end
if lines.n > 0 then
lines:add('')
end
if omitted.n > 0 then
lines:add('Предупреждение, пропущено как недействительное: ' .. omitted:join(' '))
lines:add('')
end
local heading_line
if not iplist.nolist then
lines:add('') -- эта пустая строка заменяется заголовком
heading_line = lines.n
end
local duplicates = Collection.new()
local previous
iplist:sort()
-- Проверяет на дубликаты, которые могут помешать методу получения диапазонов.
for i, ip in ipairs(iplist) do
if previous == ip then
duplicates:add(i) -- индекс для пропуска дубликата позже
elseif not iplist.nolist then
show_ip(ip)
end
previous = ip
end
show_sequence()
local duplicate_text = ''
if duplicates.n > 0 then
duplicate_text = ' (после удаления некоторых дубликатов)'
for i = duplicates.n, 1, -1 do
iplist:remove(duplicates[i])
end
end
local heading_text = string.format(';Отсортировано %d %s адрес%s',
iplist.n,
iplist.ipname,
iplist.n == 1 and '' or 'ов'
)
if heading_line then
lines[heading_line] = heading_text .. duplicate_text .. ':'
end
local strings = iplist.want_plain and plaintext or wikitable
strings.notes = nil -- требуется, когда модуль остаётся загруженным для множественных тестов
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
-- Показывает сообщение в крайне маловероятном случае, если результаты не найдены.
lines:add('----')
lines:add('Подходящие диапазоны не найдены; используйте <code>|results=all</code>, чтобы увидеть все диапазоны.')
end
if strings.notes then
lines:add('')
lines:add("'''Примечания'''")
for _, key in ipairs({'range', 'gadget'}) do
if strings.notes[key] then
lines:add(strings.notes[key])
end
end
end
end
local function make_options(args)
-- Возвращает таблицу опций из проверенных аргументов или выдаёт ошибку.
local options = {}
if not empty(args.comment) then
options.comment = args.comment
end
-- Параметр 'months' используется только если 'old' также используется.
local months = math.floor(tonumber(args.months) or tonumber(args.month) or 1)
if months < 1 then
months = 1
elseif months > 12 then
months = 12
end
options.months = months -- молча игнорирует недействительный ввод
local allocation
if not empty(args.allocation) then
allocation = tonumber(args.allocation)
if not (allocation and 48 <= allocation and allocation <= 128) then
error('Недействительное выделение "' .. args.allocation .. '" (должно быть от 48 до 128; по умолчанию 64)', 0)
end
end
local maxresults
if not empty(args.results) then
if args.results == 'all' then
options.all = true
allocation = allocation or 128
maxresults = 1000
else
maxresults = tonumber(args.results)
if not (maxresults and 1 <= maxresults and maxresults <= 100) then
error('Недействительные результаты "' .. args.results .. '" (должно быть от 1 до 100)', 0)
end
end
end
options.allocation = allocation or 64
options.maxresults = maxresults or 10
local keywords = {
-- Таблица k, v строк.
-- Если аргумент соответствует k, опция с именем v устанавливается в true.
ok = 'noannounce',
old = 'want_old',
nolist = 'nolist',
nonote = 'nonote',
text = 'text',
}
local want_old
for i, arg in ipairs(args) do
local flag = keywords[arg:match('^%s*(%w+)%s*$')]
if flag then
options[i] = 'skip'
options[flag] = true
if flag == 'want_old' then
want_old = true
end
end
end
if not want_old then
options.nonote = true
end
return options
end
local function _IPblock(args)
-- Обрабатывает заданные аргументы; может быть вызвана из другого модуля.
-- Выдаёт ошибку, если нужно сообщить о проблеме.
local options = make_options(args)
local v4list, v4omitted = Collection.new(), Collection.new()
local v6list, v6omitted = Collection.new(), Collection.new()
v4list.ipsize = 32
v4list.ipname = 'IPv4'
v6list.ipsize = 128
v6list.ipname = 'IPv6'
v4list.allocation = 32
v6list.allocation = options.allocation
if options.all then
v4list.minsize = 0
v6list.minsize = 0
else
v4list.minsize = 16 -- нельзя блокировать больше IP, чем /16 для IPv4
v6list.minsize = 19 -- или /19 для IPv6 ($wgBlockCIDRLimit)
end
for _, k in ipairs({'maxresults', 'months', 'want_old', 'nolist', 'nonote'}) do
v4list[k] = options[k]
v6list[k] = options[k]
end
if options.text then
v4list.want_plain = true
v6list.want_plain = true
end
for i, arg in ipairs(args) do
if options[i] ~= 'skip' then
for line in string.gmatch(arg .. '\n', '[\t ]*(.-)[\t\r ]*\n') do
-- Пропускает строку, если она пустая или комментарий.
if line ~= '' then
local comment = options.comment
if not (comment and line:sub(1, #comment) == comment) then
line = line
:gsub('[Ss]pecial:[Cc]ontrib%w*/', ' ') -- чтобы ввод "Special:Contributions/1.2.3.4" работал
:gsub('[Сс]лужебная:[Вв]клад%w*/', ' ') -- русский вариант
:gsub('[Tt]alk:', ' ')
:gsub('[Оо]бсуждение:', ' ') -- русский вариант
:gsub('[Uu]ser:', ' ')
:gsub('[Уу]частник:', ' ') -- русский вариант
:gsub('[Уу]частница:', ' ') -- русский вариант (женский род)
:gsub('[!"#&\'()+,%-;<=>?[%]_{|}]', ' ') -- заменяет принятые разделители пробелом
:gsub('\226\128\142', ' ') -- заменяет LTR метки (U+200E)
extract_ipv4(v4list, v4omitted, line)
extract_ipv6(v6list, v6omitted, line)
end
end
end
end
end
if v4list.n < 1 and v6list.n < 1 then
error('Нет валидного IPv4 или IPv6 адреса в аргументах', 0)
end
local lines = Collection.new()
if not options.noannounce then
-- 1: Закомментировано в апреле 2016 как устаревшее.
-- 1: lines:add("'''Пожалуйста, смотрите [[Template talk:Blockcalc#Version February 2016|это объявление]].'''")
-- 2: Закомментировано в декабре 2017 как устаревшее.
-- 2: lines:add("'''По умолчанию ссылки теперь используют [[Служебная:Вклад]] согласно [[Template talk:IP range calculator#Version November 2017|этому объявлению]].'''")
end
process_ips(lines, v4list, v4omitted)
process_ips(lines, v6list, v6omitted)
return lines:join('\n')
end
local function IPblock(frame)
-- Возвращает викитекст для отображения наименьшего IPv4 или IPv6 CIDR диапазона, который
-- покрывает каждый адрес, заданный в аргументах, или возвращает текст ошибки.
-- Ввод может содержать любую смесь IP; IPv4 и IPv6 обрабатываются отдельно.
local ok, msg = pcall(_IPblock, frame:getParent().args)
if ok then
return msg
end
return '<strong class="error">Ошибка: ' .. msg .. '</strong>'
end
local function sha1(frame)
-- Возвращает SHA-1 хеш первого параметра.
-- Это для использования на [[User:Johnuniq/Security]] для генерации хеша пароля.
local text = (frame.args[1] or ''):match("^%s*(.-)%s*$")
if text ~= '' then
return 'SHA-1 хеш после удаления любых ведущих или конечных пробелов: <code>' .. mw.hash.hashValue('sha1', text) .. '</code>'
end
return 'Использование: <code>{{#invoke:IPblock|sha1|text}}</code> для отображения SHA-1 хеша <code>text</code>.'
end
return {
IPblock = IPblock,
_IPblock = _IPblock,
sha1 = sha1,
}