<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://salisford.net/index.php?action=history&amp;feed=atom&amp;title=Module%3ASensitive_IP_addresses%2FAPI</id>
	<title>Module:Sensitive IP addresses/API - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://salisford.net/index.php?action=history&amp;feed=atom&amp;title=Module%3ASensitive_IP_addresses%2FAPI"/>
	<link rel="alternate" type="text/html" href="https://salisford.net/index.php?title=Module:Sensitive_IP_addresses/API&amp;action=history"/>
	<updated>2026-05-05T03:45:30Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.36.1</generator>
	<entry>
		<id>https://salisford.net/index.php?title=Module:Sensitive_IP_addresses/API&amp;diff=558&amp;oldid=prev</id>
		<title>Cascadia: 1 revision imported</title>
		<link rel="alternate" type="text/html" href="https://salisford.net/index.php?title=Module:Sensitive_IP_addresses/API&amp;diff=558&amp;oldid=prev"/>
		<updated>2023-04-05T14:38:15Z</updated>

		<summary type="html">&lt;p&gt;1 revision imported&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;1&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;1&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 14:38, 5 April 2023&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-notice&quot; lang=&quot;en&quot;&gt;&lt;div class=&quot;mw-diff-empty&quot;&gt;(No difference)&lt;/div&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</summary>
		<author><name>Cascadia</name></author>
	</entry>
	<entry>
		<id>https://salisford.net/index.php?title=Module:Sensitive_IP_addresses/API&amp;diff=557&amp;oldid=prev</id>
		<title>wp&gt;Mr. Stradivarius: Protected &quot;Module:Sensitive IP addresses/API&quot;: High-risk Lua module: used in the MediaWiki interface, e.g. MediaWiki:Blockiptext via Template:Sensitive IP addresses ([Edit=Require administrator access] (indefinite...</title>
		<link rel="alternate" type="text/html" href="https://salisford.net/index.php?title=Module:Sensitive_IP_addresses/API&amp;diff=557&amp;oldid=prev"/>
		<updated>2016-11-23T09:27:57Z</updated>

		<summary type="html">&lt;p&gt;Protected &amp;quot;&lt;a href=&quot;/index.php?title=Module:Sensitive_IP_addresses/API&quot; title=&quot;Module:Sensitive IP addresses/API&quot;&gt;Module:Sensitive IP addresses/API&lt;/a&gt;&amp;quot;: &lt;a href=&quot;/index.php?title=WP:High-risk_templates&amp;amp;action=edit&amp;amp;redlink=1&quot; class=&quot;new&quot; title=&quot;WP:High-risk templates (page does not exist)&quot;&gt;High-risk Lua module&lt;/a&gt;: used in the MediaWiki interface, e.g. &lt;a href=&quot;/index.php?title=MediaWiki:Blockiptext&quot; title=&quot;MediaWiki:Blockiptext&quot;&gt;MediaWiki:Blockiptext&lt;/a&gt; via &lt;a href=&quot;/index.php?title=Template:Sensitive_IP_addresses&amp;amp;action=edit&amp;amp;redlink=1&quot; class=&quot;new&quot; title=&quot;Template:Sensitive IP addresses (page does not exist)&quot;&gt;Template:Sensitive IP addresses&lt;/a&gt; ([Edit=Require administrator access] (indefinite...&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;-- This module provides functions for handling sensitive IP addresses.&lt;br /&gt;
&lt;br /&gt;
-- Load modules&lt;br /&gt;
local mIP = require('Module:IP')&lt;br /&gt;
local IPAddress = mIP.IPAddress&lt;br /&gt;
local Subnet = mIP.Subnet&lt;br /&gt;
local IPv4Collection = mIP.IPv4Collection&lt;br /&gt;
local IPv6Collection = mIP.IPv6Collection&lt;br /&gt;
&lt;br /&gt;
-- Lazily load the jf-JSON module&lt;br /&gt;
local JSON&lt;br /&gt;
&lt;br /&gt;
-------------------------------------------------------------------------------&lt;br /&gt;
-- Helper functions&lt;br /&gt;
-------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local function deepCopy(val)&lt;br /&gt;
	-- Make a deep copy of a value, but don't worry about self-references or&lt;br /&gt;
	-- metatables as mw.clone does. If a table in val has a self-reference,&lt;br /&gt;
	-- you will get an infinite loop, so don't do that.&lt;br /&gt;
	if type(val) == 'table' then&lt;br /&gt;
		local ret = {}&lt;br /&gt;
		for k, v in pairs(val) do&lt;br /&gt;
			ret[k] = deepCopy(v)&lt;br /&gt;
		end&lt;br /&gt;
		return ret&lt;br /&gt;
	else&lt;br /&gt;
		return val&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function deepCopyInto(source, dest)&lt;br /&gt;
	-- Do a deep copy of a source table into a destination table, ignoring&lt;br /&gt;
	-- self-references and metatables. If a table in source has a self-reference&lt;br /&gt;
	-- you will get an infinite loop.&lt;br /&gt;
	for k, v in pairs(source) do&lt;br /&gt;
		if type(v) == 'table' then&lt;br /&gt;
			dest[k] = {}&lt;br /&gt;
			deepCopyInto(v, dest[k])&lt;br /&gt;
		else&lt;br /&gt;
			dest[k] = v&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function removeDuplicates(t)&lt;br /&gt;
	-- Return a copy of an array with duplicate values removed.&lt;br /&gt;
	local keys, ret = {}, {}&lt;br /&gt;
	for i, v in ipairs(t) do&lt;br /&gt;
		if not keys[v] then&lt;br /&gt;
			table.insert(ret, v)&lt;br /&gt;
			keys[v] = true&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return ret&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-------------------------------------------------------------------------------&lt;br /&gt;
-- SensitiveEntity class&lt;br /&gt;
-- A country or organization for which blocks must be handled with care.&lt;br /&gt;
-- Media organizations may inspect block messages for IP addresses and ranges&lt;br /&gt;
-- belonging to these entities and those messages may end up in the press.&lt;br /&gt;
-------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local SensitiveEntity = {}&lt;br /&gt;
SensitiveEntity.__index = SensitiveEntity&lt;br /&gt;
&lt;br /&gt;
SensitiveEntity.reasons = {&lt;br /&gt;
	-- The reasons that an entity may be sensitive. Used to verify data in&lt;br /&gt;
	-- Module:Sensitive IP addresses/list.&lt;br /&gt;
	political = true,&lt;br /&gt;
	technical = true,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
do&lt;br /&gt;
	-- Private methods&lt;br /&gt;
	local function addRanges(self, key, collectionConstructor, ranges)&lt;br /&gt;
		if ranges and ranges[1] then&lt;br /&gt;
			self[key] = collectionConstructor()&lt;br /&gt;
			for i, range in ipairs(ranges) do&lt;br /&gt;
				self[key]:addSubnet(Subnet.new(range))&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	-- Constructor&lt;br /&gt;
	function SensitiveEntity.new(data)&lt;br /&gt;
		local self = setmetatable({}, SensitiveEntity)&lt;br /&gt;
&lt;br /&gt;
		-- Set data&lt;br /&gt;
		self.data = data&lt;br /&gt;
		addRanges(self, 'v4Collection', IPv4Collection.new, data.ipv4Ranges)&lt;br /&gt;
		addRanges(self, 'v6Collection', IPv6Collection.new, data.ipv6Ranges)&lt;br /&gt;
&lt;br /&gt;
		return self&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function SensitiveEntity:matchesIPOrRange(str)&lt;br /&gt;
	-- Returns true, matchObj, queryObj if there is a match for the IP address&lt;br /&gt;
	-- string or CIDR range str in the sensitive entity. Returns false&lt;br /&gt;
	-- otherwise. matchObj is the Subnet object that was matched, and queryObj&lt;br /&gt;
	-- is the IPAddress or Subnet object corresponding to the input string.&lt;br /&gt;
&lt;br /&gt;
	-- Get the IPAddress or Subnet object for str&lt;br /&gt;
	local isIP, isSubnet, obj&lt;br /&gt;
	isIP, obj = pcall(IPAddress.new, str)&lt;br /&gt;
	if isIP and not obj then&lt;br /&gt;
		isIP = false&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	if not isIP then&lt;br /&gt;
		isSubnet, obj = pcall(Subnet.new, str)&lt;br /&gt;
		if not isSubnet or not obj then&lt;br /&gt;
			error(string.format(&lt;br /&gt;
				&amp;quot;'%s' is not a valid IP address or CIDR string&amp;quot;,&lt;br /&gt;
				str&lt;br /&gt;
			), 2)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	-- Try matching the object to the appropriate collection&lt;br /&gt;
	local function isInCollection(collection, obj, isIP)&lt;br /&gt;
		if isIP then&lt;br /&gt;
			if collection then&lt;br /&gt;
				local isMatch, matchObj = collection:containsIP(obj)&lt;br /&gt;
				return isMatch, matchObj, obj&lt;br /&gt;
			else&lt;br /&gt;
				return false&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			if collection then&lt;br /&gt;
				local isMatch, matchObj = collection:overlapsSubnet(obj)&lt;br /&gt;
				return isMatch, matchObj, obj&lt;br /&gt;
			else&lt;br /&gt;
				return false&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	if obj:isIPv4() then&lt;br /&gt;
		return isInCollection(self.v4Collection, obj, isIP)&lt;br /&gt;
	else&lt;br /&gt;
		return isInCollection(self.v6Collection, obj, isIP)&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-------------------------------------------------------------------------------&lt;br /&gt;
-- Sensitive IP API&lt;br /&gt;
-------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
-- This API is used by external tools and gadgets, so it should be kept&lt;br /&gt;
-- backwards-compatible. Clients query the API with a query table, and the&lt;br /&gt;
-- API returns a response table. The response table is available as a Lua table&lt;br /&gt;
-- for other Lua modules, and as JSON for external clients.&lt;br /&gt;
&lt;br /&gt;
-- Example query tables:&lt;br /&gt;
--&lt;br /&gt;
-- Query IP addresses and ranges:&lt;br /&gt;
-- {&lt;br /&gt;
-- 	test = {'1.2.3.4', '4.5.6.0/24', '2001:db8::ff00:12:3456', '2001:db8::ff00:12:0/112'},&lt;br /&gt;
-- }&lt;br /&gt;
--&lt;br /&gt;
-- Query specific entities:&lt;br /&gt;
-- {&lt;br /&gt;
-- 	entities = {'ussenate', 'ushr'}&lt;br /&gt;
-- }&lt;br /&gt;
--&lt;br /&gt;
-- Query all entities:&lt;br /&gt;
-- {&lt;br /&gt;
-- 	entities = {'all'}&lt;br /&gt;
-- }&lt;br /&gt;
--&lt;br /&gt;
-- Query all entities and format the result as a JSON string:&lt;br /&gt;
-- {&lt;br /&gt;
-- 	entities = {'all'},&lt;br /&gt;
--  format = 'json'&lt;br /&gt;
-- }&lt;br /&gt;
--&lt;br /&gt;
-- Combined query:&lt;br /&gt;
-- {&lt;br /&gt;
-- 	test = {'1.2.3.4', '4.5.6.0/24', '2001:db8::ff00:12:3456', '2001:db8::ff00:12:0/112'},&lt;br /&gt;
-- 	entities = {'ussenate', 'ushr'}&lt;br /&gt;
-- }&lt;br /&gt;
&lt;br /&gt;
-- Example response:&lt;br /&gt;
--&lt;br /&gt;
-- {&lt;br /&gt;
--     sensitiveips = {&lt;br /&gt;
--         matches = {&lt;br /&gt;
--             {&lt;br /&gt;
--                 ip = '1.2.3.4',&lt;br /&gt;
--                 type = 'ip',&lt;br /&gt;
--                 ['ip-version'] = 'IPv4',&lt;br /&gt;
--                 ['matches-range'] = '1.2.3.0/24',&lt;br /&gt;
--                 ['entity-id'] = 'entityid'&lt;br /&gt;
--             },&lt;br /&gt;
--             {&lt;br /&gt;
--                 range = '4.5.6.0/24',&lt;br /&gt;
--                 type = 'range',&lt;br /&gt;
--                 ['ip-version'] = 'IPv4',&lt;br /&gt;
--                 ['matches-range'] = '4.5.0.0/16',&lt;br /&gt;
--                 ['entity-id'] = 'entityid'&lt;br /&gt;
--             }&lt;br /&gt;
--         },&lt;br /&gt;
--         ['matched-ranges'] = {&lt;br /&gt;
--             ['1.2.3.0/24'] = {&lt;br /&gt;
--                 range = '1.2.3.0/24',&lt;br /&gt;
--                 ['ip-version'] = 'IPv4',&lt;br /&gt;
--                 ['entity-id'] = 'entityid'&lt;br /&gt;
--             },&lt;br /&gt;
--             ['4.5.0.0/16'] = {&lt;br /&gt;
--                 range = '4.5.0.0/16',&lt;br /&gt;
--                 ['ip-version'] = 'IPv4',&lt;br /&gt;
--                 ['entity-id'] = 'entityid'&lt;br /&gt;
--             }&lt;br /&gt;
--         },&lt;br /&gt;
--         entities = {&lt;br /&gt;
--             ['entityid'] = {&lt;br /&gt;
--                 id = 'entityid',&lt;br /&gt;
--                 name = 'The entity name',&lt;br /&gt;
--                 description = 'A description of the entity',&lt;br /&gt;
--                 ['ipv4-ranges'] = {&lt;br /&gt;
--                     '1.2.3.0/24',&lt;br /&gt;
--                     '4.5.0.0/16'&lt;br /&gt;
--                     '6.7.0.0/16'&lt;br /&gt;
--                 },&lt;br /&gt;
--                 ['ipv6-ranges'] = {&lt;br /&gt;
--                     '2001:db8::ff00:12:0/112'&lt;br /&gt;
--                 },&lt;br /&gt;
--                 notes = 'Notes about the entity or its ranges'&lt;br /&gt;
--             }&lt;br /&gt;
--         }&lt;br /&gt;
--         ['entity-ids'] = {&lt;br /&gt;
--             'entityid'&lt;br /&gt;
--         }&lt;br /&gt;
--     }&lt;br /&gt;
-- }&lt;br /&gt;
--&lt;br /&gt;
-- Response with errors:&lt;br /&gt;
--&lt;br /&gt;
-- {&lt;br /&gt;
--     error = {&lt;br /&gt;
--         code = 'example-error',&lt;br /&gt;
--         info = 'There was an error',&lt;br /&gt;
--         ['*'] = 'See https://en.wikipedia.org/wiki/Module:Sensitive_IP_addresses for API usage'&lt;br /&gt;
--     }&lt;br /&gt;
-- }&lt;br /&gt;
&lt;br /&gt;
local function query(options)&lt;br /&gt;
	-- Make entity objects&lt;br /&gt;
	local entities, entityIndexes = {}, {}&lt;br /&gt;
	local data = mw.loadData('Module:Sensitive IP addresses/list')&lt;br /&gt;
	for i, entityData in ipairs(data) do&lt;br /&gt;
		entities[entityData.id] = SensitiveEntity.new(entityData)&lt;br /&gt;
		entityIndexes[entityData.id] = i -- Keep track of the original order&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local function makeError(code, info, format)&lt;br /&gt;
		local ret = {['error'] = {&lt;br /&gt;
			code = code,&lt;br /&gt;
			info = info,&lt;br /&gt;
			['*'] = 'See https://en.wikipedia.org/wiki/Module:Sensitive_IP_addresses/API for API usage',&lt;br /&gt;
		}}&lt;br /&gt;
		if format == 'json' then&lt;br /&gt;
			return mw.text.jsonEncode(ret)&lt;br /&gt;
		else&lt;br /&gt;
			return ret&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	-- Construct result&lt;br /&gt;
	local result = {&lt;br /&gt;
		matches = {},&lt;br /&gt;
		['matched-ranges'] = {},&lt;br /&gt;
		entities = {},&lt;br /&gt;
		['entity-ids'] = {}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	if type(options) ~= 'table' then&lt;br /&gt;
		return makeError(&lt;br /&gt;
			'sipa-options-type-error',&lt;br /&gt;
			string.format(&lt;br /&gt;
				&amp;quot;type error in argument #1 of 'query' (expected table, received %s)&amp;quot;,&lt;br /&gt;
				type(options)&lt;br /&gt;
			)&lt;br /&gt;
		)&lt;br /&gt;
	elseif not options.test and not options.entities then&lt;br /&gt;
		return makeError(&lt;br /&gt;
			'sipa-blank-options',&lt;br /&gt;
			&amp;quot;the options table didn't contain a 'test' or an 'entities' key&amp;quot;,&lt;br /&gt;
			options.format&lt;br /&gt;
		)&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	if options.test then&lt;br /&gt;
		if type(options.test) ~= 'table' then&lt;br /&gt;
			return makeError(&lt;br /&gt;
				'sipa-test-type-error',&lt;br /&gt;
				string.format(&lt;br /&gt;
					&amp;quot;'test' options key was type %s (expected table)&amp;quot;,&lt;br /&gt;
					type(options.test)&lt;br /&gt;
				),&lt;br /&gt;
				options.format&lt;br /&gt;
			)&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		for i, testString in ipairs(options.test) do&lt;br /&gt;
			if type(testString) ~= 'string' then&lt;br /&gt;
				return makeError(&lt;br /&gt;
					'sipa-test-string-type-error',&lt;br /&gt;
					string.format(&lt;br /&gt;
						&amp;quot;type error in item #%d in the 'test' array (expected string, received %s)&amp;quot;,&lt;br /&gt;
						i,&lt;br /&gt;
						type(testString)&lt;br /&gt;
					),&lt;br /&gt;
					options.format&lt;br /&gt;
				)&lt;br /&gt;
			end&lt;br /&gt;
&lt;br /&gt;
			for k, entity in pairs(entities) do&lt;br /&gt;
				-- Try to match the range with the current sensitive entity.&lt;br /&gt;
				local success, isMatch, matchObj, queryObj = pcall(&lt;br /&gt;
					entity.matchesIPOrRange,&lt;br /&gt;
					entity,&lt;br /&gt;
					testString&lt;br /&gt;
				)&lt;br /&gt;
				if not success then&lt;br /&gt;
					-- The string was invalid.&lt;br /&gt;
					return makeError(&lt;br /&gt;
						'sipa-invalid-test-string',&lt;br /&gt;
						string.format(&lt;br /&gt;
							&amp;quot;test string #%d '%s' was not a valid IP address or CIDR string&amp;quot;,&lt;br /&gt;
							i,&lt;br /&gt;
							testString&lt;br /&gt;
						),&lt;br /&gt;
						options.format&lt;br /&gt;
					)&lt;br /&gt;
				end&lt;br /&gt;
				if isMatch then&lt;br /&gt;
					-- The string was a sensitive IP address or subnet.&lt;br /&gt;
&lt;br /&gt;
					-- Add match data&lt;br /&gt;
					local match = {}&lt;br /&gt;
					-- Quick and dirty hack to find if queryObj is an IPAddress object.&lt;br /&gt;
					local isIP = queryObj.getNextIP ~= nil and queryObj.isInSubnet ~= nil&lt;br /&gt;
					if isIP then&lt;br /&gt;
						match.type = 'ip'&lt;br /&gt;
						match.ip = tostring(queryObj)&lt;br /&gt;
					else&lt;br /&gt;
						match.type = 'range'&lt;br /&gt;
						match.range = tostring(queryObj)&lt;br /&gt;
					end&lt;br /&gt;
					match['ip-version'] = queryObj:getVersion()&lt;br /&gt;
					match['matches-range'] = matchObj:getCIDR()&lt;br /&gt;
					match['entity-id'] = entity.data.id&lt;br /&gt;
					table.insert(result.matches, match)&lt;br /&gt;
&lt;br /&gt;
					-- Add the matched range data.&lt;br /&gt;
					result['matched-ranges'][match['matches-range']] = {&lt;br /&gt;
						range = match['matches-range'],&lt;br /&gt;
						['ip-version'] = match['ip-version'],&lt;br /&gt;
						['entity-id'] = match['entity-id'],&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					-- Add the entity data for the entity we matched.&lt;br /&gt;
					result.entities[match['entity-id']] = deepCopy(&lt;br /&gt;
						entities[match['entity-id']].data&lt;br /&gt;
					)&lt;br /&gt;
&lt;br /&gt;
					-- Add the entity ID for the entity we matched.&lt;br /&gt;
					table.insert(result['entity-ids'], match['entity-id'])&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	-- Add entity data requested explicitly.&lt;br /&gt;
	if options.entities then&lt;br /&gt;
		if type(options.entities) ~= 'table' then&lt;br /&gt;
			return makeError(&lt;br /&gt;
				'sipa-entities-type-error',&lt;br /&gt;
				string.format(&lt;br /&gt;
					&amp;quot;'entities' options key was type %s (expected table)&amp;quot;,&lt;br /&gt;
					type(options.test)&lt;br /&gt;
				),&lt;br /&gt;
				options.format&lt;br /&gt;
			)&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		-- Check the type of all the entity strings, and check if 'all' has&lt;br /&gt;
		-- been specified.&lt;br /&gt;
		local isAll = false&lt;br /&gt;
		for i, entityString in ipairs(options.entities) do&lt;br /&gt;
			if type(entityString) ~= 'string' then&lt;br /&gt;
				return makeError(&lt;br /&gt;
					'sipa-entity-string-type-error',&lt;br /&gt;
					string.format(&lt;br /&gt;
						&amp;quot;type error in item #%d in the 'entities' array (expected string, received %s)&amp;quot;,&lt;br /&gt;
						i,&lt;br /&gt;
						type(entityString)&lt;br /&gt;
					),&lt;br /&gt;
					options.format&lt;br /&gt;
				)&lt;br /&gt;
			end&lt;br /&gt;
			if entityString == 'all' then&lt;br /&gt;
				isAll = true&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		if isAll then&lt;br /&gt;
			-- Add all the entity data.&lt;br /&gt;
			-- As the final result will contain all the entity data, we can&lt;br /&gt;
			-- just create the entities and entity-ids subtables from scratch&lt;br /&gt;
			-- without worrying about what any existing values might be.&lt;br /&gt;
			result.entities = {}&lt;br /&gt;
			result['entity-ids'] = {}&lt;br /&gt;
			for i, entityData in ipairs(data) do&lt;br /&gt;
				result.entities[entityData.id] = deepCopy(entityData)&lt;br /&gt;
				result['entity-ids'][i] = entityData.id&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			-- Add data for the entities specified.&lt;br /&gt;
			-- Insert the entity and entity-id subtables if they aren't already&lt;br /&gt;
			-- present.&lt;br /&gt;
			for i, entityString in ipairs(options.entities) do&lt;br /&gt;
				if entities[entityString] then&lt;br /&gt;
					result.entities[entityString] = deepCopy(&lt;br /&gt;
						entities[entityString].data&lt;br /&gt;
					)&lt;br /&gt;
					table.insert(result['entity-ids'], entityString)&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
			result['entity-ids'] = removeDuplicates(result['entity-ids'])&lt;br /&gt;
			table.sort(result['entity-ids'], function(s1, s2)&lt;br /&gt;
				return entityIndexes[s1] &amp;lt; entityIndexes[s2]&lt;br /&gt;
			end)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	-- Add any missing reason fields from entities.&lt;br /&gt;
	for id, entityData in pairs(result.entities) do&lt;br /&gt;
		entityData.reason = entityData.reason or 'political'&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	-- Wrap the result in an outer layer like the MediaWiki Action API does.&lt;br /&gt;
	result = {sensitiveips = result}&lt;br /&gt;
&lt;br /&gt;
	if options.format == 'json' then&lt;br /&gt;
		-- Load jf-JSON&lt;br /&gt;
		JSON = JSON or require('Module:jf-JSON')&lt;br /&gt;
		JSON.strictTypes = true -- Necessary for correct blank-object encoding&lt;br /&gt;
		-- Decode a skeleton result JSON string. This ensures that blank objects&lt;br /&gt;
		-- are re-encoded as blank objects and not as blank arrays.&lt;br /&gt;
		local jsonResult = JSON:decode([[{&amp;quot;sensitiveips&amp;quot;: {&lt;br /&gt;
			&amp;quot;matches&amp;quot;: [],&lt;br /&gt;
			&amp;quot;matched-ranges&amp;quot;: {},&lt;br /&gt;
			&amp;quot;entities&amp;quot;: {},&lt;br /&gt;
			&amp;quot;entity-ids&amp;quot;: []&lt;br /&gt;
		}}]])&lt;br /&gt;
		for i, key in ipairs{'matches', 'matched-ranges', 'entities', 'entity-ids'} do&lt;br /&gt;
			deepCopyInto(result.sensitiveips[key], jsonResult.sensitiveips[key])&lt;br /&gt;
		end&lt;br /&gt;
		return JSON:encode(jsonResult)&lt;br /&gt;
	elseif options.format == nil or options.format == 'lua' then&lt;br /&gt;
		return result&lt;br /&gt;
	elseif type(options.format) ~= 'string' then&lt;br /&gt;
		return makeError(&lt;br /&gt;
			'sipa-format-type-error',&lt;br /&gt;
			string.format(&lt;br /&gt;
				&amp;quot;'format' options key was type %s (expected string or nil)&amp;quot;,&lt;br /&gt;
				type(options.format)&lt;br /&gt;
			)&lt;br /&gt;
		)&lt;br /&gt;
	else&lt;br /&gt;
		return makeError(&lt;br /&gt;
			'sipa-invalid-format',&lt;br /&gt;
			string.format(&lt;br /&gt;
				&amp;quot;invalid format '%s' (expected 'json' or 'lua')&amp;quot;,&lt;br /&gt;
				type(options.format)&lt;br /&gt;
			)&lt;br /&gt;
		)&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--------------------------------------------------------------------------------&lt;br /&gt;
-- Exports&lt;br /&gt;
--------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
function p._isValidSensitivityReason(s)&lt;br /&gt;
	-- Return true if s is a valid sensitivity reason; otherwise return false.&lt;br /&gt;
	return s ~= nil and SensitiveEntity.reasons[s] ~= nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function p._getSensitivityReasons(separator, conjunction)&lt;br /&gt;
	-- Return an string of valid sensitivity reasons, ordered alphabetically.&lt;br /&gt;
	-- The reasons are separated by an optional separator; if conjunction is&lt;br /&gt;
	-- specified it is used instead of the last separator, as in&lt;br /&gt;
	-- mw.text.listToText.&lt;br /&gt;
&lt;br /&gt;
	-- Get an array of valid sensitivity reasons.&lt;br /&gt;
	local reasons = {}&lt;br /&gt;
	for reason in pairs(SensitiveEntity.reasons) do&lt;br /&gt;
		reasons[#reasons + 1] = reason&lt;br /&gt;
	end&lt;br /&gt;
	table.sort(reasons)&lt;br /&gt;
&lt;br /&gt;
	-- Convert arguments if we are being called from wikitext.&lt;br /&gt;
	if type(separator) == 'table' and type(separator.getParent) == 'function' then&lt;br /&gt;
		-- separator is a frame object&lt;br /&gt;
		local frame = separator&lt;br /&gt;
		separator = frame.args[1]&lt;br /&gt;
		conjunction = frame.args[2]&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	-- Return a formatted string&lt;br /&gt;
	return mw.text.listToText(reasons, separator, conjunction)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Export the API query function&lt;br /&gt;
p.query = query&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>wp&gt;Mr. Stradivarius</name></author>
	</entry>
</feed>