Module:Protection banner
From MINR.ORG WIKI
Revision as of 15:06, 23 June 2014 by Mr. Stradivarius (talk) (Add render methods to the Padlock and Banner classes, and call them from the exportToLua function. Now we have something to look at, yay!)
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 reason if args.reason then self._reason = args.reason:lower() end -- Set protection date 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() 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._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._configObj: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 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, 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:setImageTooltip(tooltip) self._imageCaption = tooltip end function BannerTemplate:renderImage() local filename = self._imageFilename 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(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() 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, 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 = ProtectionStatus: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 do -- 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) local imageFilename = bannerConfig.image bannerObj:setImageFilename( imageFilename, protectionObj:getAction(), protectionObj:getLevel(), titleObj.namespace, protectionObj:getExpiry() ) 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 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