มอดูล:Protection banner

-- This module implements  and its daughter templates such as
-- ,  and .
-- Initialise necessary modules.
require('มอดูล:No globals')
local makeFileLink = require('มอดูล:File link')._main
local effectiveProtectionLevel = require('มอดูล:Effective protection level')._main
local effectiveProtectionExpiry = require('มอดูล:Effective protection expiry')._main
local yesno = require('มอดูล:Yesno')
-- Lazily initialise modules and objects we don't always need.
local getArgs, makeMessageBox, lang
-- Set constants.
local CONFIG_MODULE = 'มอดูล:Protection banner/config'
--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
local function makeCategoryLink(cat, sort)
	if cat then
		return string.format(
			'%s',
			mw.site.namespaces14.name,
			cat,
			sort
		)
	end
end
-- Validation function for the expiry and the protection date
local function validateDate(dateString, dateType)
	if not lang then
		lang = mw.language.getContentLanguage()
	end
	local success, result = pcall(lang.formatDate, lang, 'U', dateString)
	if success then
		result = tonumber(result)
		if result then
			return result
		end
	end
	error(string.format(
		'invalid %s: %s',
		dateType,
		tostring(dateString)
	), 4)
end
local function makeFullUrl(page, query, display)
	return string.format(
		'%s %s',
		tostring(mw.uri.fullUrl(page, query)),
		display
	)
end
-- Given a directed graph formatted as node -> table of direct successors,
-- get a table of all nodes reachable from a given node (though always
-- including the given node).
local function getReachableNodes(graph, start)
	local toWalk, retval = {start = true}, {}
	while true do
		-- Can't use pairs() since we're adding and removing things as we're iterating
		local k = next(toWalk) -- This always gets the "first" key
		if k == nil then
			return retval
		end
		toWalkk = nil
		retvalk = true
		for _,v in ipairs(graphk) do
			if not retvalv then
				toWalkv = true
			end
		end
	end
end
--------------------------------------------------------------------------------
-- Protection class
--------------------------------------------------------------------------------
local Protection = {}
Protection.__index = Protection
Protection.supportedActions = {
	edit = true,
	move = true,
	autoreview = true,
	upload = true
}
Protection.bannerConfigFields = {
	'text',
	'explanation',
	'tooltip',
	'alt',
	'link',
	'image'
}
function Protection.new(args, cfg, title)
	local obj = {}
	obj._cfg = cfg
	obj.title = title or mw.title.getCurrentTitle()
	-- Set action
	if not args.action then
		obj.action = 'edit'
	elseif Protection.supportedActionsargs.action then
		obj.action = args.action
	else
		error(string.format(
			'invalid action: %s',
			tostring(args.action)
		), 3)
	end
	-- Set level
	obj.level = args.demolevel or effectiveProtectionLevel(obj.action, obj.title)
	if not obj.level or (obj.action == 'move' and obj.level == 'autoconfirmed') then
		-- Users need to be autoconfirmed to move pages anyway, so treat
		-- semi-move-protected pages as unprotected.
		obj.level = '*'
	end
	-- Set expiry
	local effectiveExpiry = effectiveProtectionExpiry(obj.action, obj.title)
	if effectiveExpiry == 'infinity' then
		obj.expiry = 'indef'
	elseif effectiveExpiry ~= 'unknown' then
		obj.expiry = validateDate(effectiveExpiry, 'expiry date')
	end
	-- Set reason
	if args1 then
		obj.reason = mw.ustring.lower(args1)
		if obj.reason:find('|') then
			error('reasons cannot contain the pipe character ("|")', 3)
		end
	end
	-- Set protection date
	if args.date then
		obj.protectionDate = validateDate(args.date, 'protection date')
	end
	
	-- Set banner config
	do
		obj.bannerConfig = {}
		local configTables = {}
		if cfg.bannersobj.action then
			configTables#configTables + 1 = cfg.bannersobj.actionobj.reason
		end
		if cfg.defaultBannersobj.action then
			configTables#configTables + 1 = cfg.defaultBannersobj.actionobj.level
			configTables#configTables + 1 = cfg.defaultBannersobj.action.default
		end
		configTables#configTables + 1 = cfg.masterBanner
		for i, field in ipairs(Protection.bannerConfigFields) do
			for j, t in ipairs(configTables) do
				if tfield then
					obj.bannerConfigfield = tfield
					break
				end
			end
		end
	end
	return setmetatable(obj, Protection)
end
function Protection:isProtected()
	return self.level ~= '*'
end
function Protection:isTemporary()
	return type(self.expiry) == 'number'
end
function Protection:makeProtectionCategory()
	local cfg = self._cfg
	local title = self.title
	
	-- Exit if the page is not protected.
	if not self:isProtected() then
		return 
	end
	
	-- Get the expiry key fragment.
	local expiryFragment
	if self.expiry == 'indef' then
		expiryFragment = self.expiry
	elseif type(self.expiry) == 'number' then
		expiryFragment = 'temp'
	end
	-- Get the namespace key fragment.
	local namespaceFragment = cfg.categoryNamespaceKeystitle.namespace
	if not namespaceFragment and title.namespace % 2 == 1 then
			namespaceFragment = 'talk'
	end

	-- Define the order that key fragments are tested in. This is done with an
	-- array of tables containing the value to be tested, along with its
	-- position in the cfg.protectionCategories table.
	local order = {
		{val = expiryFragment,    keypos = 1},
		{val = namespaceFragment, keypos = 2},
		{val = self.reason,       keypos = 3},
		{val = self.level,        keypos = 4},
		{val = self.action,       keypos = 5}
	}
	--
	-- The old protection templates used an ad-hoc protection category system,
	-- with some templates prioritising namespaces in their categories, and
	-- others prioritising the protection reason. To emulate this in this module
	-- we use the config table cfg.reasonsWithNamespacePriority to set the
	-- reasons for which namespaces have priority over protection reason.
	-- If we are dealing with one of those reasons, move the namespace table to
	-- the end of the order table, i.e. give it highest priority. If not, the
	-- reason should have highest priority, so move that to the end of the table
	-- instead.
	--
	table.insert(order, table.remove(order, self.reason and cfg.reasonsWithNamespacePriorityself.reason and 2 or 3))

	--
	-- Define the attempt order. Inactive subtables (subtables with nil "value"
	-- fields) are moved to the end, where they will later be given the key
	-- "all". This is to cut down on the number of table lookups in
	-- cfg.protectionCategories, which grows exponentially with the number of
	-- non-nil keys. We keep track of the number of active subtables with the
	-- noActive parameter.
	--
	local noActive, attemptOrder
	do
		local active, inactive = {}, {}
		for i, t in ipairs(order) do
			if t.val then
				active#active + 1 = t
			else
				inactive#inactive + 1 = t
			end
		end
		noActive = #active
		attemptOrder = active
		for i, t in ipairs(inactive) do
			attemptOrder#attemptOrder + 1 = t
		end
	end

	--
	-- Check increasingly generic key combinations until we find a match. If a
	-- specific category exists for the combination of key fragments we are
	-- given, that match will be found first. If not, we keep trying different
	-- key fragment combinations until we match using the key
	-- "all-all-all-all-all".
	--
	-- To generate the keys, we index the key subtables using a binary matrix
	-- with indexes i and j. j is only calculated up to the number of active
	-- subtables. For example, if there were three active subtables, the matrix
	-- would look like this, with 0 corresponding to the key fragment "all", and
	-- 1 corresponding to other key fragments.
	-- 
	--   j 1  2  3
	-- i  
	-- 1   1  1  1
	-- 2   0  1  1
	-- 3   1  0  1
	-- 4   0  0  1
	-- 5   1  1  0
	-- 6   0  1  0
	-- 7   1  0  0
	-- 8   0  0  0
	-- 
	-- Values of j higher than the number of active subtables are set
	-- to the string "all".
	--
	-- A key for cfg.protectionCategories is constructed for each value of i.
	-- The position of the value in the key is determined by the keypos field in
	-- each subtable.
	--
	local cats = cfg.protectionCategories
	for i = 1, 2^noActive do
		local key = {}
		for j, t in ipairs(attemptOrder) do
			if j > noActive then
				keyt.keypos = 'all'
			else
				local quotient = i / 2 ^ (j - 1)
				quotient = math.ceil(quotient)
				if quotient % 2 == 1 then
					keyt.keypos = t.val
				else
					keyt.keypos = 'all'
				end
			end
		end
		key = table.concat(key, '|')
		local attempt = catskey
		if attempt then
			return makeCategoryLink(attempt, title.text)
		end
	end
	return 
end
function Protection:isIncorrect()
	local expiry = self.expiry
	return not self:isProtected()
		or type(expiry) == 'number' and expiry ' .. explanationText or 
		)
	}
	return makeMessageBox('mbox', mbargs)
end
--------------------------------------------------------------------------------
-- Padlock class
--------------------------------------------------------------------------------
local Padlock = setmetatable({}, BannerTemplate)
Padlock.__index = Padlock
function Padlock.new(protectionObj, blurbObj, cfg)
	local obj = BannerTemplate.new(protectionObj, cfg) -- This doesn't need the blurb.
	obj.imageWidth = 20
	obj.imageCaption = blurbObj:makeBannerText('tooltip')
	obj._imageAlt = blurbObj:makeBannerText('alt')
	obj._imageLink = blurbObj:makeBannerText('link')
	obj._indicatorName = cfg.padlockIndicatorNamesprotectionObj.action
		or cfg.padlockIndicatorNames.default
		or 'pp-default'
	return setmetatable(obj, Padlock)
end
function Padlock:__tostring()
	local frame = mw.getCurrentFrame()
	-- The nowiki tag helps prevent whitespace at the top of articles.
	return frame:extensionTag{name = 'nowiki'} .. frame:extensionTag{
		name = 'indicator',
		args = {name = self._indicatorName},
		content = self:renderImage()
	}
end
--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------
local p = {}
function p._exportClasses()
	-- This is used for testing purposes.
	return {
		Protection = Protection,
		Blurb = Blurb,
		BannerTemplate = BannerTemplate,
		Banner = Banner,
		Padlock = Padlock,
	}
end
function p._main(args, cfg, title)
	args = args or {}
	cfg = cfg or require(CONFIG_MODULE)
	local protectionObj = Protection.new(args, cfg, title)
	local ret = {}
	-- If a page's edit protection is equally or more restrictive than its
	-- protection from some other action, then don't bother displaying anything
	-- for the other action (except categories).
	if protectionObj.action == 'edit' or
		args.demolevel or
		not getReachableNodes(
			cfg.hierarchy,
			protectionObj.level
		)effectiveProtectionLevel('edit', protectionObj.title)
	then
		-- Initialise the blurb object
		local blurbObj = Blurb.new(protectionObj, args, cfg)
	
		-- Render the banner
		if protectionObj:isProtected() then
			ret#ret + 1 = tostring(
				(yesno(args.small) and Padlock or Banner)
				.new(protectionObj, blurbObj, cfg)
			)
		end
	end
	-- Render the categories
	if yesno(args.category) ~= false then
		ret#ret + 1 = protectionObj:makeCategoryLinks()
	end
	
	return table.concat(ret)	
end
function p.main(frame, cfg)
	cfg = cfg or require(CONFIG_MODULE)
	-- Find default args, if any.
	local parent = frame.getParent and frame:getParent()
	local defaultArgs = parent and cfg.wrappersparent:getTitle():gsub('/sandbox$', )
	-- Find user args, and use the parent frame if we are being called from a
	-- wrapper template.
	getArgs = getArgs or require('มอดูล:Arguments').getArgs
	local userArgs = getArgs(frame, {
		parentOnly = defaultArgs,
		frameOnly = not defaultArgs
	})
	-- Build the args table. User-specified args overwrite default args.
	local args = {}
	for k, v in pairs(defaultArgs or {}) do
		argsk = v
	end
	for k, v in pairs(userArgs) do
		argsk = v
	end
	return p._main(args, cfg)
end
return p