Module:Protection banner
From Timelines
Revision as of 10:26, 29 June 2014 by Jackmcbarn (talk) (no need to special case indef expiry. if it's indef, the messages that get used don't use it anyway)
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 getArgs, makeMessageBox, lang -------------------------------------------------------------------------------- -- Helper functions -------------------------------------------------------------------------------- local function makeCategoryLink(cat) if cat then return string.format( '[[%s:%s]]', mw.site.namespaces[14].name, cat ) else return '' end end -- Validation function for the expiry and the protection date local function validateDate(dateString, dateType) lang = lang or mw.language.getContentLanguage() local success, result = pcall(lang.formatDate, lang, 'U', dateString) if success then result = tonumber(result) if result then return result end end error(string.format( 'invalid %s ("%s")', dateType, tostring(dateString) )) end local function makeFullUrl(page, query, display) return string.format( '[%s %s]', tostring(mw.uri.fullUrl(page, query)), display ) end -------------------------------------------------------------------------------- -- Protection class -------------------------------------------------------------------------------- local Protection = class('Protection') Protection.supportedActions = { create = true, edit = true, move = true, autoreview = true } Protection.bannerConfigFields = { 'text', 'explanation', 'tooltip', 'alt', 'link', 'image' } function Protection:initialize(args, cfg, title) self._cfg = cfg self.title = title or mw.title.getCurrentTitle() -- Set action if not args.action then self.action = 'edit' elseif self.supportedActions[args.action] then self.action = args.action else error('Unsupported action ' .. args.action, 2) end -- Set level self.level = effectiveProtectionLevel(self.action, self.title) if self.level == 'accountcreator' then -- Lump titleblacklisted pages in with template-protected pages, -- since templateeditors can do both. self.level = 'templateeditor' elseif not self.level or (self.action == 'move' and self.level == 'autoconfirmed') then -- Users need to be autoconfirmed to move pages anyway, so treat -- semi-move-protected pages as unprotected. self.level = '*' end -- Set expiry if args.expiry then if cfg.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') -- Set banner config do self.bannerConfig = {} 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(self.bannerConfigFields) 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:isProtected() return self.level ~= '*' end function Protection:makeProtectionCategory() local cfg = self._cfg local title = self.title -- Exit if the page is not protected. if not self:isProtected() then return '' end -- Get the expiry key fragment. local expiryFragment if self.expiry == 'indef' then expiryFragment = self.expiry elseif type(self.expiry) == 'number' then expiryFragment = 'temp' end -- Get the namespace key fragment. local namespaceFragment do namespaceFragment = cfg.categoryNamespaceKeys[title.namespace] if not namespaceFragment and title.namespace % 2 == 1 then namespaceFragment = 'talk' end end -- Define the order that key fragments are tested in. This is done with an -- array of tables containing the value to be tested, along with its -- position in the cfg.protectionCategories table. local order = { {val = expiryFragment, keypos = 1}, {val = namespaceFragment, keypos = 2}, {val = self.reason, keypos = 3}, {val = self.level, keypos = 4}, {val = self.action, keypos = 5} } -- To generate the correct category for some reason values, we need to -- prioritise the position of the namespace key fragment over that of the -- reason key fragment. For these reasn values, swap the namespace subtable -- and the reason subtable around. if self.reason and cfg.reasonsWithNamespacePriority[self.reason] then table.insert(order, 3, table.remove(order, 2)) end --[[ -- Define the attempt order. Inactive subtables (subtables with nil "value" -- fields) are moved to the end, where they will later be given the key -- "all". This is to cut down on the number of table lookups in -- cfg.protectionCategories, which grows exponentially with the number of -- non-nil keys. We keep track of the number of active subtables with the -- noActive parameter. --]] local noActive, attemptOrder do local active, inactive = {}, {} for i, t in ipairs(order) do if t.val then active[#active + 1] = t else inactive[#inactive + 1] = t end end noActive = #active attemptOrder = active for i, t in ipairs(inactive) do attemptOrder[#attemptOrder + 1] = t end end --[[ -- Check increasingly generic key combinations until we find a match. If a -- specific category exists for the combination of key fragments we are -- given, that match will be found first. If not, we keep trying different -- key fragment combinations until we match using the key -- "all-all-all-all-all". -- -- To generate the keys, we index the key subtables using a binary matrix -- with indexes i and j. j is only calculated up to the number of active -- subtables. For example, if there were three active subtables, the matrix -- would look like this, with 0 corresponding to the key fragment "all", and -- 1 corresponding to other key fragments. -- -- j 1 2 3 -- i -- 1 1 1 1 -- 2 0 1 1 -- 3 1 0 1 -- 4 0 0 1 -- 5 1 1 0 -- 6 0 1 0 -- 7 1 0 0 -- 8 0 0 0 -- -- Values of j higher than the number of active subtables are set -- to the string "all". -- -- A key for cfg.protectionCategories is constructed for each value of i. -- The position of the value in the key is determined by the keypos field in -- each subtable. --]] local cats = cfg.protectionCategories for i = 1, 2^noActive do local key = {} for j, t in ipairs(attemptOrder) do if j > noActive then key[t.keypos] = 'all' else local quotient = i / 2 ^ (j - 1) quotient = math.ceil(quotient) if quotient % 2 == 1 then key[t.keypos] = t.val else key[t.keypos] = 'all' end end end key = table.concat(key, '-') local attempt = cats[key] if attempt then return makeCategoryLink(attempt) end end end function Protection:needsExpiry() local cfg = self._cfg return not self.expiry and cfg.expiryCheckActions[self.action] and self.reason -- the old {{pp-protected}} didn't check for expiry and not cfg.reasonsWithoutExpiryCheck[self.reason] end function Protection:isIncorrect() local expiry = self.expiry return not self:isProtected() or type(expiry) == 'number' and expiry < os.time() end function Protection:isTemplateProtectedNonTemplate() local action, namespace = self.action, self.title.namespace return self.level == 'templateeditor' and ( (action ~= 'edit' and action ~= 'move') or (namespace ~= 10 and namespace ~= 828) ) end function Protection:makeCategoryLinks() local msg = self._cfg.msg local ret = { self:makeProtectionCategory() } if self:needsExpiry() then ret[#ret + 1] = makeCategoryLink(msg['tracking-category-expiry']) end if self:isIncorrect() then ret[#ret + 1] = makeCategoryLink(msg['tracking-category-incorrect']) end if self:isTemplateProtectedNonTemplate() then ret[#ret + 1] = makeCategoryLink(msg['tracking-category-template']) end return table.concat(ret) end -------------------------------------------------------------------------------- -- Blurb class -------------------------------------------------------------------------------- local Blurb = class('Blurb') function Blurb:initialize(protectionObj, args, cfg) self._cfg = cfg self._protectionObj = protectionObj self._deletionDiscussionPage = args.xfd self._username = args.user self._section = args.section end -- Static methods -- function Blurb.formatDate(num) -- Formats a Unix timestamp into dd Month, YYYY format. lang = lang or mw.language.getContentLanguage() local success, date = pcall( lang.formatDate, lang, 'j F Y', '@' .. tostring(num) ) if success then return date end end -- Private methods -- function Blurb:_getExpandedMessage(msgKey) return self:_substituteParameters(self._cfg.msg[msgKey]) end function Blurb:_substituteParameters(msg) if not self._params then local parameterFuncs = {} parameterFuncs.CURRENTVERSION = self._makeCurrentVersionParameter parameterFuncs.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 = setmetatable({}, { __index = function (t, k) local param if parameterFuncs[k] then param = parameterFuncs[k](self) end param = param or '' t[k] = param return param end }) end msg = msg:gsub('${(%u+)}', self._params) return msg end function Blurb:_makeCurrentVersionParameter() -- A link to the page history or the move log, depending on the kind of -- protection. local pagename = self._protectionObj.title.prefixedText if self._protectionObj.action == 'move' then -- We need the move log link. return makeFullUrl( 'Special:Log', {type = 'move', page = pagename}, self:_getExpandedMessage('current-version-move-display') ) else -- We need the history link. return makeFullUrl( pagename, {action = 'history'}, self:_getExpandedMessage('current-version-edit-display') ) end end function Blurb:_makeDeletionDiscussionLinkParameter() if self._deletionDiscussionPage then local display = self:_getExpandedMessage('deletion-discussion-link-display') return string.format('[[%s|%s]]', self._deletionDiscussionPage, display) end end function Blurb:_makeDisputeBlurbParameter() if type(self._protectionObj.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 disputes = self:_getExpandedMessage('dispute-section-link-display') if self._section then return string.format( '[[%s:%s#%s|%s]]', mw.site.namespaces[self._protectionObj.title.namespace].talk.name, self._protectionObj.title.text, self._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 type(expiry) == 'number' then return Blurb.formatDate(expiry) else return expiry end end function Blurb:_makeExplanationBlurbParameter() local action = self._protectionObj.action local level = self._protectionObj.level local namespace = self._protectionObj.title.namespace local isTalk = self._protectionObj.title.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 if self._deletionDiscussion 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._cfg.imageLinks local action = self._protectionObj.action local level = self._protectionObj.level local msg if imageLinks[action][level] then msg = imageLinks[action][level] elseif imageLinks[action].default then msg = imageLinks[action].default else msg = imageLinks.edit.default end return self:_substituteParameters(msg) end function Blurb:_makeIntroBlurbParameter() if type(self._protectionObj.expiry) == 'number' then return self:_getExpandedMessage('intro-blurb-expiry') else return self:_getExpandedMessage('intro-blurb-noexpiry') end end function Blurb:_makeOfficeBlurbParameter() if self._protectionObj.protectionDate then return self:_getExpandedMessage('office-blurb-protectiondate') else return self:_getExpandedMessage('office-blurb-noprotectiondate') end end function Blurb:_makePagetypeParameter() local pagetypes = self._cfg.pagetypes return pagetypes[self._protectionObj.title.namespace] or pagetypes.default or error('no default pagetype defined') end function Blurb:_makeProtectionBlurbParameter() local protectionBlurbs = self._cfg.protectionBlurbs local action = self._protectionObj.action local level = self._protectionObj.level local msg if protectionBlurbs[action][level] then msg = protectionBlurbs[action][level] elseif protectionBlurbs[action].default then msg = protectionBlurbs[action].default elseif protectionBlurbs.edit.default then msg = protectionBlurbs.edit.default else error('no protection blurb defined for protectionBlurbs.edit.default') 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._cfg.protectionLevels local action = self._protectionObj.action local level = self._protectionObj.level local msg if protectionLevels[action][level] then msg = protectionLevels[action][level] elseif protectionLevels[action].default then msg = protectionLevels[action].default elseif protectionLevels.edit.default then msg = protectionLevels.edit.default else error('no protection level defined for protectionLevels.edit.default') end return self:_substituteParameters(msg) end function Blurb:_makeProtectionLogParameter() local pagename = self._protectionObj.title.prefixedText if self._protectionObj.action == 'autoreview' then -- We need the pending changes log. return makeFullUrl( 'Special:Log', {type = 'stable', page = pagename}, self:_getExpandedMessage('pc-log-display') ) else -- We need the protection log. return makeFullUrl( 'Special:Log', {type = 'protect', page = pagename}, self:_getExpandedMessage('protection-log-display') ) end end function Blurb:_makeResetBlurbParameter() if self._protectionObj.protectionDate then return self:_getExpandedMessage('reset-blurb-protectiondate') else return self:_getExpandedMessage('reset-blurb-noprotectiondate') end end function Blurb:_makeTalkPageParameter() return string.format( '[[%s:%s#%s|%s]]', mw.site.namespaces[self._protectionObj.title.namespace].talk.name, self._protectionObj.title.text, self._section or 'top', self:_getExpandedMessage('talk-page-link-display') ) end function Blurb:_makeTooltipBlurbParameter() if type(self._protectionObj.expiry) == 'number' then return self:_getExpandedMessage('tooltip-blurb-expiry') else return self:_getExpandedMessage('tooltip-blurb-noexpiry') end end function Blurb:_makeVandalTemplateParameter() return require('Module:Vandal-m')._main{ self._username or self._protectionObj.title.baseText } end -- Public methods -- function Blurb:makeReasonText() local msg = self._protectionObj.bannerConfig.text if msg then return self:_substituteParameters(msg) end end function Blurb:makeExplanationText() local msg = self._protectionObj.bannerConfig.explanation return self:_substituteParameters(msg) end function Blurb:makeTooltipText() local msg = self._protectionObj.bannerConfig.tooltip return self:_substituteParameters(msg) end function Blurb:makeAltText() local msg = self._protectionObj.bannerConfig.alt return self:_substituteParameters(msg) end function Blurb:makeLinkText() local msg = self._protectionObj.bannerConfig.link return self:_substituteParameters(msg) end -------------------------------------------------------------------------------- -- BannerTemplate class -------------------------------------------------------------------------------- local BannerTemplate = class('BannerTemplate') function BannerTemplate:initialize(protectionObj, cfg) self._cfg = cfg -- Set the image filename. local imageFilename = protectionObj.bannerConfig.image if imageFilename then self._imageFilename = imageFilename else -- If an image filename isn't specified explicitly in the banner config, -- generate it from the protection status and the namespace. local action = protectionObj.action local level = protectionObj.level local expiry = protectionObj.expiry local namespace = protectionObj.title.namespace -- Deal with special cases first. if (namespace == 10 or namespace == 828) and action == 'edit' and level == 'sysop' and not expiry then -- Fully protected modules and templates get the special red "indef" -- padlock. self._imageFilename = self._cfg.msg['image-filename-indef'] else -- Deal with regular protection types. local images = self._cfg.images if images[action] then if images[action][level] then self._imageFilename = images[action][level] elseif images[action].default then self._imageFilename = images[action].default end end end end 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._cfg.msg['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 -------------------------------------------------------------------------------- -- Banner class -------------------------------------------------------------------------------- local Banner = BannerTemplate:subclass('Banner') function Banner:initialize(protectionObj, blurbObj, cfg) BannerTemplate.initialize(self, protectionObj, cfg) -- This doesn't need the blurb. self:setImageWidth(40) self:setImageTooltip(blurbObj:makeAltText()) -- Large banners use the alt text for the tooltip. self._reasonText = blurbObj:makeReasonText() self._explanationText = blurbObj:makeExplanationText() self._page = protectionObj.title.prefixedText -- Only makes a difference in testing. end function Banner:__tostring() -- Renders the banner. makeMessageBox = makeMessageBox or require('Module:Message box').main local reasonText = self._reasonText or error('no reason text set') local explanationText = self._explanationText local mbargs = { page = self._page, type = 'protection', image = self:renderImage(), text = string.format( "'''%s'''%s", reasonText, explanationText and '<br />' .. explanationText or '' ) } return makeMessageBox('mbox', mbargs) end -------------------------------------------------------------------------------- -- Padlock class -------------------------------------------------------------------------------- local Padlock = BannerTemplate:subclass('Padlock') function Padlock:initialize(protectionObj, blurbObj, cfg) BannerTemplate.initialize(self, protectionObj, cfg) -- This doesn't need the blurb. self:setImageWidth(20) self:setImageTooltip(blurbObj:makeTooltipText()) self._imageAlt = blurbObj:makeAltText() self._imageLink = blurbObj:makeLinkText() self._right = cfg.padlockPositions[protectionObj.action] or cfg.padlockPositions.default or '55px' end function Padlock:__tostring() local root = mw.html.create('div') root :addClass('metadata topicon nopopups') :attr('id', 'protected-icon') :css{display = 'none', right = self._right} :wikitext(self:renderImage()) return tostring(root) end -------------------------------------------------------------------------------- -- Exports -------------------------------------------------------------------------------- local p = {} function p._exportClasses() -- This is used for testing purposes. return { Protection = Protection, Blurb = Blurb, BannerTemplate = BannerTemplate, Banner = Banner, Padlock = Padlock, } end function p._main(args, cfg, title) if not cfg then cfg = mw.loadData('Module:Protection banner/config') end -- Initialise protection and blurb objects local protectionObj = Protection:new(args, cfg, title) local blurbObj = Blurb:new(protectionObj, args, cfg) local ret = {} -- Render the banner if protectionObj:isProtected() then ret[#ret + 1] = tostring( (yesno(args.small) and Padlock or Banner) :new(protectionObj, blurbObj, cfg) ) end -- Render the categories if yesno(args.category) ~= false then ret[#ret + 1] = protectionObj:makeCategoryLinks() end return table.concat(ret) end function p.main(frame) if not getArgs then getArgs = require('Module:Arguments').getArgs end local args = getArgs(frame, {wrappers = 'Template:Pp'}) return p._main(args) end return p