Module:Protection banner
-- This module implements {{pp-meta}} and its daughter templates such as
 -- {{pp-dispute}}, {{pp-vandalism}} and {{pp-sock}}.
 
 -- Initialise necessary modules.
 require('strict')
 local makeFileLink = require('Module:File link')._main
 local effectiveProtectionLevel = require('Module:Effective protection level')._main
 local effectiveProtectionExpiry = require('Module:Effective protection expiry')._main
 local yesno = require('Module:Yesno')
 
 -- Lazily initialise modules and objects we don't always need.
 local getArgs, makeMessageBox, lang
 
 -- Set constants.
 local CONFIG_MODULE = 'Module:Protection banner/config'
 
 --------------------------------------------------------------------------------
 -- Helper functions
 --------------------------------------------------------------------------------
 
 local function makeCategoryLink(cat, sort)
 	if cat then
 		return string.format(
 			'[[%s:%s|%s]]',
 			mw.site.namespaces[14].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 -&gt; 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 &quot;first&quot; key
 		if k == nil then
 			return retval
 		end
 		toWalk[k] = nil
 		retval[k] = true
 		for _,v in ipairs(graph[k]) do
 			if not retval[v] then
 				toWalk[v] = 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.supportedActions[args.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 args[1] then
 		obj.reason = mw.ustring.lower(args[1])
 		if obj.reason:find('|') then
 			error('reasons cannot contain the pipe character (&quot;|&quot;)', 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.banners[obj.action] then
 			configTables[#configTables + 1] = cfg.banners[obj.action][obj.reason]
 		end
 		if cfg.defaultBanners[obj.action] then
 			configTables[#configTables + 1] = cfg.defaultBanners[obj.action][obj.level]
 			configTables[#configTables + 1] = cfg.defaultBanners[obj.action].default
 		end
 		configTables[#configTables + 1] = cfg.masterBanner
 		for i, field in ipairs(Protection.bannerConfigFields) do
 			for j, t in ipairs(configTables) do
 				if t[field] then
 					obj.bannerConfig[field] = t[field]
 					break
 				end
 			end
 		end
 	end
 	return setmetatable(obj, Protection)
 end
 
 function Protection:isUserScript()
 	-- Whether the page is a user JavaScript or CSS page.
 	local title = self.title
 	return title.namespace == 2 and (
 		title.contentModel == 'javascript' or title.contentModel == 'css'
 	)
 end
 
 function Protection:isProtected()
 	return self.level ~= '*'
 end
 
 function Protection:shouldShowLock()
 	-- Whether we should output a banner/padlock
 	return self:isProtected() and not self:isUserScript()
 end
 
 -- Whether this page needs a protection category.
 Protection.shouldHaveProtectionCategory = Protection.shouldShowLock
 
 function Protection:isTemporary()
 	return type(self.expiry) == 'number'
 end
 
 function Protection:makeProtectionCategory()
 	if not self:shouldHaveProtectionCategory() then
 		return ''
 	end
 
 	local cfg = self._cfg
 	local title = self.title
 	
 	-- 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.categoryNamespaceKeys[title.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.reasonsWithNamespacePriority[self.reason] and 2 or 3))
  
 	--[[
 	-- Define the attempt order. Inactive subtables (subtables with nil &quot;value&quot;
 	-- fields) are moved to the end, where they will later be given the key
 	-- &quot;all&quot;. 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
 	-- &quot;all-all-all-all-all&quot;.
 	--
 	-- 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 &quot;all&quot;, 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 &quot;all&quot;.
 	--
 	-- 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 &gt; noActive then
 				key[t.keypos] = 'all'
 			else
 				local quotient = i / 2 ^ (j - 1)
 				quotient = math.ceil(quotient)
 				if quotient % 2 == 1 then
 					key[t.keypos] = t.val
 				else
 					key[t.keypos] = 'all'
 				end
 			end
 		end
 		key = table.concat(key, '|')
 		local attempt = cats[key]
 		if attempt then
 			return makeCategoryLink(attempt, title.text)
 		end
 	end
 	return ''
 end
 
 function Protection:isIncorrect()
 	local expiry = self.expiry
 	return not self:shouldHaveProtectionCategory()
 		or type(expiry) == 'number' and expiry &lt; os.time()
 end
 
 function Protection:isTemplateProtectedNonTemplate()
 	local action, namespace = self.action, self.title.namespace
 	return self.level == 'templateeditor'
 		and (
 			(action ~= 'edit' and action ~= 'move')
 			or (namespace ~= 10 and namespace ~= 828)
 		)
 end
 
 function Protection:makeCategoryLinks()
 	local msg = self._cfg.msg
 	local ret = {self:makeProtectionCategory()}
 	if self:isIncorrect() then
 		ret[#ret + 1] = makeCategoryLink(
 			msg['tracking-category-incorrect'],
 			self.title.text
 		)
 	end
 	if self:isTemplateProtectedNonTemplate() then
 		ret[#ret + 1] = makeCategoryLink(
 			msg['tracking-category-template'],
 			self.title.text
 		)
 	end
 	return table.concat(ret)
 end
 
 --------------------------------------------------------------------------------
 -- Blurb class
 --------------------------------------------------------------------------------
 
 local Blurb = {}
 Blurb.__index = Blurb
 
 Blurb.bannerTextFields = {
 	text = true,
 	explanation = true,
 	tooltip = true,
 	alt = true,
 	link = true
 }
 
 function Blurb.new(protectionObj, args, cfg)
 	return setmetatable({
 		_cfg = cfg,
 		_protectionObj = protectionObj,
 		_args = args
 	}, Blurb)
 end
 
 -- Private methods --
 
 function Blurb:_formatDate(num)
 	-- Formats a Unix timestamp into dd Month, YYYY format.
 	lang = lang or mw.language.getContentLanguage()
 	local success, date = pcall(
 		lang.formatDate,
 		lang,
 		self._cfg.msg['expiry-date-format'] or 'j F Y',
 		'@' .. tostring(num)
 	)
 	if success then
 		return date
 	end
 end
 
 function Blurb:_getExpandedMessage(msgKey)
 	return self:_substituteParameters(self._cfg.msg[msgKey])
 end
 
 function Blurb:_substituteParameters(msg)
 	if not self._params then
 		local parameterFuncs = {}
 
 		parameterFuncs.CURRENTVERSION     = self._makeCurrentVersionParameter
 		parameterFuncs.EDITREQUEST        = self._makeEditRequestParameter
 		parameterFuncs.EXPIRY             = self._makeExpiryParameter
 		parameterFuncs.EXPLANATIONBLURB   = self._makeExplanationBlurbParameter
 		parameterFuncs.IMAGELINK          = self._makeImageLinkParameter
 		parameterFuncs.INTROBLURB         = self._makeIntroBlurbParameter
 		parameterFuncs.INTROFRAGMENT      = self._makeIntroFragmentParameter
 		parameterFuncs.PAGETYPE           = self._makePagetypeParameter
 		parameterFuncs.PROTECTIONBLURB    = self._makeProtectionBlurbParameter
 		parameterFuncs.PROTECTIONDATE     = self._makeProtectionDateParameter
 		parameterFuncs.PROTECTIONLEVEL    = self._makeProtectionLevelParameter
 		parameterFuncs.PROTECTIONLOG      = self._makeProtectionLogParameter
 		parameterFuncs.TALKPAGE           = self._makeTalkPageParameter
 		parameterFuncs.TOOLTIPBLURB       = self._makeTooltipBlurbParameter
 		parameterFuncs.TOOLTIPFRAGMENT    = self._makeTooltipFragmentParameter
 		parameterFuncs.VANDAL             = self._makeVandalTemplateParameter
 		
 		self._params = setmetatable({}, {
 			__index = function (t, k)
 				local param
 				if parameterFuncs[k] then
 					param = parameterFuncs[k](self)
 				end
 				param = param or ''
 				t[k] = param
 				return param
 			end
 		})
 	end
 	
 	msg = msg:gsub('${(%u+)}', self._params)
 	return msg
 end
 
 function Blurb:_makeCurrentVersionParameter()
 	-- A link to the page history or the move log, depending on the kind of
 	-- protection.
 	local pagename = self._protectionObj.title.prefixedText
 	if self._protectionObj.action == 'move' then
 		-- We need the move log link.
 		return makeFullUrl(
 			'Special:Log',
 			{type = 'move', page = pagename},
 			self:_getExpandedMessage('current-version-move-display')
 		)
 	else
 		-- We need the history link.
 		return makeFullUrl(
 			pagename,
 			{action = 'history'},
 			self:_getExpandedMessage('current-version-edit-display')
 		)
 	end
 end
 
 function Blurb:_makeEditRequestParameter()
 	local mEditRequest = require('Module:Submit an edit request')
 	local action = self._protectionObj.action
 	local level = self._protectionObj.level
 	
 	-- Get the edit request type.
 	local requestType
 	if action == 'edit' then
 		if level == 'autoconfirmed' then
 			requestType = 'semi'
 		elseif level == 'extendedconfirmed' then
 			requestType = 'extended'
 		elseif level == 'templateeditor' then
 			requestType = 'template'
 		end
 	end
 	requestType = requestType or 'full'
 	
 	-- Get the display value.
 	local display = self:_getExpandedMessage('edit-request-display')
 
 	return mEditRequest._link{type = requestType, display = display}
 end
 
 function Blurb:_makeExpiryParameter()
 	local expiry = self._protectionObj.expiry
 	if type(expiry) == 'number' then
 		return self:_formatDate(expiry)
 	else
 		return expiry
 	end
 end
 
 function Blurb:_makeExplanationBlurbParameter()
 	-- Cover special cases first.
 	if self._protectionObj.title.namespace == 8 then
 		-- MediaWiki namespace
 		return self:_getExpandedMessage('explanation-blurb-nounprotect')
 	end
 
 	-- Get explanation blurb table keys
 	local action = self._protectionObj.action
 	local level = self._protectionObj.level
 	local talkKey = self._protectionObj.title.isTalkPage and 'talk' or 'subject'
 
 	-- Find the message in the explanation blurb table and substitute any
 	-- parameters.
 	local explanations = self._cfg.explanationBlurbs
 	local msg
 	if explanations[action][level] and explanations[action][level][talkKey] then
 		msg = explanations[action][level][talkKey]
 	elseif explanations[action][level] and explanations[action][level].default then
 		msg = explanations[action][level].default
 	elseif explanations[action].default and explanations[action].default[talkKey] then
 		msg = explanations[action].default[talkKey]
 	elseif explanations[action].default and explanations[action].default.default then
 		msg = explanations[action].default.default
 	else
 		error(string.format(
 			'could not find explanation blurb for action &quot;%s&quot;, level &quot;%s&quot; and talk key &quot;%s&quot;',
 			action,
 			level,
 			talkKey
 		), 8)
 	end
 	return self:_substituteParameters(msg)
 end
 
 function Blurb:_makeImageLinkParameter()
 	local imageLinks = self._cfg.imageLinks
 	local action = self._protectionObj.action
 	local level = self._protectionObj.level
 	local msg
 	if imageLinks[action][level] then
 		msg = imageLinks[action][level]
 	elseif imageLinks[action].default then
 		msg = imageLinks[action].default
 	else
 		msg = imageLinks.edit.default
 	end
 	return self:_substituteParameters(msg)
 end
 
 function Blurb:_makeIntroBlurbParameter()
 	if self._protectionObj:isTemporary() then
 		return self:_getExpandedMessage('intro-blurb-expiry')
 	else
 		return self:_getExpandedMessage('intro-blurb-noexpiry')
 	end
 end
 
 function Blurb:_makeIntroFragmentParameter()
 	if self._protectionObj:isTemporary() then
 		return self:_getExpandedMessage('intro-fragment-expiry')
 	else
 		return self:_getExpandedMessage('intro-fragment-noexpiry')
 	end
 end
 
 function Blurb:_makePagetypeParameter()
 	local pagetypes = self._cfg.pagetypes
 	return pagetypes[self._protectionObj.title.namespace]
 		or pagetypes.default
 		or error('no default pagetype defined', 8)
 end
 
 function Blurb:_makeProtectionBlurbParameter()
 	local protectionBlurbs = self._cfg.protectionBlurbs
 	local action = self._protectionObj.action
 	local level = self._protectionObj.level
 	local msg
 	if protectionBlurbs[action][level] then
 		msg = protectionBlurbs[action][level]
 	elseif protectionBlurbs[action].default then
 		msg = protectionBlurbs[action].default
 	elseif protectionBlurbs.edit.default then
 		msg = protectionBlurbs.edit.default
 	else
 		error('no protection blurb defined for protectionBlurbs.edit.default', 8)
 	end
 	return self:_substituteParameters(msg)
 end
 
 function Blurb:_makeProtectionDateParameter()
 	local protectionDate = self._protectionObj.protectionDate
 	if type(protectionDate) == 'number' then
 		return self:_formatDate(protectionDate)
 	else
 		return protectionDate
 	end
 end
 
 function Blurb:_makeProtectionLevelParameter()
 	local protectionLevels = self._cfg.protectionLevels
 	local action = self._protectionObj.action
 	local level = self._protectionObj.level
 	local msg
 	if protectionLevels[action][level] then
 		msg = protectionLevels[action][level]
 	elseif protectionLevels[action].default then
 		msg = protectionLevels[action].default
 	elseif protectionLevels.edit.default then
 		msg = protectionLevels.edit.default
 	else
 		error('no protection level defined for protectionLevels.edit.default', 8)
 	end
 	return self:_substituteParameters(msg)
 end
 
 function Blurb:_makeProtectionLogParameter()
 	local pagename = self._protectionObj.title.prefixedText
 	if self._protectionObj.action == 'autoreview' then
 		-- We need the pending changes log.
 		return makeFullUrl(
 			'Special:Log',
 			{type = 'stable', page = pagename},
 			self:_getExpandedMessage('pc-log-display')
 		)
 	else
 		-- We need the protection log.
 		return makeFullUrl(
 			'Special:Log',
 			{type = 'protect', page = pagename},
 			self:_getExpandedMessage('protection-log-display')
 		)
 	end
 end
 
 function Blurb:_makeTalkPageParameter()
 	return string.format(
 		'[[%s:%s#%s|%s]]',
 		mw.site.namespaces[self._protectionObj.title.namespace].talk.name,
 		self._protectionObj.title.text,
 		self._args.section or 'top',
 		self:_getExpandedMessage('talk-page-link-display')
 	)
 end
 
 function Blurb:_makeTooltipBlurbParameter()
 	if self._protectionObj:isTemporary() then
 		return self:_getExpandedMessage('tooltip-blurb-expiry')
 	else
 		return self:_getExpandedMessage('tooltip-blurb-noexpiry')
 	end
 end
 
 function Blurb:_makeTooltipFragmentParameter()
 	if self._protectionObj:isTemporary() then
 		return self:_getExpandedMessage('tooltip-fragment-expiry')
 	else
 		return self:_getExpandedMessage('tooltip-fragment-noexpiry')
 	end
 end
 
 function Blurb:_makeVandalTemplateParameter()
 	return mw.getCurrentFrame():expandTemplate{
 		title=&quot;vandal-m&quot;,
 		args={self._args.user or self._protectionObj.title.baseText}
 	}
 end
 
 -- Public methods --
 
 function Blurb:makeBannerText(key)
 	-- Validate input.
 	if not key or not Blurb.bannerTextFields[key] then
 		error(string.format(
 			'&quot;%s&quot; is not a valid banner config field',
 			tostring(key)
 		), 2)
 	end
 
 	-- Generate the text.
 	local msg = self._protectionObj.bannerConfig[key]
 	if type(msg) == 'string' then
 		return self:_substituteParameters(msg)
 	elseif type(msg) == 'function' then
 		msg = msg(self._protectionObj, self._args)
 		if type(msg) ~= 'string' then
 			error(string.format(
 				'bad output from banner config function with key &quot;%s&quot;'
 					.. ' (expected string, got %s)',
 				tostring(key),
 				type(msg)
 			), 4)
 		end
 		return self:_substituteParameters(msg)
 	end
 end
 
 --------------------------------------------------------------------------------
 -- BannerTemplate class
 --------------------------------------------------------------------------------
 
 local BannerTemplate = {}
 BannerTemplate.__index = BannerTemplate
 
 function BannerTemplate.new(protectionObj, cfg)
 	local obj = {}
 	obj._cfg = cfg
 
 	-- Set the image filename.
 	local imageFilename = protectionObj.bannerConfig.image
 	if imageFilename then
 		obj._imageFilename = imageFilename
 	else
 		-- If an image filename isn't specified explicitly in the banner config,
 		-- generate it from the protection status and the namespace.
 		local action = protectionObj.action
 		local level = protectionObj.level
 		local namespace = protectionObj.title.namespace
 		local reason = protectionObj.reason
 		
 		-- Deal with special cases first.
 		if (
 			namespace == 10
 			or namespace == 828
 			or reason and obj._cfg.indefImageReasons[reason]
 			)
 			and action == 'edit'
 			and level == 'sysop'
 			and not protectionObj:isTemporary()
 		then
 			-- Fully protected modules and templates get the special red &quot;indef&quot;
 			-- padlock.
 			obj._imageFilename = obj._cfg.msg['image-filename-indef']
 		else
 			-- Deal with regular protection types.
 			local images = obj._cfg.images
 			if images[action] then
 				if images[action][level] then
 					obj._imageFilename = images[action][level]
 				elseif images[action].default then
 					obj._imageFilename = images[action].default
 				end
 			end
 		end
 	end
 	return setmetatable(obj, BannerTemplate)
 end
 
 function BannerTemplate:renderImage()
 	local filename = self._imageFilename
 		or self._cfg.msg['image-filename-default']
 		or 'Transparent.gif'
 	return makeFileLink{
 		file = filename,
 		size = (self.imageWidth or 20) .. 'px',
 		alt = self._imageAlt,
 		link = self._imageLink,
 		caption = self.imageCaption
 	}
 end
 
 --------------------------------------------------------------------------------
 -- Banner class
 --------------------------------------------------------------------------------
 
 local Banner = setmetatable({}, BannerTemplate)
 Banner.__index = Banner
 
 function Banner.new(protectionObj, blurbObj, cfg)
 	local obj = BannerTemplate.new(protectionObj, cfg) -- This doesn't need the blurb.
 	obj.imageWidth = 40
 	obj.imageCaption = blurbObj:makeBannerText('alt') -- Large banners use the alt text for the tooltip.
 	obj._reasonText = blurbObj:makeBannerText('text')
 	obj._explanationText = blurbObj:makeBannerText('explanation')
 	obj._page = protectionObj.title.prefixedText -- Only makes a difference in testing.
 	return setmetatable(obj, Banner)
 end
 
 function Banner:__tostring()
 	-- Renders the banner.
 	makeMessageBox = makeMessageBox or require('Module:Message box').main
 	local reasonText = self._reasonText or error('no reason text set', 2)
 	local explanationText = self._explanationText
 	local mbargs = {
 		page = self._page,
 		type = 'protection',
 		image = self:renderImage(),
 		text = string.format(
 			&quot;'''%s'''%s&quot;,
 			reasonText,
 			explanationText and '&lt;br /&gt;' .. 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.padlockIndicatorNames[protectionObj.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 not yesno(args.catonly) and (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:shouldShowLock() 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.wrappers[parent:getTitle():gsub('/sandbox$', '')]
 
 	-- Find user args, and use the parent frame if we are being called from a
 	-- wrapper template.
 	getArgs = getArgs or require('Module: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
 		args[k] = v
 	end
 	for k, v in pairs(userArgs) do
 		args[k] = v
 	end
 	return p._main(args, cfg)
 end