Module:Protection banner

From Amanita Research
Revision as of 11:56, 23 June 2014 by wikipedia>Mr. Stradivarius (allow specifying a data table to config objects for testing purposes)
Jump to navigation Jump to search

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 mFileLink = require('Module:File link')
local mProtectionLevel = require('Module:Effective protection level')
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(protectionStatusObj)
	if self._bannerConfigTables[protectionStatusObj] then
		return self._bannerConfigTables[protectionStatusObj]
	else
		local ret = {}
		local cfg = self._cfg
		local action = protectionStatusObj:getAction()
		local level = protectionStatusObj:getLevel()
		local reason = protectionStatusObj:getReason()
		local fields = {
			'text',
			'explanation',
			'tooltip',
			'alt',
			'image',
			'categoryOrder',
			'categoryReason'
		}
		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
		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[protectionStatusObj] = ret
		return ret
	end
end

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

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

--------------------------------------------------------------------------------
-- ProtectionStatus class
--------------------------------------------------------------------------------

local ProtectionStatus = class('ProtectionStatus')

function ProtectionStatus: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 = mProtectionLevel._main(self._action, titleObj)
		if level == 'accountcreator' then
			-- Lump titleblacklisted pages in with template-protected pages,
			-- since templateeditors can do both.
			level = 'templateeditor'
		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 other params
	self._reason = args.reason
	self._protectionDate = validateDate(args.date, 'protection date')
end

function ProtectionStatus:getAction()
	return self._action
end

function ProtectionStatus:getLevel()
	return self._level
end

function ProtectionStatus:getReason()
	return self._reason
end

function ProtectionStatus:getExpiry()
	return self._expiry
end

function ProtectionStatus:getProtectionDate()
	return self._protectionDate
end

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

local Blurb = class('Blurb')

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

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

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:_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[1] = self._makeIntroParameter
		parameterFuncs[2] = self._makeUntilParameter
		parameterFuncs[3] = self._makeDisputesParameter
		parameterFuncs[4] = self._makePagetypeParameter
		parameterFuncs[5] = self._makeProtectionDateParameter
		parameterFuncs[6] = self._makeVandalTemplateParameter
		parameterFuncs[7] = self._makeProtectionLevelParameter
		parameterFuncs[8] = self._makeExpiryParameter
		parameterFuncs[9] = self._makeDisputeLinkParameter -- A link to the page history or the move log
		parameterFuncs[10] = self._makeProtectionLogParameter
		parameterFuncs[11] = self._makeTalkLinkParameter
		parameterFuncs[12] = self._makeEditRequestParameter
		parameterFuncs[13] = self._makeRequestUnprotectionParameter
		parameterFuncs[14] = self._makeSubjectPageLinksParameter -- Adds links to edit requests and the talk page if we are on a subject page
		parameterFuncs[15] = self._makeDeletionBlurbParameter
		parameterFuncs[16] = self._makeDeletionDiscussionLinkParameter
		parameterFuncs[17] = self._makeDeletionLogParameter
		parameterFuncs[18] = self._makeExplanationTextParameter
		
		self._params = params
	end

	local function getParameter(match)
		match = tonumber(match)
		return self._params[match]
	end

	msg = msg:gsub('$([1-9][0-9]*)', getParameter)
	return msg
end

function Blurb:_makeIntroParameter()
	-- parameter $1
	local key
	local action = self._protectionStatusObj:getAction()
	local level = self._protectionStatusObj:getLevel()
	if action == 'edit' and level == 'autoconfirmed' then
		key = 'reason-text-semi'
	elseif action == 'move' then
		key = 'reason-text-move'
	elseif action == 'create' then
		key = 'reason-text-create'
	else
		key = 'reason-text-default'
	end
	local msg = self._configObj:getMessage(key)
	return self:_substituteParameters(msg)
end

function Blurb:_makeUntilParameter()
	-- parameter $2
	-- "until" or "or until" depending on the expiry
	local expiry = self._protectionStatusObj:getExpiry()
	if expiry then
		return 'or until'
	else
		return 'until'
	end
end

function Blurb:_makeDisputesParameter()
	-- parameter $3
	-- "disputes", with or without a section link
	local section = self._section
	local disputes = self.configObj:getMessage('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:_makePagetypeParameter()
	-- parameter $4
	local pagetypes = self._configObj:getConfigTable('bannerPagetypes')
	local namespace = self._titleObj.namespace
	return pagetypes[namespace] or pagetypes.default or 'page'
end

function Blurb:_makeProtectionDateParameter()
	-- parameter $5
	local protectionDate = self._protectionStatusObj:getProtectionDate()
	if type(protectionDate) == 'number' then
		return Blurb.formatDate(protectionDate)
	else
		return protectionDate
	end
end

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

function Blurb:_makeProtectionLevelParameter()
	-- parameter $7
	local action = self._protectionStatusObj:getAction()
	local level = self._protectionStatusObj:getLevel()
	local key
	if action == 'edit' then
		if level == 'sysop' then
			key = 'protection-level-full'
		elseif level == 'templateeditor' then
			key = 'protection-level-template'
		elseif level == 'autoconfirmed' then
			key = 'protection-level-semi'
		end
	elseif action == 'move' then
		key = 'protection-level-move'
	elseif action == 'create' then
		key = 'protection-level-create'
	else
		key = 'protection-level-default'
	end
	return self._configObj:getMessage(key)
end

function Blurb:_makeExpiryParameter()
	-- parameter $8
	-- @TODO: Check to see if the expiry is valid.
	local expiry = self._protectionStatusObj:getExpiry()
	if expiry == 'indef' then
		return nil
	elseif type(expiry) == 'number' then
		local formatted = Blurb.formatDate(expiry)
		return ' until ' .. formatted
	elseif expiry then
		-- Expiry is an error string.
		return expiry
	end
end

function Blurb:_makeDisputeLinkParameter()
	-- parameter $9
	-- A link to the page history or the move log, depending on the kind of
	-- protection.
	local action = self._protectionStatusObj:getAction()
	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._configObj:getMessage('dispute-move-link-display')
		)
	else
		-- We need the history link.
		return self.makeFullUrl(
			pagename,
			{action = 'history'},
			self._configObj:getMessage('dispute-edit-link-display')
		)
	end
end

function Blurb:_makeProtectionLogParameter()
	-- parameter $10
	local action = self._protectionStatusObj:getAction()
	if action == 'autoreview' then
		-- We need the pending changes log.
		return self.makeFullUrl(
			'Special:Log',
			{type = 'stable', page = pagename},
			self._configObj:getMessage('more-details-pc-log-display')
		)
	else
		-- We need the protection log.
		return self.makeFullUrl(
			'Special:Log',
			{type = 'protect', page = pagename},
			self._configObj:getMessage('more-details-protection-log-display')
		)
	end
end

function Blurb:_makeTalkLinkParameter()
	-- parameter $11
	local section = self._section
	local display = self._configObj:getMessage('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:_makeEditRequestParameter()
	-- parameter $12
	local mEditRequest = require('Module:Submit an edit request')
	local action = self._protectionStatusObj:getAction()
	local level = self._protectionStatusObj:getLevel()
	
	-- 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._configObj:getMessage(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:_makeRequestUnprotectionParameter()
	-- parameter $13
	if self._titleObj.namespace ~= 8 then
		-- MediaWiki pages can't be unprotected.
		return self._configObj:getMessage('request-unprotection-blurb')
	end
end

function Blurb:_makeSubjectPageLinksParameter()
	-- parameter $14
	-- Don't display these links if we are on a talk page.
	if not self._titleObj.isTalkPage then
		local msg = self._config:getMessage('semi-subject-page-links')
		return self._substituteParameters(msg)
	end
end

function Blurb:_makeDeletionBlurbParameter()
	-- parameter $15
	local deletionDiscussionPage = self._deletionDiscussionPage
	local key
	if deletionDiscussionPage then
		key = 'deletion-discussion-blurb-xfd'
	else
		key = 'deletion-discussion-blurb-noxfd'
	end
	local msg = self._configObj:getMessage(msg)
	return self._substituteParameters(msg)
end

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

function Blurb:_makeDeletionLogParameter()
	-- parameter $17
	return Blurb.makeFullUrl(
		'Special:Log',
		{type = 'delete', page = self._titleObj.prefixedText},
		self._configObj:getMessage('deletion-log-link-display')
	)
end

function Blurb:_makeExplanationTextParameter()
	-- parameter $18
	local action = self._protectionStatusObj:getAction()
	local level = self._protectionStatusObj:getLevel()
	local key
	if action == 'edit' and level == 'autoconfirmed' then
		key = 'explanation-text-semi'
	elseif action == 'move' then
		key = 'explanation-text-move'
	elseif action == 'create' then
		key = 'explanation-text-create'
	else
		key = 'explanation-text-default'
	end
	local msg = self._configObj:getMessage(key)
	return self:_substituteParameters(msg)
end

function Blurb:makeReasonText()
	local msg = self._bannerConfig.text
	return self:_substituteParameters(msg)
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

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

local BannerTemplate = class('BannerTemplate')

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

function BannerTemplate:setImageFilename(filename, action, level, namespace, expiry)
	if filename then
		self._imageFilename = filename
		return nil
	end
	
	if not action or not level then
		-- If the filename is not specified, we need the action and the level
		-- to find the image name. The namespace and the expiry are optional,
		-- however.
		return nil
	end
	
	-- 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:setImageAlt(alt)
	self._imageAlt = alt
end

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

function BannerTemplate:setImageCaption(caption)
	self._imageCaption = caption
end

function BannerTemplate:renderImage()
	local filename = self._filename
		or self._configObj:getMessage('image-filename-default')
		or 'Transparent.gif'
	return mFileLink.new(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()
	self:setImageWidth(40)
end

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

local Padlock = BannerTemplate:subclass('Padlock')

function Padlock:initialize()
	self:setImageWidth(20)
end

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

local Category = class('Category')

function Category:initialize()
end

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

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

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

local ProtectionCategory = Category:subclass('ProtectionCategory')

function ProtectionCategory:setName(
		name,
		configObj,
		protectionStatusObj,
		namespace
	)
	--[[
	-- Sets the protection category. If a category name is not provided, this
	-- method gets a category name from the module config, given a combination
	-- of the protection type, the protection level, the namespace number, the
	-- reason for protection, and the expiry date.
	--]]
	
	-- If a name was provided, use that.
	if name then
		Category.setName(self, name)
	end
	
	-- Get the namespace category key from the namespace number.
	local nskey
	do
		local categoryNamespaces = configObj:getConfigTable('categoryNamespaces')
		if not namespace or type(namespace) ~= 'number' then
			nskey = nil
		else
			nskey = categoryNamespaces[ns]
			if not nskey and ns % 2 == 1 then
					nskey = 'talk'
			end
		end
	end
 
	--[[
	-- 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, keypos = 5, val = protectionStatusObj:getExpiry()},
		namespace = {order = 2, keypos = 3, val = nskey},
		reason = {order = 3, keypos = 4, val = protectionStatusObj:getReason()},
		level = {order = 4, keypos = 2, val = protectionStatusObj:getLevel()},
		action = {order = 5, keypos = 1, val = protectionStatusObj:getAction()}
	}
 
	--[[
	-- 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 bannerConfig = configObj:getBannerConfig(protectionStatusObj)
		local categoryOrder = bannerConfig.categoryOrder
		for propertiesKey, t in pairs(properties) do
			configOrder[t.order] = t
		end
		if categoryOrder then
			local property = properties[categoryOrder]
			if not property then
				local msg = '"'
					.. categoryOrder
					.. '" is not a valid value of cfg.reasons.'
					.. reason
					.. '.categoryOrder'
				error(msg)
			end
			table.insert(configOrder, table.remove(configOrder, property.order))
		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('categories')
	local cat
	for i = 1, 2^noActive do
		local key = {}
		for j, t in ipairs(attemptOrder) do
			if j > 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
			cat = attempt
			break
		end
	end
	if cat then
		Category.setName(self, cat)
	else
		error(
			'No category match found;'
			.. ' please define the category for key "all-all-all-all-all"'
		)
	end
end

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

local ExpiryCategory = Category:subclass('ExpiryCategory')

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

local ErrorCategory = Category:subclass('ErrorCategory')

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

local ProtectionBanner = {}

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

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

	-- Get data objects
	local theConfig = Config:new()
	local theProtectionStatus = ProtectionStatus:new(args, theConfig, title)

	local ret = {}

	-- Render the banner
	local theBanner
	if yesno(args.small) then
		theBanner = Padlock.new(theConfig, theProtectionStatus, title)
	else
		theBanner = Banner.new(theConfig, theProtectionStatus, title)
	end
	theBanner = Padlock.new(theConfig, theProtectionStatus, title)
	theBanner:setDeletionDiscussionPage(args.xfd)
	theBanner:setUsername(args.user)
	theBanner:setSection(args.section)
	ret[#ret + 1] = theBanner:render()
	
	-- Render the categories

	return table.concat(ret)	
end

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

return ProtectionBanner