Permanently protected module
From Wikipedia, the free encyclopedia


-- This module provides functions for handling sensitive IP addresses.



-- Load modules

local mIP = require('Module:IP')

local IPAddress = mIP.IPAddress

local Subnet = mIP.Subnet

local IPv4Collection = mIP.IPv4Collection

local IPv6Collection = mIP.IPv6Collection



-- Lazily load the jf-JSON module

local JSON



-------------------------------------------------------------------------------

-- Helper functions

-------------------------------------------------------------------------------



local function deepCopy(val)

	-- Make a deep copy of a value, but don't worry about self-references or

	-- metatables as mw.clone does. If a table in val has a self-reference,

	-- you will get an infinite loop, so don't do that.

	if type(val) == 'table' then

		local ret = {}

		for k, v in pairs(val) do

			retk = deepCopy(v)

		end

		return ret

	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

			destk = {}

			deepCopyInto(v, destk])

		else

			destk = v

		end

	end

end



local function removeDuplicates(t)

	-- Return a copy of an array with duplicate values removed.

	local keys, ret = {}, {}

	for i, v in ipairs(t) do

		if not keysv then

			table.insert(ret, v)

			keysv = true

		end

	end

	return ret

end



-------------------------------------------------------------------------------

-- SensitiveEntity class

-- A country or organization for which blocks must be handled with care.

-- Media organizations may inspect block messages for IP addresses and ranges

-- belonging to these entities and those messages may end up in the press.

-------------------------------------------------------------------------------



local SensitiveEntity = {}

SensitiveEntity.__index = SensitiveEntity



SensitiveEntity.reasons = {

	-- The reasons that an entity may be sensitive. Used to verify data in

	-- Module:Sensitive IP addresses/list.

	political = true,

	technical = true,

}



do

	-- Private methods

	local function addRanges(self, key, collectionConstructor, ranges)

		if ranges and ranges1 then

			selfkey = collectionConstructor()

			for i, range in ipairs(ranges) do

				selfkey]:addSubnet(Subnet.new(range))

			end

		end

	end



	-- Constructor

	function SensitiveEntity.new(data)

		local self = setmetatable({}, SensitiveEntity)



		-- Set data

		self.data = data

		addRanges(self, 'v4Collection', IPv4Collection.new, data.ipv4Ranges)

		addRanges(self, 'v6Collection', IPv6Collection.new, data.ipv6Ranges)



		return self

	end

end



function SensitiveEntity:matchesIPOrRange(str)

	-- Returns true, matchObj, queryObj if there is a match for the IP address

	-- string or CIDR range str in the sensitive entity. Returns false

	-- otherwise. matchObj is the Subnet object that was matched, and queryObj

	-- is the IPAddress or Subnet object corresponding to the input string.



	-- Get the IPAddress or Subnet object for str

	local isIP, isSubnet, obj

	isIP, obj = pcall(IPAddress.new, str)

	if isIP and not obj then

		isIP = false

	end



	if not isIP then

		isSubnet, obj = pcall(Subnet.new, str)

		if not isSubnet or not obj then

			error(string.format(

				"'%s' is not a valid IP address or CIDR string",

				str

			), 2)

		end

	end



	-- Try matching the object to the appropriate collection

	local function isInCollection(collection, obj, isIP)

		if isIP then

			if collection then

				local isMatch, matchObj = collection:containsIP(obj)

				return isMatch, matchObj, obj

			else

				return false

			end

		else

			if collection then

				local isMatch, matchObj = collection:overlapsSubnet(obj)

				return isMatch, matchObj, obj

			else

				return false

			end

		end

	end



	if obj:isIPv4() then

		return isInCollection(self.v4Collection, obj, isIP)

	else

		return isInCollection(self.v6Collection, obj, isIP)

	end

end



-------------------------------------------------------------------------------

-- Sensitive IP API

-------------------------------------------------------------------------------



-- This API is used by external tools and gadgets, so it should be kept

-- backwards-compatible. Clients query the API with a query table, and the

-- API returns a response table. The response table is available as a Lua table

-- for other Lua modules, and as JSON for external clients.



-- Example query tables:

--

-- Query IP addresses and ranges:

-- {

-- 	test = {'1.2.3.4', '4.5.6.0/24', '2001:db8::ff00:12:3456', '2001:db8::ff00:12:0/112'},

-- }

--

-- Query specific entities:

-- {

-- 	entities = {'ussenate', 'ushr'}

-- }

--

-- Query all entities:

-- {

-- 	entities = {'all'}

-- }

--

-- Query all entities and format the result as a JSON string:

-- {

-- 	entities = {'all'},

--  format = 'json'

-- }

--

-- Combined query:

-- {

-- 	test = {'1.2.3.4', '4.5.6.0/24', '2001:db8::ff00:12:3456', '2001:db8::ff00:12:0/112'},

-- 	entities = {'ussenate', 'ushr'}

-- }



-- Example response:

--

-- {

--     sensitiveips = {

--         matches = {

--             {

--                 ip = '1.2.3.4',

--                 type = 'ip',

--                 ['ip-version'] = 'IPv4',

--                 ['matches-range'] = '1.2.3.0/24',

--                 ['entity-id'] = 'entityid'

--             },

--             {

--                 range = '4.5.6.0/24',

--                 type = 'range',

--                 ['ip-version'] = 'IPv4',

--                 ['matches-range'] = '4.5.0.0/16',

--                 ['entity-id'] = 'entityid'

--             }

--         },

--         ['matched-ranges'] = {

--             ['1.2.3.0/24'] = {

--                 range = '1.2.3.0/24',

--                 ['ip-version'] = 'IPv4',

--                 ['entity-id'] = 'entityid'

--             },

--             ['4.5.0.0/16'] = {

--                 range = '4.5.0.0/16',

--                 ['ip-version'] = 'IPv4',

--                 ['entity-id'] = 'entityid'

--             }

--         },

--         entities = {

--             ['entityid'] = {

--                 id = 'entityid',

--                 name = 'The entity name',

--                 description = 'A description of the entity',

--                 ['ipv4-ranges'] = {

--                     '1.2.3.0/24',

--                     '4.5.0.0/16'

--                     '6.7.0.0/16'

--                 },

--                 ['ipv6-ranges'] = {

--                     '2001:db8::ff00:12:0/112'

--                 },

--                 notes = 'Notes about the entity or its ranges'

--             }

--         }

--         ['entity-ids'] = {

--             'entityid'

--         }

--     }

-- }

--

-- Response with errors:

--

-- {

--     error = {

--         code = 'example-error',

--         info = 'There was an error',

--         ['*'] = 'See /info/en/?search=Module:Sensitive_IP_addresses for API usage'

--     }

-- }



local function query(options)

	-- Make entity objects

	local entities, entityIndexes = {}, {}

	local data = mw.loadData('Module:Sensitive IP addresses/list')

	for i, entityData in ipairs(data) do

		entitiesentityData.id = SensitiveEntity.new(entityData)

		entityIndexesentityData.id = i -- Keep track of the original order

	end



	local function makeError(code, info, format)

		local ret = {['error' = {

			code = code,

			info = info,

			'*' = 'See /info/en/?search=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

		return makeError(

			'sipa-options-type-error',

			string.format(

				"type error in argument #1 of 'query' (expected table, received %s)",

				type(options)

			)

		)

	elseif not options.test and not options.entities then

		return makeError(

			'sipa-blank-options',

			"the options table didn't contain a 'test' or an 'entities' key",

			options.format

		)

	end



	if options.test then

		if type(options.test) ~= 'table' then

			return makeError(

				'sipa-test-type-error',

				string.format(

					"'test' options key was type %s (expected table)",

					type(options.test)

				),

				options.format

			)

		end



		for i, testString in ipairs(options.test) do

			if type(testString) ~= 'string' then

				return makeError(

					'sipa-test-string-type-error',

					string.format(

						"type error in item #%d in the 'test' array (expected string, received %s)",

						i,

						type(testString)

					),

					options.format

				)

			end



			for k, entity in pairs(entities) do

				-- Try to match the range with the current sensitive entity.

				local success, isMatch, matchObj, queryObj = pcall(

					entity.matchesIPOrRange,

					entity,

					testString

				)

				if not success then

					-- The string was invalid.

					return makeError(

						'sipa-invalid-test-string',

						string.format(

							"test string #%d '%s' was not a valid IP address or CIDR string",

							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

					if isIP then

						match.type = 'ip'

						match.ip = tostring(queryObj)

					else

						match.type = 'range'

						match.range = tostring(queryObj)

					end

					match'ip-version' = queryObj:getVersion()

					match'matches-range' = matchObj:getCIDR()

					match'entity-id' = entity.data.id

					table.insert(result.matches, match)



					-- Add the matched range data.

					result'matched-ranges'][match'matches-range']] = {

						range = match'matches-range'],

						'ip-version' = match'ip-version'],

						'entity-id' = match'entity-id'],

					}



					-- Add the entity data for the entity we matched.

					result.entitiesmatch'entity-id']] = deepCopy(

						entitiesmatch'entity-id']].data

					)



					-- Add the entity ID for the entity we matched.

					table.insert(result'entity-ids'], match'entity-id'])

				end

			end

		end

	end



	-- Add entity data requested explicitly.

	if options.entities then

		if type(options.entities) ~= 'table' then

			return makeError(

				'sipa-entities-type-error',

				string.format(

					"'entities' options key was type %s (expected table)",

					type(options.test)

				),

				options.format

			)

		end



		-- Check the type of all the entity strings, and check if 'all' has

		-- been specified.

		local isAll = false

		for i, entityString in ipairs(options.entities) do

			if type(entityString) ~= 'string' then

				return makeError(

					'sipa-entity-string-type-error',

					string.format(

						"type error in item #%d in the 'entities' array (expected string, received %s)",

						i,

						type(entityString)

					),

					options.format

				)

			end

			if entityString == 'all' then

				isAll = true

			end

		end



		if isAll then

			-- Add all the entity data.

			-- As the final result will contain all the entity data, we can

			-- just create the entities and entity-ids subtables from scratch

			-- without worrying about what any existing values might be.

			result.entities = {}

			result'entity-ids' = {}

			for i, entityData in ipairs(data) do

				result.entitiesentityData.id = deepCopy(entityData)

				result'entity-ids'][i = entityData.id

			end

		else

			-- Add data for the entities specified.

			-- Insert the entity and entity-id subtables if they aren't already

			-- present.

			for i, entityString in ipairs(options.entities) do

				if entitiesentityString then

					result.entitiesentityString = deepCopy(

						entitiesentityString].data

					)

					table.insert(result'entity-ids'], entityString)

				end

			end

			result'entity-ids' = removeDuplicates(result'entity-ids'])

			table.sort(result'entity-ids'], function(s1, s2)

				return entityIndexess1 < entityIndexess2

			end)

		end

	end



	-- Add any missing reason fields from entities.

	for id, entityData in pairs(result.entities) do

		entityData.reason = entityData.reason or 'political'

	end



	-- Wrap the result in an outer layer like the MediaWiki Action API does.

	result = {sensitiveips = result}



	if options.format == 'json' then

		-- Load jf-JSON

		JSON = JSON or require('Module:jf-JSON')

		JSON.strictTypes = true -- Necessary for correct blank-object encoding

		-- Decode a skeleton result JSON string. This ensures that blank objects

		-- are re-encoded as blank objects and not as blank arrays.

		local jsonResult = JSON:decode([[{"sensitiveips": {

			"matches": [],

			"matched-ranges": {},

			"entities": {},

			"entity-ids": []

		}}]])

		for i, key in ipairs{'matches', 'matched-ranges', 'entities', 'entity-ids'} do

			deepCopyInto(result.sensitiveipskey], jsonResult.sensitiveipskey])

		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

end



--------------------------------------------------------------------------------

-- Exports

--------------------------------------------------------------------------------



local p = {}



function p._isValidSensitivityReason(s)

	-- Return true if s is a valid sensitivity reason; otherwise return false.

	return s ~= nil and SensitiveEntity.reasonss ~= nil

end



function p._getSensitivityReasons(separator, conjunction)

	-- Return an string of valid sensitivity reasons, ordered alphabetically.

	-- The reasons are separated by an optional separator; if conjunction is

	-- 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

		reasons#reasons + 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.args1

		conjunction = frame.args2

	end



	-- Return a formatted string

	return mw.text.listToText(reasons, separator, conjunction)

end



-- Export the API query function

p.query = query



return p
Permanently protected module
From Wikipedia, the free encyclopedia


-- This module provides functions for handling sensitive IP addresses.



-- Load modules

local mIP = require('Module:IP')

local IPAddress = mIP.IPAddress

local Subnet = mIP.Subnet

local IPv4Collection = mIP.IPv4Collection

local IPv6Collection = mIP.IPv6Collection



-- Lazily load the jf-JSON module

local JSON



-------------------------------------------------------------------------------

-- Helper functions

-------------------------------------------------------------------------------



local function deepCopy(val)

	-- Make a deep copy of a value, but don't worry about self-references or

	-- metatables as mw.clone does. If a table in val has a self-reference,

	-- you will get an infinite loop, so don't do that.

	if type(val) == 'table' then

		local ret = {}

		for k, v in pairs(val) do

			retk = deepCopy(v)

		end

		return ret

	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

			destk = {}

			deepCopyInto(v, destk])

		else

			destk = v

		end

	end

end



local function removeDuplicates(t)

	-- Return a copy of an array with duplicate values removed.

	local keys, ret = {}, {}

	for i, v in ipairs(t) do

		if not keysv then

			table.insert(ret, v)

			keysv = true

		end

	end

	return ret

end



-------------------------------------------------------------------------------

-- SensitiveEntity class

-- A country or organization for which blocks must be handled with care.

-- Media organizations may inspect block messages for IP addresses and ranges

-- belonging to these entities and those messages may end up in the press.

-------------------------------------------------------------------------------



local SensitiveEntity = {}

SensitiveEntity.__index = SensitiveEntity



SensitiveEntity.reasons = {

	-- The reasons that an entity may be sensitive. Used to verify data in

	-- Module:Sensitive IP addresses/list.

	political = true,

	technical = true,

}



do

	-- Private methods

	local function addRanges(self, key, collectionConstructor, ranges)

		if ranges and ranges1 then

			selfkey = collectionConstructor()

			for i, range in ipairs(ranges) do

				selfkey]:addSubnet(Subnet.new(range))

			end

		end

	end



	-- Constructor

	function SensitiveEntity.new(data)

		local self = setmetatable({}, SensitiveEntity)



		-- Set data

		self.data = data

		addRanges(self, 'v4Collection', IPv4Collection.new, data.ipv4Ranges)

		addRanges(self, 'v6Collection', IPv6Collection.new, data.ipv6Ranges)



		return self

	end

end



function SensitiveEntity:matchesIPOrRange(str)

	-- Returns true, matchObj, queryObj if there is a match for the IP address

	-- string or CIDR range str in the sensitive entity. Returns false

	-- otherwise. matchObj is the Subnet object that was matched, and queryObj

	-- is the IPAddress or Subnet object corresponding to the input string.



	-- Get the IPAddress or Subnet object for str

	local isIP, isSubnet, obj

	isIP, obj = pcall(IPAddress.new, str)

	if isIP and not obj then

		isIP = false

	end



	if not isIP then

		isSubnet, obj = pcall(Subnet.new, str)

		if not isSubnet or not obj then

			error(string.format(

				"'%s' is not a valid IP address or CIDR string",

				str

			), 2)

		end

	end



	-- Try matching the object to the appropriate collection

	local function isInCollection(collection, obj, isIP)

		if isIP then

			if collection then

				local isMatch, matchObj = collection:containsIP(obj)

				return isMatch, matchObj, obj

			else

				return false

			end

		else

			if collection then

				local isMatch, matchObj = collection:overlapsSubnet(obj)

				return isMatch, matchObj, obj

			else

				return false

			end

		end

	end



	if obj:isIPv4() then

		return isInCollection(self.v4Collection, obj, isIP)

	else

		return isInCollection(self.v6Collection, obj, isIP)

	end

end



-------------------------------------------------------------------------------

-- Sensitive IP API

-------------------------------------------------------------------------------



-- This API is used by external tools and gadgets, so it should be kept

-- backwards-compatible. Clients query the API with a query table, and the

-- API returns a response table. The response table is available as a Lua table

-- for other Lua modules, and as JSON for external clients.



-- Example query tables:

--

-- Query IP addresses and ranges:

-- {

-- 	test = {'1.2.3.4', '4.5.6.0/24', '2001:db8::ff00:12:3456', '2001:db8::ff00:12:0/112'},

-- }

--

-- Query specific entities:

-- {

-- 	entities = {'ussenate', 'ushr'}

-- }

--

-- Query all entities:

-- {

-- 	entities = {'all'}

-- }

--

-- Query all entities and format the result as a JSON string:

-- {

-- 	entities = {'all'},

--  format = 'json'

-- }

--

-- Combined query:

-- {

-- 	test = {'1.2.3.4', '4.5.6.0/24', '2001:db8::ff00:12:3456', '2001:db8::ff00:12:0/112'},

-- 	entities = {'ussenate', 'ushr'}

-- }



-- Example response:

--

-- {

--     sensitiveips = {

--         matches = {

--             {

--                 ip = '1.2.3.4',

--                 type = 'ip',

--                 ['ip-version'] = 'IPv4',

--                 ['matches-range'] = '1.2.3.0/24',

--                 ['entity-id'] = 'entityid'

--             },

--             {

--                 range = '4.5.6.0/24',

--                 type = 'range',

--                 ['ip-version'] = 'IPv4',

--                 ['matches-range'] = '4.5.0.0/16',

--                 ['entity-id'] = 'entityid'

--             }

--         },

--         ['matched-ranges'] = {

--             ['1.2.3.0/24'] = {

--                 range = '1.2.3.0/24',

--                 ['ip-version'] = 'IPv4',

--                 ['entity-id'] = 'entityid'

--             },

--             ['4.5.0.0/16'] = {

--                 range = '4.5.0.0/16',

--                 ['ip-version'] = 'IPv4',

--                 ['entity-id'] = 'entityid'

--             }

--         },

--         entities = {

--             ['entityid'] = {

--                 id = 'entityid',

--                 name = 'The entity name',

--                 description = 'A description of the entity',

--                 ['ipv4-ranges'] = {

--                     '1.2.3.0/24',

--                     '4.5.0.0/16'

--                     '6.7.0.0/16'

--                 },

--                 ['ipv6-ranges'] = {

--                     '2001:db8::ff00:12:0/112'

--                 },

--                 notes = 'Notes about the entity or its ranges'

--             }

--         }

--         ['entity-ids'] = {

--             'entityid'

--         }

--     }

-- }

--

-- Response with errors:

--

-- {

--     error = {

--         code = 'example-error',

--         info = 'There was an error',

--         ['*'] = 'See /info/en/?search=Module:Sensitive_IP_addresses for API usage'

--     }

-- }



local function query(options)

	-- Make entity objects

	local entities, entityIndexes = {}, {}

	local data = mw.loadData('Module:Sensitive IP addresses/list')

	for i, entityData in ipairs(data) do

		entitiesentityData.id = SensitiveEntity.new(entityData)

		entityIndexesentityData.id = i -- Keep track of the original order

	end



	local function makeError(code, info, format)

		local ret = {['error' = {

			code = code,

			info = info,

			'*' = 'See /info/en/?search=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

		return makeError(

			'sipa-options-type-error',

			string.format(

				"type error in argument #1 of 'query' (expected table, received %s)",

				type(options)

			)

		)

	elseif not options.test and not options.entities then

		return makeError(

			'sipa-blank-options',

			"the options table didn't contain a 'test' or an 'entities' key",

			options.format

		)

	end



	if options.test then

		if type(options.test) ~= 'table' then

			return makeError(

				'sipa-test-type-error',

				string.format(

					"'test' options key was type %s (expected table)",

					type(options.test)

				),

				options.format

			)

		end



		for i, testString in ipairs(options.test) do

			if type(testString) ~= 'string' then

				return makeError(

					'sipa-test-string-type-error',

					string.format(

						"type error in item #%d in the 'test' array (expected string, received %s)",

						i,

						type(testString)

					),

					options.format

				)

			end



			for k, entity in pairs(entities) do

				-- Try to match the range with the current sensitive entity.

				local success, isMatch, matchObj, queryObj = pcall(

					entity.matchesIPOrRange,

					entity,

					testString

				)

				if not success then

					-- The string was invalid.

					return makeError(

						'sipa-invalid-test-string',

						string.format(

							"test string #%d '%s' was not a valid IP address or CIDR string",

							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

					if isIP then

						match.type = 'ip'

						match.ip = tostring(queryObj)

					else

						match.type = 'range'

						match.range = tostring(queryObj)

					end

					match'ip-version' = queryObj:getVersion()

					match'matches-range' = matchObj:getCIDR()

					match'entity-id' = entity.data.id

					table.insert(result.matches, match)



					-- Add the matched range data.

					result'matched-ranges'][match'matches-range']] = {

						range = match'matches-range'],

						'ip-version' = match'ip-version'],

						'entity-id' = match'entity-id'],

					}



					-- Add the entity data for the entity we matched.

					result.entitiesmatch'entity-id']] = deepCopy(

						entitiesmatch'entity-id']].data

					)



					-- Add the entity ID for the entity we matched.

					table.insert(result'entity-ids'], match'entity-id'])

				end

			end

		end

	end



	-- Add entity data requested explicitly.

	if options.entities then

		if type(options.entities) ~= 'table' then

			return makeError(

				'sipa-entities-type-error',

				string.format(

					"'entities' options key was type %s (expected table)",

					type(options.test)

				),

				options.format

			)

		end



		-- Check the type of all the entity strings, and check if 'all' has

		-- been specified.

		local isAll = false

		for i, entityString in ipairs(options.entities) do

			if type(entityString) ~= 'string' then

				return makeError(

					'sipa-entity-string-type-error',

					string.format(

						"type error in item #%d in the 'entities' array (expected string, received %s)",

						i,

						type(entityString)

					),

					options.format

				)

			end

			if entityString == 'all' then

				isAll = true

			end

		end



		if isAll then

			-- Add all the entity data.

			-- As the final result will contain all the entity data, we can

			-- just create the entities and entity-ids subtables from scratch

			-- without worrying about what any existing values might be.

			result.entities = {}

			result'entity-ids' = {}

			for i, entityData in ipairs(data) do

				result.entitiesentityData.id = deepCopy(entityData)

				result'entity-ids'][i = entityData.id

			end

		else

			-- Add data for the entities specified.

			-- Insert the entity and entity-id subtables if they aren't already

			-- present.

			for i, entityString in ipairs(options.entities) do

				if entitiesentityString then

					result.entitiesentityString = deepCopy(

						entitiesentityString].data

					)

					table.insert(result'entity-ids'], entityString)

				end

			end

			result'entity-ids' = removeDuplicates(result'entity-ids'])

			table.sort(result'entity-ids'], function(s1, s2)

				return entityIndexess1 < entityIndexess2

			end)

		end

	end



	-- Add any missing reason fields from entities.

	for id, entityData in pairs(result.entities) do

		entityData.reason = entityData.reason or 'political'

	end



	-- Wrap the result in an outer layer like the MediaWiki Action API does.

	result = {sensitiveips = result}



	if options.format == 'json' then

		-- Load jf-JSON

		JSON = JSON or require('Module:jf-JSON')

		JSON.strictTypes = true -- Necessary for correct blank-object encoding

		-- Decode a skeleton result JSON string. This ensures that blank objects

		-- are re-encoded as blank objects and not as blank arrays.

		local jsonResult = JSON:decode([[{"sensitiveips": {

			"matches": [],

			"matched-ranges": {},

			"entities": {},

			"entity-ids": []

		}}]])

		for i, key in ipairs{'matches', 'matched-ranges', 'entities', 'entity-ids'} do

			deepCopyInto(result.sensitiveipskey], jsonResult.sensitiveipskey])

		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

end



--------------------------------------------------------------------------------

-- Exports

--------------------------------------------------------------------------------



local p = {}



function p._isValidSensitivityReason(s)

	-- Return true if s is a valid sensitivity reason; otherwise return false.

	return s ~= nil and SensitiveEntity.reasonss ~= nil

end



function p._getSensitivityReasons(separator, conjunction)

	-- Return an string of valid sensitivity reasons, ordered alphabetically.

	-- The reasons are separated by an optional separator; if conjunction is

	-- 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

		reasons#reasons + 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.args1

		conjunction = frame.args2

	end



	-- Return a formatted string

	return mw.text.listToText(reasons, separator, conjunction)

end



-- Export the API query function

p.query = query



return p

Videos

Youtube | Vimeo | Bing

Websites

Google | Yahoo | Bing

Encyclopedia

Google | Yahoo | Bing

Facebook