Module:Sensitive IP addresses/API: Difference between revisions

Content deleted Content added
finish the query function, add a couple of helper functions, and make a few bug fixes
m Protected "Module:Sensitive IP addresses/API": High-risk Lua module: used in the MediaWiki interface, e.g. MediaWiki:Blockiptext via Template:Sensitive IP addresses ([Edit=Require administrator access] (indefinite...
 
(15 intermediate revisions by the same user not shown)
Line 7:
local IPv4Collection = mIP.IPv4Collection
local IPv6Collection = mIP.IPv6Collection
 
local libraryUtil = require('libraryUtil')
-- Lazily load the jf-JSON module
local checkType = libraryUtil.checkType
local JSON
 
-------------------------------------------------------------------------------
Line 26 ⟶ 27:
else
return val
end
end
 
local function deepCopyInto(source, dest)
-- Do a deep copy of a source table into a destination table, ignoring
-- self-references and metatables. If a table in source has a self-reference
-- you will get an infinite loop.
for k, v in pairs(source) do
if type(v) == 'table' then
dest[k] = {}
deepCopyInto(v, dest[k])
else
dest[k] = v
end
end
end
Line 87 ⟶ 102:
-- otherwise. matchObj is the Subnet object that was matched, and queryObj
-- is the IPAddress or Subnet object corresponding to the input string.
checkType('matchesIPOrRange', 1, str, 'string')
 
-- Get the IPAddress or Subnet object for str
Line 156 ⟶ 170:
-- {
-- entities = {'all'}
-- }
--
-- Query all entities and format the result as a JSON string:
-- {
-- entities = {'all'},
-- format = 'json'
-- }
--
Line 237 ⟶ 257:
end
 
local function makeError(code, info, format)
local ret = {['error'] = {
return {
code = code,
info = info,
['*'] = 'See https://en.wikipedia.org/wiki/Module:Sensitive_IP_addresses/API for API usage',
}}
if format == 'json' then
return mw.text.jsonEncode(ret)
else
return ret
end
end
 
-- Construct result
local result = {}
matches = {},
['matched-ranges'] = {},
entities = {},
['entity-ids'] = {}
}
 
if type(options) ~= 'table' then
Line 259 ⟶ 289:
return makeError(
'sipa-blank-options',
"the options table didn't contain a 'test' or an 'entities' key",
options.format
)
end
Line 270 ⟶ 301:
"'test' options key was type %s (expected table)",
type(options.test)
),
options.format
)
end
 
-- Insert the result subtables. We do this now rather than when we
-- create the results table, as there shouldn't be a matches subtable
-- if the user didn't specify any strings to match.
result.matches = {}
result['matched-ranges'] = {}
result.entities = {}
result['entity-ids'] = {}
 
for i, testString in ipairs(options.test) do
Line 290 ⟶ 314:
i,
type(testString)
),
options.format
)
end
Line 309 ⟶ 334:
i,
testString
),
options.format
)
end
if isMatch then
-- The string was a sensitive IP address or subnet.
 
-- Add match data
local match = {}
-- Quick and dirty hack to find if queryObj is an IPAddress object.
local isIP = queryObj.getNextIP ~= nil and queryObj.isInSubnet ~= nil
 
-- Add the match to the match subtable.
if isIP then
match.type = 'ip'
Line 358 ⟶ 384:
"'entities' options key was type %s (expected table)",
type(options.test)
),
options.format
)
end
Line 373 ⟶ 400:
i,
type(entityString)
),
options.format
)
end
Line 396 ⟶ 424:
-- Insert the entity and entity-id subtables if they aren't already
-- present.
result.entities = result.entities or {}
result['entity-ids'] = result['entity-ids'] or {}
for i, entityString in ipairs(options.entities) do
if entities[entityString] then
Line 413 ⟶ 439:
end
 
-- Add any missing reason fields from entities.
return result
for id, entityData in pairs(result.entities) do
end
entityData.reason = entityData.reason or 'political'
end
 
-- Wrap the result in an outer layer like the MediaWiki Action API does.
--------------------------------------------------------------------------------
result = {sensitiveips = result}
-- Q&D demo of loading data from [[Module:Sensitive IP addresses/list]]
-- into a structure that could be used to determine whether a particular
-- IP or subnet overlaps a sensitive range.
-- If used, this would be greatly refactored and possibly split to
-- [[Module:Sensitive IP addresses/data]].
--
-- Usage in a sandbox:
-- {{#invoke:Sensitive IP addresses|main}}
 
if options.format == 'json' then
local function main()
-- Load jf-JSON
-- Test Module:IP.
JSON = JSON or require('Module:jf-JSON')
----------------------------------------------------------------------------
JSON.strictTypes = true -- Necessary for correct blank-object encoding
-- An IP collection in Module:IP should hold both IPv4 and IPv6 lists and
-- Decode a skeleton result JSON string. This ensures that blank objects
-- it would use the appropriate list depending on the object queried?
-- are re-encoded as blank objects and not as blank arrays.
-- That would make this code more straight forward.
local jsonResult = JSON:decode([[{"sensitiveips": {
----------------------------------------------------------------------------
"matches": [],
-- Support stuff
"matched-ranges": {},
----------------------------------------------------------------------------
"entities": {},
local modcode = require('Module:IP')
"entity-ids": []
local IPAddress = modcode.IPAddress
}}]])
local Subnet = modcode.Subnet
for i, key in ipairs{'matches', 'matched-ranges', 'entities', 'entity-ids'} do
local IPv4Collection = modcode.IPv4Collection
deepCopyInto(result.sensitiveips[key], jsonResult.sensitiveips[key])
local IPv6Collection = modcode.IPv6Collection
local Collection = {}
Collection.__index = Collection
do
function Collection:add(item)
if item ~= nil then
self.n = self.n + 1
self[self.n] = item
end
end
function Collection:join(sep)
return table.concat(self, sep)
end
function Collection:sort(comp)
table.sort(self, comp)
end
function Collection.new()
return setmetatable({n = 0}, Collection)
end
end
local function getObject(ipStr)
-- Parse a string and return an appropriate object:
-- IPv4 or IPv6 IP or subnet, or nil.
-- TODO This should be in Module:IP (see IPCollection:_store).
local maker
if ipStr:find('/', 1, true) then
maker = Subnet.new
else
maker = IPAddress.new
end
local success, obj = pcall(maker, ipStr)
if success then
return obj
end
return nil
end
local function preBlock(text)
-- Pre tags returned by a module do not act like wikitext <pre>...</pre>.
return '<pre>\n' ..
mw.text.nowiki(text) ..
(text:sub(-1) == '\n' and '' or '\n') ..
'</pre>\n'
end
----------------------------------------------------------------------------
-- Load sensitive IP information
----------------------------------------------------------------------------
local function loadList(modname)
-- Return a table to query an IP/subnet wrt sensitive ranges.
local data = {
subnetToInfo = {},
v4Collection = IPv4Collection.new(),
v6Collection = IPv6Collection.new(),
}
local sensitiveList = mw.loadData(modname)
for i, info in ipairs(sensitiveList) do
for _, r in ipairs({
{key = 'ipv4Ranges', list = data.v4Collection},
{key = 'ipv6Ranges', list = data.v6Collection},
}) do
local rangeStrings = info[r.key]
if rangeStrings then
for _, str in ipairs(rangeStrings) do
local subnet = Subnet.new(str)
r.list:addSubnet(subnet)
data.subnetToInfo[subnet] = info
end
end
end
end
return data
end
----------------------------------------------------------------------------
-- Run test using Module:IP
----------------------------------------------------------------------------
local data = loadList('Module:Sensitive IP addresses/list')
local results = Collection.new()
results:add('IP ranges equivalent to collection')
for _, col in ipairs({data.v4Collection, data.v6Collection}) do
for _, range in ipairs(col:getRanges()) do
if range[1] == range[2] then
results:add(' ' .. range[1])
else
results:add(' ' .. range[1] .. ' – ' .. range[2])
end
end
end
for _, ipStr in ipairs({
-- Each of the following is tested against the sensitive list.
'143.228.19.123',
'2620:0:E21:9F2::',
'131.132.224.0/19',
'198.35.27.255',
'2620:0:860::1',
'1.2.3.4',
'11.12.13.192/26',
'2001:db8::abcd',
'2001:db8::/72',
}) do
local obj = getObject(ipStr)
if obj then
local isPresent, clashObj
local col = obj:getVersion() == 'IPv4' and
data.v4Collection or data.v6Collection
if obj.getNextIP then -- dirty trick to check if obj is an IP
isPresent, clashObj = col:containsIP(obj)
else
isPresent, clashObj = col:overlapsSubnet(obj)
end
results:add('')
results:add('IP or range under test: ' .. ipStr)
if isPresent then
local info = data.subnetToInfo[clashObj]
if info then
results:add(' sensitive: ' .. clashObj)
results:add(' name: ' .. (info.name or '?'))
results:add(' id: ' .. (info.id or '?'))
results:add(' description: ' .. (info.description or '?'))
results:add(' reason: ' .. (info.reason or '?'))
else
-- Should not occur!
results:add(' info not found!')
end
else
results:add(' not sensitive')
end
else
-- Report problem?
end
return JSON:encode(jsonResult)
elseif options.format == nil or options.format == 'lua' then
return result
elseif type(options.format) ~= 'string' then
return makeError(
'sipa-format-type-error',
string.format(
"'format' options key was type %s (expected string or nil)",
type(options.format)
)
)
else
return makeError(
'sipa-invalid-format',
string.format(
"invalid format '%s' (expected 'json' or 'lua')",
type(options.format)
)
)
end
return preBlock(results:join('\n'))
end
 
Line 576 ⟶ 489:
 
local p = {}
p.main = main
 
function p.isValidSensitivityReason_isValidSensitivityReason(s)
-- Return true if s is a valid sensitivity reason; otherwise return false.
return s ~= nil and SensitiveEntity.reasons[s] ~= nil
checkType('isValidSensitivityReason', 1, s, 'string')
return SensitiveEntity.reasons[s] ~= nil
end
 
function p.getSensitivityReasons_getSensitivityReasons(separator, conjunction)
-- Return an arraystring of valid sensitivity reasons, ordered alphabetically.
-- The reasons are separated by an optional separator; if conjunction is
local ret = {}
-- specified it is used instead of the last separator, as in
-- mw.text.listToText.
 
-- Get an array of valid sensitivity reasons.
local reasons = {}
for reason in pairs(SensitiveEntity.reasons) do
retreasons[#retreasons + 1] = reason
end
table.sort(reasons)
 
-- Convert arguments if we are being called from wikitext.
if type(separator) == 'table' and type(separator.getParent) == 'function' then
-- separator is a frame object
local frame = separator
separator = frame.args[1]
conjunction = frame.args[2]
end
table.sort(ret)
return ret
end
 
-- Return a formatted string
function p.query()
return mw.text.listToText(reasons, separator, conjunction)
end
 
-- Export the API query function
p.query = query
 
return p