Difference between revisions of "Module:Protection banner"

From MINR.ORG WIKI
(get rid of the config class while still allowing replacing it for testing)
((edit conflict) replace Protection getter functions with public attributes)
Line 10: Line 10:
  
 
-- Lazily initialise modules and objects we don't always need.
 
-- Lazily initialise modules and objects we don't always need.
local mArguments, mMessageBox, lang, config
+
local mArguments, mMessageBox, lang
 +
 
 +
--------------------------------------------------------------------------------
 +
-- Config class
 +
--------------------------------------------------------------------------------
 +
 
 +
local Config = class('Config')
 +
 
 +
function Config:initialize(data)
 +
data = data or mw.loadData('Module:Protection banner/config')
 +
self._cfg = data.cfg
 +
self._msg = data.msg
 +
self._bannerConfigTables = {}
 +
end
 +
 
 +
function Config:getBannerConfig(protectionObj)
 +
if self._bannerConfigTables[protectionObj] then
 +
return self._bannerConfigTables[protectionObj]
 +
else
 +
local ret = {}
 +
local cfg = self._cfg
 +
local action = protectionObj.action
 +
local level = protectionObj.level
 +
local reason = protectionObj.reason
 +
local fields = {
 +
'text',
 +
'explanation',
 +
'tooltip',
 +
'alt',
 +
'link',
 +
'image'
 +
}
 +
local configTables = {}
 +
if cfg.banners[action] then
 +
configTables[#configTables + 1] = cfg.banners[action][reason]
 +
end
 +
if cfg.defaultBanners[action] then
 +
configTables[#configTables + 1] = cfg.defaultBanners[action][level]
 +
configTables[#configTables + 1] = cfg.defaultBanners[action].default
 +
end
 +
configTables[#configTables + 1] = cfg.masterBanner
 +
for i, field in ipairs(fields) do
 +
for j, t in ipairs(configTables) do
 +
if t[field] then
 +
ret[field] = t[field]
 +
break
 +
end
 +
end
 +
end
 +
self._bannerConfigTables[protectionObj] = ret
 +
return ret
 +
end
 +
end
 +
 
 +
function Config:getConfigTable(key)
 +
local blacklist = {
 +
banners = true,
 +
defaultBanners = true,
 +
masterBanner = true
 +
}
 +
if not blacklist[key] then
 +
return self._cfg[key]
 +
else
 +
return nil
 +
end
 +
end
 +
 
 +
function Config:getMessage(key)
 +
return self._msg[key]
 +
end
  
 
--------------------------------------------------------------------------------
 
--------------------------------------------------------------------------------
Line 28: Line 97:
 
}
 
}
 
if args.action and actions[args.action] then
 
if args.action and actions[args.action] then
self._action = args.action
+
self.action = args.action
 
else
 
else
self._action = 'edit'
+
self.action = 'edit'
 
end
 
end
 
end
 
end
Line 36: Line 105:
 
-- Set level
 
-- Set level
 
do
 
do
local level = effectiveProtectionLevel(self._action, titleObj)
+
local level = effectiveProtectionLevel(self.action, titleObj)
 
if level == 'accountcreator' then
 
if level == 'accountcreator' then
 
-- Lump titleblacklisted pages in with template-protected pages,
 
-- Lump titleblacklisted pages in with template-protected pages,
Line 42: Line 111:
 
level = 'templateeditor'
 
level = 'templateeditor'
 
end
 
end
if self._action == 'move' and level == 'autoconfirmed' then
+
if self.action == 'move' and level == 'autoconfirmed' then
 
-- Users need to be autoconfirmed to move pages anyway, so treat
 
-- Users need to be autoconfirmed to move pages anyway, so treat
 
-- semi-move-protected pages as unprotected.
 
-- semi-move-protected pages as unprotected.
 
level = '*'
 
level = '*'
 
end
 
end
self._level = level or '*'
+
self.level = level or '*'
 
end
 
end
  
Line 68: Line 137:
 
-- Set expiry
 
-- Set expiry
 
if args.expiry then
 
if args.expiry then
local indefStrings = configObj.cfg.indefStrings
+
local indefStrings = configObj:getConfigTable('indefStrings')
 
if indefStrings[args.expiry] then
 
if indefStrings[args.expiry] then
self._expiry = 'indef'
+
self.expiry = 'indef'
 
elseif type(args.expiry) == 'number' then
 
elseif type(args.expiry) == 'number' then
self._expiry = args.expiry
+
self.expiry = args.expiry
 
else
 
else
self._expiry = validateDate(args.expiry, 'expiry date')
+
self.expiry = validateDate(args.expiry, 'expiry date')
 
end
 
end
 
end
 
end
Line 82: Line 151:
 
local reason = args.reason or args[1]
 
local reason = args.reason or args[1]
 
if reason then
 
if reason then
self._reason = reason:lower()
+
self.reason = reason:lower()
 
end
 
end
 
end
 
end
  
 
-- Set protection date
 
-- Set protection date
self._protectionDate = validateDate(args.date, 'protection date')
+
self.protectionDate = validateDate(args.date, 'protection date')
 
-- Set banner config
 
do
 
self.bannerConfig = {}
 
local cfg = configObj.cfg
 
local fields = {
 
'text',
 
'explanation',
 
'tooltip',
 
'alt',
 
'link',
 
'image'
 
}
 
local configTables = {}
 
if cfg.banners[self._action] then
 
configTables[#configTables + 1] = cfg.banners[self._action][self._reason]
 
end
 
if cfg.defaultBanners[self._action] then
 
configTables[#configTables + 1] = cfg.defaultBanners[self._action][self._level]
 
configTables[#configTables + 1] = cfg.defaultBanners[self._action].default
 
end
 
configTables[#configTables + 1] = cfg.masterBanner
 
for i, field in ipairs(fields) do
 
for j, t in ipairs(configTables) do
 
if t[field] then
 
self.bannerConfig[field] = t[field]
 
break
 
end
 
end
 
end
 
end
 
end
 
 
 
function Protection:getAction()
 
return self._action
 
end
 
 
 
function Protection:getLevel()
 
return self._level
 
 
end
 
end
  
 
function Protection:isProtected()
 
function Protection:isProtected()
 
return self._level ~= '*'
 
return self._level ~= '*'
end
 
 
function Protection:getReason()
 
return self._reason
 
end
 
 
function Protection:getExpiry()
 
return self._expiry
 
end
 
 
function Protection:getProtectionDate()
 
return self._protectionDate
 
 
end
 
end
  
Line 154: Line 172:
 
self._configObj = configObj
 
self._configObj = configObj
 
self._protectionObj = protectionObj
 
self._protectionObj = protectionObj
self._bannerConfig = protectionObj.bannerConfig
+
self._bannerConfig = configObj:getBannerConfig(protectionObj)
 
self._titleObj = titleObj
 
self._titleObj = titleObj
 
end
 
end
Line 183: Line 201:
  
 
function Blurb:_getExpandedMessage(msg)
 
function Blurb:_getExpandedMessage(msg)
local msg = self._configObj.msg[msg]
+
local msg = self._configObj:getMessage(msg)
 
return self:_substituteParameters(msg)
 
return self:_substituteParameters(msg)
 
end
 
end
Line 232: Line 250:
 
-- A link to the page history or the move log, depending on the kind of
 
-- A link to the page history or the move log, depending on the kind of
 
-- protection.
 
-- protection.
local action = self._protectionObj:getAction()
+
local action = self._protectionObj.action
 
local pagename = self._titleObj.prefixedText
 
local pagename = self._titleObj.prefixedText
 
if action == 'move' then
 
if action == 'move' then
Line 260: Line 278:
  
 
function Blurb:_makeDisputeBlurbParameter()
 
function Blurb:_makeDisputeBlurbParameter()
local expiry = self._protectionObj:getExpiry()
+
local expiry = self._protectionObj.expiry
 
if type(expiry) == 'number' then
 
if type(expiry) == 'number' then
 
return self:_getExpandedMessage('dispute-blurb-expiry')
 
return self:_getExpandedMessage('dispute-blurb-expiry')
Line 287: Line 305:
 
function Blurb:_makeEditRequestParameter()
 
function Blurb:_makeEditRequestParameter()
 
local mEditRequest = require('Module:Submit an edit request')
 
local mEditRequest = require('Module:Submit an edit request')
local action = self._protectionObj:getAction()
+
local action = self._protectionObj.action
local level = self._protectionObj:getLevel()
+
local level = self._protectionObj.level
 
 
 
-- Get the display message key.
 
-- Get the display message key.
Line 314: Line 332:
  
 
function Blurb:_makeExpiryParameter()
 
function Blurb:_makeExpiryParameter()
local expiry = self._protectionObj:getExpiry()
+
local expiry = self._protectionObj.expiry
 
if expiry == 'indef' then
 
if expiry == 'indef' then
 
return nil
 
return nil
Line 326: Line 344:
  
 
function Blurb:_makeExplanationBlurbParameter()
 
function Blurb:_makeExplanationBlurbParameter()
local action = self._protectionObj:getAction()
+
local action = self._protectionObj.action
local level = self._protectionObj:getLevel()
+
local level = self._protectionObj.level
 
local namespace = self._titleObj.namespace
 
local namespace = self._titleObj.namespace
 
local isTalk = self._titleObj.isTalkPage
 
local isTalk = self._titleObj.isTalkPage
Line 358: Line 376:
  
 
function Blurb:_makeImageLinkParameter()
 
function Blurb:_makeImageLinkParameter()
local imageLinks = self._configObj.cfg.imageLinks
+
local imageLinks = self._configObj:getConfigTable('imageLinks')
local action = self._protectionObj:getAction()
+
local action = self._protectionObj.action
local level = self._protectionObj:getLevel()
+
local level = self._protectionObj.level
 
local msg
 
local msg
 
if imageLinks[action][level] then
 
if imageLinks[action][level] then
Line 373: Line 391:
  
 
function Blurb:_makeIntroBlurbParameter()
 
function Blurb:_makeIntroBlurbParameter()
local expiry = self._protectionObj:getExpiry()
+
local expiry = self._protectionObj.expiry
 
if type(expiry) == 'number' then
 
if type(expiry) == 'number' then
 
return self:_getExpandedMessage('intro-blurb-expiry')
 
return self:_getExpandedMessage('intro-blurb-expiry')
Line 382: Line 400:
  
 
function Blurb:_makeOfficeBlurbParameter()
 
function Blurb:_makeOfficeBlurbParameter()
local protectionDate = self._protectionObj:getProtectionDate()
+
local protectionDate = self._protectionObj.protectionDate
 
if protectionDate then
 
if protectionDate then
 
return self:_getExpandedMessage('office-blurb-protectiondate')
 
return self:_getExpandedMessage('office-blurb-protectiondate')
Line 391: Line 409:
  
 
function Blurb:_makePagetypeParameter()
 
function Blurb:_makePagetypeParameter()
local pagetypes = self._configObj.cfg.pagetypes
+
local pagetypes = self._configObj:getConfigTable('pagetypes')
 
local namespace = self._titleObj.namespace
 
local namespace = self._titleObj.namespace
 
return pagetypes[namespace] or pagetypes.default or error('no default pagetype defined')
 
return pagetypes[namespace] or pagetypes.default or error('no default pagetype defined')
Line 397: Line 415:
  
 
function Blurb:_makeProtectionBlurbParameter()
 
function Blurb:_makeProtectionBlurbParameter()
local protectionBlurbs = self._configObj.cfg.protectionBlurbs
+
local protectionBlurbs = self._configObj:getConfigTable('protectionBlurbs')
local action = self._protectionObj:getAction()
+
local action = self._protectionObj.action
local level = self._protectionObj:getLevel()
+
local level = self._protectionObj.level
 
local msg
 
local msg
 
if protectionBlurbs[action][level] then
 
if protectionBlurbs[action][level] then
Line 414: Line 432:
  
 
function Blurb:_makeProtectionDateParameter()
 
function Blurb:_makeProtectionDateParameter()
local protectionDate = self._protectionObj:getProtectionDate()
+
local protectionDate = self._protectionObj.protectionDate
 
if type(protectionDate) == 'number' then
 
if type(protectionDate) == 'number' then
 
return Blurb.formatDate(protectionDate)
 
return Blurb.formatDate(protectionDate)
Line 423: Line 441:
  
 
function Blurb:_makeProtectionLevelParameter()
 
function Blurb:_makeProtectionLevelParameter()
local protectionLevels = self._configObj.cfg.protectionLevels
+
local protectionLevels = self._configObj:getConfigTable('protectionLevels')
local action = self._protectionObj:getAction()
+
local action = self._protectionObj.action
local level = self._protectionObj:getLevel()
+
local level = self._protectionObj.level
 
local msg
 
local msg
 
if protectionLevels[action][level] then
 
if protectionLevels[action][level] then
Line 440: Line 458:
  
 
function Blurb:_makeProtectionLogParameter()
 
function Blurb:_makeProtectionLogParameter()
local action = self._protectionObj:getAction()
+
local action = self._protectionObj.action
 
local pagename = self._titleObj.prefixedText
 
local pagename = self._titleObj.prefixedText
 
if action == 'autoreview' then
 
if action == 'autoreview' then
Line 460: Line 478:
  
 
function Blurb:_makeResetBlurbParameter()
 
function Blurb:_makeResetBlurbParameter()
local protectionDate = self._protectionObj:getProtectionDate()
+
local protectionDate = self._protectionObj.protectionDate
 
if protectionDate then
 
if protectionDate then
 
return self:_getExpandedMessage('reset-blurb-protectiondate')
 
return self:_getExpandedMessage('reset-blurb-protectiondate')
Line 481: Line 499:
  
 
function Blurb:_makeTooltipBlurbParameter()
 
function Blurb:_makeTooltipBlurbParameter()
local expiry = self._protectionObj:getExpiry()
+
local expiry = self._protectionObj.expiry
 
if type(expiry) == 'number' then
 
if type(expiry) == 'number' then
 
return self:_getExpandedMessage('tooltip-blurb-expiry')
 
return self:_getExpandedMessage('tooltip-blurb-expiry')
Line 553: Line 571:
 
end
 
end
  
local action = protectionObj:getAction()
+
local action = protectionObj.action
local level = protectionObj:getLevel()
+
local level = protectionObj.level
local expiry = protectionObj:getExpiry()
+
local expiry = protectionObj.expiry
 
local namespace = titleObj.namespace
 
local namespace = titleObj.namespace
 
 
Line 566: Line 584:
 
-- Fully protected modules and templates get the special red "indef"
 
-- Fully protected modules and templates get the special red "indef"
 
-- padlock.
 
-- padlock.
self._imageFilename = self._configObj.msg['image-filename-indef']
+
self._imageFilename = self._configObj:getMessage('image-filename-indef')
 
return nil
 
return nil
 
end
 
end
  
 
-- Deal with regular protection types.
 
-- Deal with regular protection types.
local images = self._configObj.cfg.images
+
local images = self._configObj:getConfigTable('images')
 
if images[action] then
 
if images[action] then
 
if images[action][level] then
 
if images[action][level] then
Line 595: Line 613:
 
function BannerTemplate:renderImage()
 
function BannerTemplate:renderImage()
 
local filename = self._imageFilename
 
local filename = self._imageFilename
or self._configObj.msg['image-filename-default']
+
or self._configObj:getMessage('image-filename-default')
 
or 'Transparent.gif'
 
or 'Transparent.gif'
 
return newFileLink(filename)
 
return newFileLink(filename)
Line 729: Line 747:
 
 
 
-- Get the expiry.
 
-- Get the expiry.
local expiry = protectionObj:getExpiry()
+
local expiry = protectionObj.expiry
 
if type(expiry) == 'number' then
 
if type(expiry) == 'number' then
 
expiry = 'temp'
 
expiry = 'temp'
Line 740: Line 758:
 
do
 
do
 
local namespace = titleObj.namespace
 
local namespace = titleObj.namespace
local categoryNamespaces = configObj.cfg.categoryNamespaceKeys
+
local categoryNamespaces = configObj:getConfigTable('categoryNamespaceKeys')
 
nskey = categoryNamespaces[namespace]
 
nskey = categoryNamespaces[namespace]
 
if not nskey and namespace % 2 == 1 then
 
if not nskey and namespace % 2 == 1 then
Line 748: Line 766:
  
 
-- Get the other inputs.
 
-- Get the other inputs.
local reason = protectionObj:getReason()
+
local reason = protectionObj.reason
local action = protectionObj:getAction()
+
local action = protectionObj.action
local level = protectionObj:getLevel()
+
local level = protectionObj.level
 
   
 
   
 
--[[
 
--[[
Line 775: Line 793:
 
local configOrder = {}
 
local configOrder = {}
 
do
 
do
local reasonsWithNamespacePriority = configObj.cfg.reasonsWithNamespacePriority
+
local reasonsWithNamespacePriority = configObj:getConfigTable('reasonsWithNamespacePriority')
 
local namespaceFirst = reason and reasonsWithNamespacePriority[reason] or false
 
local namespaceFirst = reason and reasonsWithNamespacePriority[reason] or false
 
for propertiesKey, t in pairs(properties) do
 
for propertiesKey, t in pairs(properties) do
Line 844: Line 862:
 
-- pos field in the property table.
 
-- pos field in the property table.
 
--]]
 
--]]
local cats = configObj.cfg.protectionCategories
+
local cats = configObj:getConfigTable('protectionCategories')
 
local cat
 
local cat
 
for i = 1, 2^noActive do
 
for i = 1, 2^noActive do
Line 881: Line 899:
  
 
function ExpiryCategory:render()
 
function ExpiryCategory:render()
local configObj = self._configObj
+
local reasonsWithoutExpiryCheck = self._configObj:getConfigTable('reasonsWithoutExpiryCheck')
local protectionObj = self._protectionObj
+
local expiryCheckActions = self._configObj:getConfigTable('expiryCheckActions')
+
local expiry = self._protectionObj.expiry
local reasonsWithoutExpiryCheck = configObj.cfg.reasonsWithoutExpiryCheck
+
local action = self._protectionObj.action
local expiryCheckActions = configObj.cfg.expiryCheckActions
+
local reason = self._protectionObj.reason
local expiry = protectionObj:getExpiry()
 
local action = protectionObj:getAction()
 
local reason = protectionObj:getReason()
 
 
 
 
if not expiry
 
if not expiry
Line 895: Line 910:
 
and not reasonsWithoutExpiryCheck[reason]
 
and not reasonsWithoutExpiryCheck[reason]
 
then
 
then
self:setName(configObj.msg['tracking-category-expiry'])
+
self:setName(configObj:getMessage('tracking-category-expiry'))
 
end
 
end
 
return Category.render(self)
 
return Category.render(self)
Line 909: Line 924:
 
local configObj = self._configObj
 
local configObj = self._configObj
 
local protectionObj = self._protectionObj
 
local protectionObj = self._protectionObj
+
local expiry = protectionObj.expiry
local expiry = protectionObj:getExpiry()
+
local action = protectionObj.action
local action = protectionObj:getAction()
+
local level = protectionObj.level
local level = protectionObj:getLevel()
 
  
 
if not protectionObj:isProtected()
 
if not protectionObj:isProtected()
 
or type(expiry) == 'number' and expiry < os.time()
 
or type(expiry) == 'number' and expiry < os.time()
 
then
 
then
self:setName(configObj.msg['tracking-category-incorrect'])
+
self:setName(configObj:getMessage('tracking-category-incorrect'))
 
end
 
end
 
return Category.render(self)
 
return Category.render(self)
Line 935: Line 949:
 
function TemplateCategory:render()
 
function TemplateCategory:render()
 
local configObj = self._configObj
 
local configObj = self._configObj
local protectionObj = self._protectionObj
+
local action = self._protectionObj.action
local titleObj = self._titleObj
+
local level = self._protectionObj.level
+
local namespace = self._titleObj.namespace
local action = protectionObj:getAction()
 
local level = protectionObj:getLevel()
 
local namespace = titleObj.namespace
 
 
 
 
if level == 'templateeditor'
 
if level == 'templateeditor'
Line 948: Line 959:
 
)
 
)
 
then
 
then
self:setName(configObj.msg['tracking-category-template'])
+
self:setName(configObj:getMessage('tracking-category-template'))
 
end
 
end
 
return Category.render(self)
 
return Category.render(self)
Line 969: Line 980:
  
 
-- Get data objects
 
-- Get data objects
if not config then
+
local configObj = Config:new()
config = mw.loadData('Module:Protection banner/config')
 
end
 
local configObj = config
 
 
local protectionObj = Protection:new(args, configObj, titleObj)
 
local protectionObj = Protection:new(args, configObj, titleObj)
  
Line 995: Line 1,003:
  
 
-- Set the image fields
 
-- Set the image fields
local bannerConfig = protectionObj.bannerConfig
+
local bannerConfig = configObj:getBannerConfig(protectionObj)
 
bannerObj:setImageFilename(bannerConfig.image, protectionObj, titleObj)
 
bannerObj:setImageFilename(bannerConfig.image, protectionObj, titleObj)
 
if isPadlock then
 
if isPadlock then
Line 1,035: Line 1,043:
 
return {
 
return {
 
Protection = Protection,
 
Protection = Protection,
 +
Config = Config,
 
Blurb = Blurb,
 
Blurb = Blurb,
 
BannerTemplate = BannerTemplate,
 
BannerTemplate = BannerTemplate,

Revision as of 03:47, 28 June 2014

Documentation for this module may be created at Module:Protection banner/doc

-- This module implements {{pp-meta}} and its daughter templates such as
-- {{pp-dispute}}, {{pp-vandalism}} and {{pp-sock}}.

-- Initialise necessary modules.
require('Module:No globals')
local class = require('Module:Middleclass').class
local newFileLink = require('Module:File link').new
local effectiveProtectionLevel = require('Module:Effective protection level')._main
local yesno = require('Module:Yesno')

-- Lazily initialise modules and objects we don't always need.
local mArguments, mMessageBox, lang

--------------------------------------------------------------------------------
-- Config class
--------------------------------------------------------------------------------

local Config = class('Config')

function Config:initialize(data)
	data = data or mw.loadData('Module:Protection banner/config')
	self._cfg = data.cfg
	self._msg = data.msg
	self._bannerConfigTables = {}
end

function Config:getBannerConfig(protectionObj)
	if self._bannerConfigTables[protectionObj] then
		return self._bannerConfigTables[protectionObj]
	else
		local ret = {}
		local cfg = self._cfg
		local action = protectionObj.action
		local level = protectionObj.level
		local reason = protectionObj.reason
		local fields = {
			'text',
			'explanation',
			'tooltip',
			'alt',
			'link',
			'image'
		}
		local configTables = {}
		if cfg.banners[action] then
			configTables[#configTables + 1] = cfg.banners[action][reason]
		end
		if cfg.defaultBanners[action] then
			configTables[#configTables + 1] = cfg.defaultBanners[action][level]
			configTables[#configTables + 1] = cfg.defaultBanners[action].default
		end
		configTables[#configTables + 1] = cfg.masterBanner
		for i, field in ipairs(fields) do
			for j, t in ipairs(configTables) do
				if t[field] then
					ret[field] = t[field]
					break
				end
			end
		end
		self._bannerConfigTables[protectionObj] = ret
		return ret
	end
end

function Config:getConfigTable(key)
	local blacklist = {
		banners = true,
		defaultBanners = true,
		masterBanner = true
	}
	if not blacklist[key] then
		return self._cfg[key]
	else
		return nil
	end
end

function Config:getMessage(key)
	return self._msg[key]
end

--------------------------------------------------------------------------------
-- Protection class
--------------------------------------------------------------------------------

local Protection = class('Protection')

function Protection:initialize(args, configObj, titleObj)
	-- Set action
	do
		local actions = {
			create = true,
			edit = true,
			move = true,
			autoreview = true
		}
		if args.action and actions[args.action] then
			self.action = args.action
		else
			self.action = 'edit'
		end
	end

	-- Set level
	do
		local level = effectiveProtectionLevel(self.action, titleObj)
		if level == 'accountcreator' then
			-- Lump titleblacklisted pages in with template-protected pages,
			-- since templateeditors can do both.
			level = 'templateeditor'
		end
		if self.action == 'move' and level == 'autoconfirmed' then
			-- Users need to be autoconfirmed to move pages anyway, so treat
			-- semi-move-protected pages as unprotected.
			level = '*'
		end
		self.level = level or '*'
	end

	-- Validation function for the expiry and the protection date
	local function validateDate(date, dateType)
		lang = lang or mw.language.getContentLanguage()
		local success, expiry = pcall(lang.formatDate, lang, 'U', args.expiry)
		expiry = tonumber(expiry)
		if success and expiry then
			return expiry
		else
			return string.format(
				'<strong class="error">Error: invalid %s ("%s")</strong>',
				dateType,
				tostring(args.expiry)
			)
		end
	end

	-- Set expiry
	if args.expiry then
		local indefStrings = configObj:getConfigTable('indefStrings')
		if indefStrings[args.expiry] then
			self.expiry = 'indef'
		elseif type(args.expiry) == 'number' then
			self.expiry = args.expiry
		else
			self.expiry = validateDate(args.expiry, 'expiry date')
		end
	end

	-- Set reason
	do
		local reason = args.reason or args[1]
		if reason then
			self.reason = reason:lower()
		end
	end

	-- Set protection date
	self.protectionDate = validateDate(args.date, 'protection date')
end

function Protection:isProtected()
	return self._level ~= '*'
end

--------------------------------------------------------------------------------
-- Blurb class
--------------------------------------------------------------------------------

local Blurb = class('Blurb')

function Blurb:initialize(configObj, protectionObj, titleObj)
	self._configObj = configObj
	self._protectionObj = protectionObj
	self._bannerConfig = configObj:getBannerConfig(protectionObj)
	self._titleObj = titleObj
end

-- Static methods --

function Blurb.makeFullUrl(page, query, display)
	local url = mw.uri.fullUrl(page, query)
	url = tostring(url)
	return string.format('[%s %s]', url, display)
end

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,
		'j F Y',
		'@' .. tostring(num)
	)
	if success then
		return date
	end
end

-- Private methods --

function Blurb:_getExpandedMessage(msg)
	local msg = self._configObj:getMessage(msg)
	return self:_substituteParameters(msg)
end

function Blurb:_substituteParameters(msg)
	if not self._params then
		local params, parameterFuncs = {}, {}
		setmetatable(params, {
			__index = function (t, k)
				local param
				if parameterFuncs[k] then
					param = parameterFuncs[k](self)
				end
				param = param or ''
				params[k] = param
				return param
			end
		})

		parameterFuncs.CURRENTVERSION     = self._makeCurrentVersionParameter
		parameterFuncs.DELETIONDISCUSSION = self._makeDeletionDiscussionParameter
		parameterFuncs.DISPUTEBLURB       = self._makeDisputeBlurbParameter
		parameterFuncs.DISPUTESECTION     = self._makeDisputeSectionParameter
		parameterFuncs.EDITREQUEST        = self._makeEditRequestParameter
		parameterFuncs.EXPIRY             = self._makeExpiryParameter
		parameterFuncs.EXPLANATIONBLURB   = self._makeExplanationBlurbParameter
		parameterFuncs.IMAGELINK          = self._makeImageLinkParameter
		parameterFuncs.INTROBLURB         = self._makeIntroBlurbParameter
		parameterFuncs.OFFICEBLURB        = self._makeOfficeBlurbParameter
		parameterFuncs.PAGETYPE           = self._makePagetypeParameter
		parameterFuncs.PROTECTIONBLURB    = self._makeProtectionBlurbParameter
		parameterFuncs.PROTECTIONDATE     = self._makeProtectionDateParameter
		parameterFuncs.PROTECTIONLEVEL    = self._makeProtectionLevelParameter
		parameterFuncs.PROTECTIONLOG      = self._makeProtectionLogParameter
		parameterFuncs.RESETBLURB         = self._makeResetBlurbParameter
		parameterFuncs.TALKPAGE           = self._makeTalkPageParameter
		parameterFuncs.TOOLTIPBLURB       = self._makeTooltipBlurbParameter
		parameterFuncs.VANDAL             = self._makeVandalTemplateParameter
		
		self._params = params
	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 action = self._protectionObj.action
	local pagename = self._titleObj.prefixedText
	if action == 'move' then
		-- We need the move log link.
		return self.makeFullUrl(
			'Special:Log',
			{type = 'move', page = pagename},
			self:_getExpandedMessage('current-version-move-display')
		)
	else
		-- We need the history link.
		return self.makeFullUrl(
			pagename,
			{action = 'history'},
			self:_getExpandedMessage('current-version-edit-display')
		)
	end
end

function Blurb:_makeDeletionDiscussionLinkParameter()
	local deletionDiscussionPage = self._deletionDiscussionPage
	if deletionDiscussionPage then
		local display = self:_getExpandedMessage('deletion-discussion-link-display')
		return string.format('[[%s|%s]]', deletionDiscussionPage, display)
	end
end

function Blurb:_makeDisputeBlurbParameter()
	local expiry = self._protectionObj.expiry
	if type(expiry) == 'number' then
		return self:_getExpandedMessage('dispute-blurb-expiry')
	else
		return self:_getExpandedMessage('dispute-blurb-noexpiry')
	end
end

function Blurb:_makeDisputeSectionParameter()
	-- "disputes", with or without a section link
	local section = self._section
	local disputes = self:_getExpandedMessage('dispute-section-link-display')
	if section then
		return string.format(
			'[[%s:%s#%s|%s]]',
			mw.site.namespaces[self._titleObj.namespace].talk.name,
			self._titleObj.text,
			section,
			disputes
		)
	else
		return disputes
	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 display message key.
	local key
	if action == 'edit' and level == 'autoconfirmed' then
		key = 'edit-request-semi-display'
	else
		key = 'edit-request-full-display'
	end
	local display = self:_getExpandedMessage(key)
	
	-- Get the edit request type.
	local requestType
	if action == 'edit' then
		if level == 'autoconfirmed' then
			requestType = 'semi'
		elseif level == 'templateeditor' then
			requestType = 'template'
		end
	end
	requestType = requestType or 'full'
	
	return mEditRequest.exportLinkToLua{type = requestType, display = display}
end

function Blurb:_makeExpiryParameter()
	local expiry = self._protectionObj.expiry
	if expiry == 'indef' then
		return nil
	elseif type(expiry) == 'number' then
		return Blurb.formatDate(expiry)
	elseif expiry then
		-- Expiry is an error string.
		return expiry
	end
end

function Blurb:_makeExplanationBlurbParameter()
	local action = self._protectionObj.action
	local level = self._protectionObj.level
	local namespace = self._titleObj.namespace
	local isTalk = self._titleObj.isTalkPage

	-- @TODO: add semi-protection and pending changes blurbs
	local key
	if namespace == 8 then
		-- MediaWiki namespace
		key = 'explanation-blurb-full-nounprotect'
	elseif action == 'edit' and level == 'sysop' and not isTalk then
		key = 'explanation-blurb-full-subject'
	elseif action == 'move' then
		if isTalk then
			key = 'explanation-blurb-move-talk'
		else
			key = 'explanation-blurb-move-subject'
		end
	elseif action == 'create' then
		local xfd = self._deletionDiscussion
		if xfd then
			key = 'explanation-blurb-create-xfd'
		else
			key = 'explanation-blurb-create-noxfd'
		end
	else
		key = 'explanation-blurb-default'
	end
	return self:_getExpandedMessage(key)
end

function Blurb:_makeImageLinkParameter()
	local imageLinks = self._configObj:getConfigTable('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()
	local expiry = self._protectionObj.expiry
	if type(expiry) == 'number' then
		return self:_getExpandedMessage('intro-blurb-expiry')
	else
		return self:_getExpandedMessage('intro-blurb-noexpiry')
	end
end

function Blurb:_makeOfficeBlurbParameter()
	local protectionDate = self._protectionObj.protectionDate
	if protectionDate then
		return self:_getExpandedMessage('office-blurb-protectiondate')
	else
		return self:_getExpandedMessage('office-blurb-noprotectiondate')
	end
end

function Blurb:_makePagetypeParameter()
	local pagetypes = self._configObj:getConfigTable('pagetypes')
	local namespace = self._titleObj.namespace
	return pagetypes[namespace] or pagetypes.default or error('no default pagetype defined')
end

function Blurb:_makeProtectionBlurbParameter()
	local protectionBlurbs = self._configObj:getConfigTable('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 cfg.protectionBlurbs.edit.default')
	end
	return self:_substituteParameters(msg)
end

function Blurb:_makeProtectionDateParameter()
	local protectionDate = self._protectionObj.protectionDate
	if type(protectionDate) == 'number' then
		return Blurb.formatDate(protectionDate)
	else
		return protectionDate
	end
end

function Blurb:_makeProtectionLevelParameter()
	local protectionLevels = self._configObj:getConfigTable('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 cfg.protectionLevels.edit.default')
	end
	return self:_substituteParameters(msg)
end

function Blurb:_makeProtectionLogParameter()
	local action = self._protectionObj.action
	local pagename = self._titleObj.prefixedText
	if action == 'autoreview' then
		-- We need the pending changes log.
		return self.makeFullUrl(
			'Special:Log',
			{type = 'stable', page = pagename},
			self:_getExpandedMessage('pc-log-display')
		)
	else
		-- We need the protection log.
		return self.makeFullUrl(
			'Special:Log',
			{type = 'protect', page = pagename},
			self:_getExpandedMessage('protection-log-display')
		)
	end
end

function Blurb:_makeResetBlurbParameter()
	local protectionDate = self._protectionObj.protectionDate
	if protectionDate then
		return self:_getExpandedMessage('reset-blurb-protectiondate')
	else
		return self:_getExpandedMessage('reset-blurb-noprotectiondate')
	end
end

function Blurb:_makeTalkPageParameter()
	local section = self._section
	local display = self:_getExpandedMessage('talk-page-link-display')
	return string.format(
		'[[%s:%s#%s|%s]]',
		mw.site.namespaces[self._titleObj.namespace].talk.name,
		self._titleObj.text,
		section or 'top',
		display
	)
end

function Blurb:_makeTooltipBlurbParameter()
	local expiry = self._protectionObj.expiry
	if type(expiry) == 'number' then
		return self:_getExpandedMessage('tooltip-blurb-expiry')
	else
		return self:_getExpandedMessage('tooltip-blurb-noexpiry')
	end
end

function Blurb:_makeVandalTemplateParameter()
	local mVandalM = require('Module:Vandal-m')
	local username = self._username
	username = username or self._titleObj.baseText
	return mVandalM._main{username}
end

-- Public methods --

function Blurb:setDeletionDiscussionPage(page)
	self._deletionDiscussionPage = page
end

function Blurb:setUsername(username)
	self._username = username
end

function Blurb:setSection(section)
	self._section = section
end

function Blurb:makeReasonText()
	local msg = self._bannerConfig.text
	if msg then
		return self:_substituteParameters(msg)
	end
end

function Blurb:makeExplanationText()
	local msg = self._bannerConfig.explanation
	return self:_substituteParameters(msg)
end

function Blurb:makeTooltipText()
	local msg = self._bannerConfig.tooltip
	return self:_substituteParameters(msg)
end

function Blurb:makeAltText()
	local msg = self._bannerConfig.alt
	return self:_substituteParameters(msg)
end

function Blurb:makeLinkText()
	local msg = self._bannerConfig.link
	return self:_substituteParameters(msg)
end

--------------------------------------------------------------------------------
-- BannerTemplate class
--------------------------------------------------------------------------------

local BannerTemplate = class('BannerTemplate')

function BannerTemplate:initialize(configObj)
	self._configObj = configObj
end

function BannerTemplate:setImageFilename(filename, protectionObj, titleObj)
	if filename then
		self._imageFilename = filename
		return nil
	end

	local action = protectionObj.action
	local level = protectionObj.level
	local expiry = protectionObj.expiry
	local namespace = titleObj.namespace
	
	-- Deal with special cases first.
	if (namespace == 10 or namespace == 828) -- Maybe we don't need the namespace check?
		and action == 'edit'
		and level == 'sysop'
		and not expiry
	then
		-- Fully protected modules and templates get the special red "indef"
		-- padlock.
		self._imageFilename = self._configObj:getMessage('image-filename-indef')
		return nil
	end

	-- Deal with regular protection types.
	local images = self._configObj:getConfigTable('images')
	if images[action] then
		if images[action][level] then
			self._imageFilename = images[action][level]
			return nil
		elseif images[action].default then
			self._imageFilename = images[action].default
			return nil
		end
	end

	return nil
end

function BannerTemplate:setImageWidth(width)
	self._imageWidth = width
end

function BannerTemplate:setImageTooltip(tooltip)
	self._imageCaption = tooltip
end

function BannerTemplate:renderImage()
	local filename = self._imageFilename
		or self._configObj:getMessage('image-filename-default')
		or 'Transparent.gif'
	return newFileLink(filename)
		:width(self._imageWidth or 20)
		:alt(self._imageAlt)
		:link(self._imageLink)
		:caption(self._imageCaption)
		:render()
end

function BannerTemplate:render()
	-- Dummy method, to be implemented by the subclasses.
	return ''
end

--------------------------------------------------------------------------------
-- Banner class
--------------------------------------------------------------------------------

local Banner = BannerTemplate:subclass('Banner')

function Banner:initialize(configObj)
	BannerTemplate.initialize(self, configObj)
	self:setImageWidth(40)
end

function Banner:setReasonText(s)
	self._reasonText = s
end

function Banner:setExplanationText(s)
	self._explanationText = s
end

function Banner:render(page)
	-- Renders the banner.
	-- The page parameter specifies the page to generate the banner for, for
	-- testing purposes.
	mMessageBox = mMessageBox or require('Module:Message box')
	local reasonText = self._reasonText or error('no reason text set')
	local explanationText = self._explanationText
	local mbargs = {
		page = page,
		type = 'protection',
		image = self:renderImage(),
		text = string.format(
			"'''%s'''%s",
			reasonText,
			explanationText and '<br />' .. explanationText or ''
		)
	}
	return mMessageBox.main('mbox', mbargs)
end

--------------------------------------------------------------------------------
-- Padlock class
--------------------------------------------------------------------------------

local Padlock = BannerTemplate:subclass('Padlock')

function Padlock:initialize(configObj)
	BannerTemplate.initialize(self, configObj)
	self:setImageWidth(20)
end

function Padlock:setImageAlt(alt)
	self._imageAlt = alt
end

function Padlock:setImageLink(link)
	self._imageLink = link
end

function Padlock:setRight(px)
	self._right = px
end

function Padlock:render()
	local root = mw.html.create('div')
	root
		:addClass('metadata topicon nopopups')
		:attr('id', 'protected-icon')
		:css{display = 'none', right = self._right or '55px'}
		:wikitext(self:renderImage())
	return tostring(root)
end

--------------------------------------------------------------------------------
-- Category class
--------------------------------------------------------------------------------

local Category = class('Category')

function Category:initialize(configObj, protectionObj)
	self._configObj = configObj
	self._protectionObj = protectionObj
end

function Category:setName(name)
	self._name = name
end

function Category:render()
	if self._name then
		return string.format(
			'[[%s:%s]]',
			mw.site.namespaces[14].name,
			self._name
		)
	end
end

--------------------------------------------------------------------------------
-- ProtectionCategory class
--------------------------------------------------------------------------------

local ProtectionCategory = Category:subclass('ProtectionCategory')

function ProtectionCategory:initialize(configObj, protectionObj, titleObj)
	Category.initialize(self, configObj, protectionObj)
	self._titleObj = titleObj
end

function ProtectionCategory:render()
	local configObj = self._configObj
	local protectionObj = self._protectionObj
	local titleObj = self._titleObj
	
	-- Get the level and exit if the page is not protected.
	if not protectionObj:isProtected() then
		return ''
	end
	
	-- Get the expiry.
	local expiry = protectionObj.expiry
	if type(expiry) == 'number' then
		expiry = 'temp'
	elseif expiry ~= 'indef' then
		expiry = nil
	end

	-- Get the namespace category key.
	local nskey
	do
		local namespace = titleObj.namespace
		local categoryNamespaces = configObj:getConfigTable('categoryNamespaceKeys')
		nskey = categoryNamespaces[namespace]
		if not nskey and namespace % 2 == 1 then
				nskey = 'talk'
		end
	end

	-- Get the other inputs.
	local reason = protectionObj.reason
	local action = protectionObj.action
	local level = protectionObj.level
 
	--[[
	-- Define the properties table. Each property is a table containing the
	-- canonical order that the property is tested in, the position the
	-- property has in the category key strings, and the property value itself.
	--]]
	local properties = {
		expiry    = {order = 1, val = expiry},
		namespace = {order = 2, val = nskey},
		reason    = {order = 3, val = reason},
		level     = {order = 4, val = level},
		action    = {order = 5, val = action}
	}
 
	--[[
	-- Apply the category order configuration, if any. The configuration value
	-- will be a property string, e.g. 'reason', 'namespace', etc. The property
	-- corresponding to that string is tested last (i.e. it is the most
	-- important, because it keeps its specified value the longest) and the
	-- other properties are tested in the canonical order. If no configuration
	-- value is specified then the canonical order is used.
	--]]
	local configOrder = {}
	do
		local reasonsWithNamespacePriority = configObj:getConfigTable('reasonsWithNamespacePriority')
		local namespaceFirst = reason and reasonsWithNamespacePriority[reason] or false
		for propertiesKey, t in pairs(properties) do
			configOrder[t.order] = t
		end
		if namespaceFirst then
			-- Swap namespace and reason around.
			local namespaceTable = table.remove(configOrder, 2)
			table.insert(configOrder, 3, namespaceTable)
		end
	end
 
	--[[
	-- Define the attempt order. Properties with no value defined are moved
	-- to the end, where they will later be given the value "all". This is
	-- to cut down on the number of table lookups in the cats table, which
	-- grows exponentially with the number of properties with valid values.
	-- We keep track of the number of active properties with the noActive
	-- parameter.
	--]]
	local noActive, attemptOrder
	do
		local active, inactive = {}, {}
		for i, t in ipairs(configOrder) 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 properties
	-- we are given, that match will be found first. If not, we keep
	-- trying different key combinations until we match using the key
	-- "all-all-all-all-all".
	--
	-- To generate the keys, we index the property subtables using a
	-- binary matrix with indexes i and j. j is only calculated up to
	-- the number of active properties. For example, if there were three
	-- active properties, the matrix would look like this, with 0
	-- corresponding to the string "all", and 1 corresponding to the
	-- val field in the property table:
	-- 
	--   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 properties are set
	-- to the string "all".
	--
	-- A key for the category table is constructed for each value of i.
	-- The correct position of the value in the key is determined by the
	-- pos field in the property table.
	--]]
	local cats = configObj:getConfigTable('protectionCategories')
	local cat
	for i = 1, 2^noActive do
		local key = {}
		for j, t in ipairs(attemptOrder) do
			if j > noActive then
				key[t.order] = 'all'
			else
				local quotient = i / 2 ^ (j - 1)
				quotient = math.ceil(quotient)
				if quotient % 2 == 1 then
					key[t.order] = t.val
				else
					key[t.order] = 'all'
				end
			end
		end
		key = table.concat(key, '-')
		local attempt = cats[key]
		if attempt then
			cat = attempt
			break
		end
	end
	if cat then
		self:setName(cat)
	end
	return Category.render(self)
end

--------------------------------------------------------------------------------
-- ExpiryCategory class
--------------------------------------------------------------------------------

local ExpiryCategory = Category:subclass('ExpiryCategory')

function ExpiryCategory:render()
	local reasonsWithoutExpiryCheck = self._configObj:getConfigTable('reasonsWithoutExpiryCheck')
	local expiryCheckActions = self._configObj:getConfigTable('expiryCheckActions')
	local expiry = self._protectionObj.expiry
	local action = self._protectionObj.action
	local reason = self._protectionObj.reason
	
	if not expiry
		and expiryCheckActions[action]
		and reason -- the old {{pp-protected}} didn't check for expiry
		and not reasonsWithoutExpiryCheck[reason]
	then
		self:setName(configObj:getMessage('tracking-category-expiry'))
	end
	return Category.render(self)
end

--------------------------------------------------------------------------------
-- ErrorCategory class
--------------------------------------------------------------------------------

local ErrorCategory = Category:subclass('ErrorCategory')

function ErrorCategory:render()
	local configObj = self._configObj
	local protectionObj = self._protectionObj
	local expiry = protectionObj.expiry
	local action = protectionObj.action
	local level = protectionObj.level

	if not protectionObj:isProtected()
		or type(expiry) == 'number' and expiry < os.time()
	then
		self:setName(configObj:getMessage('tracking-category-incorrect'))
	end
	return Category.render(self)
end

--------------------------------------------------------------------------------
-- TemplateCategory class
--------------------------------------------------------------------------------

local TemplateCategory = Category:subclass('TemplateCategory')

function TemplateCategory:initialize(configObj, protectionObj, titleObj)
	Category.initialize(self, configObj, protectionObj)
	self._titleObj = titleObj
end

function TemplateCategory:render()
	local configObj = self._configObj
	local action = self._protectionObj.action
	local level = self._protectionObj.level
	local namespace = self._titleObj.namespace
	
	if level == 'templateeditor'
		and (
			(action ~= 'edit' and action ~= 'move')
			or (namespace ~= 10 and namespace ~= 828)
		)
	then
		self:setName(configObj:getMessage('tracking-category-template'))
	end
	return Category.render(self)
end

--------------------------------------------------------------------------------
-- ProtectionBanner class
--------------------------------------------------------------------------------

local ProtectionBanner = {}

function ProtectionBanner.exportToWiki(frame, titleObj)
	mArguments = mArguments or require('Module:Arguments')
	local args = mArguments.getArgs(frame)
	return ProtectionBanner.exportToLua(args, titleObj)
end

function ProtectionBanner.exportToLua(args, titleObj)
	titleObj = titleObj or mw.title.getCurrentTitle()

	-- Get data objects
	local configObj = Config:new()
	local protectionObj = Protection:new(args, configObj, titleObj)

	-- Initialise the blurb object
	local blurbObj = Blurb:new(configObj, protectionObj, titleObj)
	blurbObj:setDeletionDiscussionPage(args.xfd)
	blurbObj:setUsername(args.user)
	blurbObj:setSection(args.section)

	local ret = {}

	-- Render the banner
	if protectionObj:isProtected() then
		-- Get the banner object
		local isPadlock = yesno(args.small)
		local bannerObj
		if isPadlock then
			bannerObj = Padlock:new(configObj)
		else
			bannerObj = Banner:new(configObj)
		end

		-- Set the image fields
		local bannerConfig = configObj:getBannerConfig(protectionObj)
		bannerObj:setImageFilename(bannerConfig.image, protectionObj, titleObj)
		if isPadlock then
			bannerObj:setImageTooltip(blurbObj:makeTooltipText())
			bannerObj:setImageAlt(blurbObj:makeAltText())
			bannerObj:setImageLink(blurbObj:makeLinkText())
		else
			-- Large banners use the alt text for the tooltip.
			bannerObj:setImageTooltip(blurbObj:makeAltText())
		end

		-- Set the text fields
		if not isPadlock then
			bannerObj:setReasonText(blurbObj:makeReasonText())
			bannerObj:setExplanationText(blurbObj:makeExplanationText())
		end

		ret[#ret + 1] = bannerObj:render()
	end
	
	-- Render the categories
	if yesno(args.category) ~= false then
		local objects = {
			ProtectionCategory:new(configObj, protectionObj, titleObj),
			ExpiryCategory:new(configObj, protectionObj),
			ErrorCategory:new(configObj, protectionObj),
			TemplateCategory:new(configObj, protectionObj, titleObj)
		}
		for _, obj in ipairs(objects) do
			ret[#ret + 1] = obj:render()
		end
	end
	
	return table.concat(ret)	
end

function ProtectionBanner._exportClasses()
	-- This is used to export the classes for testing purposes.
	return {
		Protection = Protection,
		Config = Config,
		Blurb = Blurb,
		BannerTemplate = BannerTemplate,
		Banner = Banner,
		Padlock = Padlock,
		Category = Category,
		ProtectionCategory = ProtectionCategory,
		ErrorCategory = ErrorCategory,
		ExpiryCategory = ExpiryCategory,
	}
end

return ProtectionBanner