Module:Protection banner: Difference between revisions

From Chalo Chatu, Zambia online encyclopedia
Jump to navigationJump to search
http://www.chalochatu.org/>SilverLocust
fix for mobile and search issue, see talk page
No edit summary
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
-- This module implements {{pp-meta}} and its daughter templates such as
-- Module:Protection banner (Chalo Chatu)
-- {{pp-dispute}}, {{pp-vandalism}} and {{pp-sock}}.
-- Lightweight, dependency-free. Declarative protection only.
-- Implements {{pp-meta}}-style usage via p.main().
-- Params (typical):
--  action=edit/move/upload
--  level=autoconfirmed/extendedconfirmed/templateeditor/sysop/* (unprotected)
--  reason=vandalism/dispute/sock/etc (free text)
--  expiry=indef | YYYY-MM-DD | free text (shown as-is)
--  date=YYYY-MM-DD (shown as "Protected on …")
--  small=yes -> padlock indicator; else banner
--   category=yes/no (default yes)
--  section=anchor on talk link (optional)
--   image=File name override (optional)
--   link=target page for padlock click (optional)


-- Initialise necessary modules.
local p = {}
require('strict')
local makeFileLink = require('Module:File link')._main
local effectiveProtectionLevel = require('Module:Effective protection level')._main
local effectiveProtectionExpiry = require('Module:Effective protection expiry')._main
local yesno = require('Module:Yesno')
 
-- Lazily initialise modules and objects we don't always need.
local getArgs, makeMessageBox, lang
 
-- Set constants.
local CONFIG_MODULE = 'Module:Protection banner/config'
 
--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
 
local function makeCategoryLink(cat, sort)
if cat then
return string.format(
'[[%s:%s|%s]]',
mw.site.namespaces[14].name,
cat,
sort
)
end
end
 
-- Validation function for the expiry and the protection date
local function validateDate(dateString, dateType)
if not lang then
lang = mw.language.getContentLanguage()
end
local success, result = pcall(lang.formatDate, lang, 'U', dateString)
if success then
result = tonumber(result)
if result then
return result
end
end
error(string.format(
'invalid %s: %s',
dateType,
tostring(dateString)
), 4)
end
 
local function makeFullUrl(page, query, display)
return string.format(
'[%s %s]',
tostring(mw.uri.fullUrl(page, query)),
display
)
end
 
-- Given a directed graph formatted as node -> table of direct successors,
-- get a table of all nodes reachable from a given node (though always
-- including the given node).
local function getReachableNodes(graph, start)
local toWalk, retval = {[start] = true}, {}
while true do
-- Can't use pairs() since we're adding and removing things as we're iterating
local k = next(toWalk) -- This always gets the "first" key
if k == nil then
return retval
end
toWalk[k] = nil
retval[k] = true
for _,v in ipairs(graph[k]) do
if not retval[v] then
toWalk[v] = true
end
end
end
end
 
--------------------------------------------------------------------------------
-- Protection class
--------------------------------------------------------------------------------
 
local Protection = {}
Protection.__index = Protection
 
Protection.supportedActions = {
edit = true,
move = true,
autoreview = true,
upload = true
}
 
Protection.bannerConfigFields = {
'text',
'explanation',
'tooltip',
'alt',
'link',
'image'
}
 
function Protection.new(args, cfg, title)
local obj = {}
obj._cfg = cfg
obj.title = title or mw.title.getCurrentTitle()
 
-- Set action
if not args.action then
obj.action = 'edit'
elseif Protection.supportedActions[args.action] then
obj.action = args.action
else
error(string.format(
'invalid action: %s',
tostring(args.action)
), 3)
end
 
-- Set level
obj.level = args.demolevel or effectiveProtectionLevel(obj.action, obj.title)
if not obj.level or (obj.action == 'move' and obj.level == 'autoconfirmed') then
-- Users need to be autoconfirmed to move pages anyway, so treat
-- semi-move-protected pages as unprotected.
obj.level = '*'
end
 
-- Set expiry
local effectiveExpiry = effectiveProtectionExpiry(obj.action, obj.title)
if effectiveExpiry == 'infinity' then
obj.expiry = 'indef'
elseif effectiveExpiry ~= 'unknown' then
obj.expiry = validateDate(effectiveExpiry, 'expiry date')
end
 
-- Set reason
if args[1] then
obj.reason = mw.ustring.lower(args[1])
if obj.reason:find('|') then
error('reasons cannot contain the pipe character ("|")', 3)
end
end
 
-- Set protection date
if args.date then
obj.protectionDate = validateDate(args.date, 'protection date')
end
-- Set banner config
do
obj.bannerConfig = {}
local configTables = {}
if cfg.banners[obj.action] then
configTables[#configTables + 1] = cfg.banners[obj.action][obj.reason]
end
if cfg.defaultBanners[obj.action] then
configTables[#configTables + 1] = cfg.defaultBanners[obj.action][obj.level]
configTables[#configTables + 1] = cfg.defaultBanners[obj.action].default
end
configTables[#configTables + 1] = cfg.masterBanner
for i, field in ipairs(Protection.bannerConfigFields) do
for j, t in ipairs(configTables) do
if t[field] then
obj.bannerConfig[field] = t[field]
break
end
end
end
end
return setmetatable(obj, Protection)
end
 
function Protection:isUserScript()
-- Whether the page is a user JavaScript or CSS page.
local title = self.title
return title.namespace == 2 and (
title.contentModel == 'javascript' or title.contentModel == 'css'
)
end
 
function Protection:isProtected()
return self.level ~= '*'
end
 
function Protection:shouldShowLock()
-- Whether we should output a banner/padlock
return self:isProtected() and not self:isUserScript()
end
 
-- Whether this page needs a protection category.
Protection.shouldHaveProtectionCategory = Protection.shouldShowLock
 
function Protection:isTemporary()
return type(self.expiry) == 'number'
end
 
function Protection:makeProtectionCategory()
if not self:shouldHaveProtectionCategory() then
return ''
end
 
local cfg = self._cfg
local title = self.title
-- Get the expiry key fragment.
local expiryFragment
if self.expiry == 'indef' then
expiryFragment = self.expiry
elseif type(self.expiry) == 'number' then
expiryFragment = 'temp'
end
 
-- Get the namespace key fragment.
local namespaceFragment = cfg.categoryNamespaceKeys[title.namespace]
if not namespaceFragment and title.namespace % 2 == 1 then
namespaceFragment = 'talk'
end
 
-- Define the order that key fragments are tested in. This is done with an
-- array of tables containing the value to be tested, along with its
-- position in the cfg.protectionCategories table.
local order = {
{val = expiryFragment,    keypos = 1},
{val = namespaceFragment, keypos = 2},
{val = self.reason,      keypos = 3},
{val = self.level,        keypos = 4},
{val = self.action,      keypos = 5}
}
 
--[[
-- The old protection templates used an ad-hoc protection category system,
-- with some templates prioritising namespaces in their categories, and
-- others prioritising the protection reason. To emulate this in this module
-- we use the config table cfg.reasonsWithNamespacePriority to set the
-- reasons for which namespaces have priority over protection reason.
-- If we are dealing with one of those reasons, move the namespace table to
-- the end of the order table, i.e. give it highest priority. If not, the
-- reason should have highest priority, so move that to the end of the table
-- instead.
--]]
table.insert(order, table.remove(order, self.reason and cfg.reasonsWithNamespacePriority[self.reason] and 2 or 3))
--[[
-- Define the attempt order. Inactive subtables (subtables with nil "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, title.text)
end
end
return ''
end
 
function Protection:isIncorrect()
local expiry = self.expiry
return not self:shouldHaveProtectionCategory()
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:isIncorrect() then
ret[#ret + 1] = makeCategoryLink(
msg['tracking-category-incorrect'],
self.title.text
)
end
if self:isTemplateProtectedNonTemplate() then
ret[#ret + 1] = makeCategoryLink(
msg['tracking-category-template'],
self.title.text
)
end
return table.concat(ret)
end
 
--------------------------------------------------------------------------------
-- Blurb class
--------------------------------------------------------------------------------
 
local Blurb = {}
Blurb.__index = Blurb
 
Blurb.bannerTextFields = {
text = true,
explanation = true,
tooltip = true,
alt = true,
link = true
}
 
function Blurb.new(protectionObj, args, cfg)
return setmetatable({
_cfg = cfg,
_protectionObj = protectionObj,
_args = args
}, Blurb)
end
 
-- Private methods --


function Blurb:_formatDate(num)
-- Basic yes/no parser
-- Formats a Unix timestamp into dd Month, YYYY format.
local function yesno(v, default)
lang = lang or mw.language.getContentLanguage()
  if v == nil then return default end
local success, date = pcall(
  if type(v) == 'boolean' then return v end
lang.formatDate,
  local s = tostring(v):lower()
lang,
  if s == 'yes' or s == 'y' or s == 'true' or s == '1' then return true end
self._cfg.msg['expiry-date-format'] or 'j F Y',
  if s == 'no'  or s == 'n' or s == 'false' or s == '0' then return false end
'@' .. tostring(num)
  return default
)
if success then
return date
end
end
end


function Blurb:_getExpandedMessage(msgKey)
-- HTML builder helpers (simple, safe)
return self:_substituteParameters(self._cfg.msg[msgKey])
local function esc(s)
  s = tostring(s or '')
  return s:gsub('&','&amp;'):gsub('<','&lt;'):gsub('>','&gt;')
end
end


function Blurb:_substituteParameters(msg)
local function tag(name, attrs, content)
if not self._params then
  local a = {}
local parameterFuncs = {}
  if attrs then
 
     for k,v in pairs(attrs) do
parameterFuncs.CURRENTVERSION     = self._makeCurrentVersionParameter
      if v ~= nil and v ~= '' then
parameterFuncs.EDITREQUEST        = self._makeEditRequestParameter
         table.insert(a, string.format(' %s="%s"', k, esc(v)))
parameterFuncs.EXPIRY            = self._makeExpiryParameter
      end
parameterFuncs.EXPLANATIONBLURB  = self._makeExplanationBlurbParameter
     end
parameterFuncs.IMAGELINK          = self._makeImageLinkParameter
  end
parameterFuncs.INTROBLURB         = self._makeIntroBlurbParameter
  if content == nil then
parameterFuncs.INTROFRAGMENT      = self._makeIntroFragmentParameter
    return string.format('<%s%s />', name, table.concat(a))
parameterFuncs.PAGETYPE          = self._makePagetypeParameter
  end
parameterFuncs.PROTECTIONBLURB    = self._makeProtectionBlurbParameter
  return string.format('<%s%s>%s</%s>', name, table.concat(a), content, name)
parameterFuncs.PROTECTIONDATE     = self._makeProtectionDateParameter
parameterFuncs.PROTECTIONLEVEL    = self._makeProtectionLevelParameter
parameterFuncs.PROTECTIONLOG      = self._makeProtectionLogParameter
parameterFuncs.TALKPAGE          = self._makeTalkPageParameter
parameterFuncs.TOOLTIPBLURB      = self._makeTooltipBlurbParameter
parameterFuncs.TOOLTIPFRAGMENT    = self._makeTooltipFragmentParameter
parameterFuncs.VANDAL            = self._makeVandalTemplateParameter
self._params = setmetatable({}, {
__index = function (t, k)
local param
if parameterFuncs[k] then
param = parameterFuncs[k](self)
end
param = param or ''
t[k] = param
return param
end
})
end
msg = msg:gsub('${(%u+)}', self._params)
return msg
end
end


function Blurb:_makeCurrentVersionParameter()
-- Minimal styling (relies on site Common.css, but works standalone)
-- A link to the page history or the move log, depending on the kind of
local function messageBox(imageHtml, textHtml)
-- protection.
  local left = tag('div', {style='flex:0 0 auto; padding-right:8px;'}, imageHtml or '')
local pagename = self._protectionObj.title.prefixedText
  local right = tag('div', {style='flex:1 1 auto;'}, textHtml or '')
if self._protectionObj.action == 'move' then
  local row = tag('div', {style='display:flex; align-items:flex-start;'}, left .. right)
-- We need the move log link.
  return tag('div', {
return makeFullUrl(
    class='cc-mbox cc-mbox-protection',
'Special:Log',
    style='border:1px solid #aaa;background:#f9f9f9;padding:8px;margin:0 0 1em 0;'
{type = 'move', page = pagename},
  }, row)
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
end


function Blurb:_makeEditRequestParameter()
local function padlockIndicator(imageHtml, linkTarget, alt)
local mEditRequest = require('Module:Submit an edit request')
  local content = imageHtml or ''
local action = self._protectionObj.action
  if linkTarget and linkTarget ~= '' then
local level = self._protectionObj.level
    content = string.format('[[%s|%s]]', linkTarget, content)
  end
-- Get the edit request type.
  return tag('span', {class='cc-padlock', title=alt or 'Page is protected'}, content)
local requestType
if action == 'edit' then
if level == 'autoconfirmed' then
requestType = 'semi'
elseif level == 'extendedconfirmed' then
requestType = 'extended'
elseif level == 'templateeditor' then
requestType = 'template'
end
end
requestType = requestType or 'full'
-- Get the display value.
local display = self:_getExpandedMessage('edit-request-display')
 
return mEditRequest._link{type = requestType, display = display}
end
end


function Blurb:_makeExpiryParameter()
local function fileLink(filename, size, alt, link)
local expiry = self._protectionObj.expiry
  if not filename or filename == '' then return '' end
if type(expiry) == 'number' then
  local core = string.format('[[File:%s|%s|%s]]',
return self:_formatDate(expiry)
    filename,
else
    size or '20px',
return expiry
    (alt and ('alt='..alt)) or ''
end
  )
  if link and link ~= '' then
    return string.format('[[%s|%s]]', link, core)
  end
  return core
end
end


function Blurb:_makeExplanationBlurbParameter()
-- Default config (overrideable by Module:Protection banner/config if present)
-- Cover special cases first.
local function getConfig()
if self._protectionObj.title.namespace == 8 then
  local ok, cfg = pcall(require, 'Module:Protection banner/config')
-- MediaWiki namespace
  if ok and type(cfg) == 'table' then return cfg end
return self:_getExpandedMessage('explanation-blurb-nounprotect')
  -- Built-in defaults for Chalo Chatu
end
  return {
 
    images = {
-- Get explanation blurb table keys
      edit = {
local action = self._protectionObj.action
        sysop = 'Padlock-red.svg',
local level = self._protectionObj.level
        templateeditor = 'Padlock-orange.svg',
local talkKey = self._protectionObj.title.isTalkPage and 'talk' or 'subject'
        extendedconfirmed = 'Padlock-amber.svg',
 
        autoconfirmed = 'Padlock-yellow.svg',
-- Find the message in the explanation blurb table and substitute any
        ['*'] = 'Padlock-grey.svg',
-- parameters.
        default = 'Padlock-blue.svg'
local explanations = self._cfg.explanationBlurbs
      },
local msg
      move = { default = 'Padlock-blue.svg' },
if explanations[action][level] and explanations[action][level][talkKey] then
      upload = { default = 'Padlock-blue.svg' },
msg = explanations[action][level][talkKey]
      default = 'Padlock-blue.svg'
elseif explanations[action][level] and explanations[action][level].default then
    },
msg = explanations[action][level].default
    padlockIndicatorName = 'pp',
elseif explanations[action].default and explanations[action].default[talkKey] then
    -- Simple phrasing tables
msg = explanations[action].default[talkKey]
    protectionLevels = {
elseif explanations[action].default and explanations[action].default.default then
      edit = {
msg = explanations[action].default.default
        sysop='fully protected', templateeditor='template-protected',
else
        extendedconfirmed='extended confirmed protected',
error(string.format(
        autoconfirmed='semi-protected', ['*']='unprotected', default='protected'
'could not find explanation blurb for action "%s", level "%s" and talk key "%s"',
      },
action,
      move = { default='move-protected' },
level,
      upload = { default='upload-protected' }
talkKey
    },
), 8)
    categories = {
end
      -- Built from: action/level -> category name. Fallbacks used below.
return self:_substituteParameters(msg)
      ['edit/sysop'] = 'Fully protected pages',
      ['edit/autoconfirmed'] = 'Semi-protected pages',
      ['edit/extendedconfirmed'] = 'Extended-confirmed protected pages',
      ['edit/templateeditor'] = 'Template-protected pages',
      ['move/sysop'] = 'Move-protected pages',
      ['upload/sysop'] = 'Upload-protected pages',
      default = 'Protected pages'
    },
    talkNsName = 'Talk'
  }
end
end


function Blurb:_makeImageLinkParameter()
local function pickImage(cfg, action, level, override)
local imageLinks = self._cfg.imageLinks
  if override and override ~= '' then return override end
local action = self._protectionObj.action
  local imgs = cfg.images
local level = self._protectionObj.level
  if imgs[action] then
local msg
    return imgs[action][level] or imgs[action].default or imgs.default
if imageLinks[action][level] then
  end
msg = imageLinks[action][level]
  return imgs.default
elseif imageLinks[action].default then
msg = imageLinks[action].default
else
msg = imageLinks.edit.default
end
return self:_substituteParameters(msg)
end
end


function Blurb:_makeIntroBlurbParameter()
local function pickLevelName(cfg, action, level)
if self._protectionObj:isTemporary() then
  local t = cfg.protectionLevels[action] or {}
return self:_getExpandedMessage('intro-blurb-expiry')
  return t[level] or t.default or 'protected'
else
return self:_getExpandedMessage('intro-blurb-noexpiry')
end
end
end


function Blurb:_makeIntroFragmentParameter()
local function buildCategory(cfg, action, level)
if self._protectionObj:isTemporary() then
  local key = (action or 'edit') .. '/' .. (level or '*')
return self:_getExpandedMessage('intro-fragment-expiry')
  local cat = cfg.categories[key] or cfg.categories.default
else
  return string.format('[[Category:%s]]', cat)
return self:_getExpandedMessage('intro-fragment-noexpiry')
end
end
end


function Blurb:_makePagetypeParameter()
local function buildText(action, levelName, reason, expiry, date, pagetype)
local pagetypes = self._cfg.pagetypes
  local parts = {}
return pagetypes[self._protectionObj.title.namespace]
  local ptype = pagetype or 'page'
or pagetypes.default
  table.insert(parts, string.format("This %s is %s", ptype, levelName))
or error('no default pagetype defined', 8)
  if reason and reason ~= '' then
    table.insert(parts, string.format(" for %s", reason))
  end
  table.insert(parts, '.')
  if expiry and expiry ~= '' and expiry ~= 'indef' then
    table.insert(parts, string.format(" Protection expires: %s.", expiry))
  elseif expiry == 'indef' then
    table.insert(parts, " Protection is indefinite.")
  end
  if date and date ~= '' then
    table.insert(parts, string.format(" Protected on %s.", date))
  end
  return table.concat(parts)
end
end


function Blurb:_makeProtectionBlurbParameter()
-- Minimal args fetcher (works for template or module invocation)
local protectionBlurbs = self._cfg.protectionBlurbs
local function getArgs(frame)
local action = self._protectionObj.action
  local parent = frame:getParent()
local level = self._protectionObj.level
  local src = parent or frame
local msg
  local args = {}
if protectionBlurbs[action][level] then
  for k,v in pairs(src.args) do
msg = protectionBlurbs[action][level]
    if v ~= '' then args[k] = v end
elseif protectionBlurbs[action].default then
  end
msg = protectionBlurbs[action].default
  return args
elseif protectionBlurbs.edit.default then
msg = protectionBlurbs.edit.default
else
error('no protection blurb defined for protectionBlurbs.edit.default', 8)
end
return self:_substituteParameters(msg)
end
end


function Blurb:_makeProtectionDateParameter()
local function render(args, cfg)
local protectionDate = self._protectionObj.protectionDate
  local action = (args.action or 'edit'):lower()
if type(protectionDate) == 'number' then
  local level  = (args.level or '*'):lower()
return self:_formatDate(protectionDate)
  local reason = args[1] or args.reason
else
  local expiry = args.expiry
return protectionDate
  local date  = args.date
end
  local small  = yesno(args.small, false)
end
  local putCat = yesno(args.category, true)
  local image  = pickImage(cfg, action, level, args.image)
  local link  = args.link
  local alt    = pickLevelName(cfg, action, level)
  local padImg = fileLink(image, small and '20px' or '40px', alt, small and link or nil)


function Blurb:_makeProtectionLevelParameter()
  local out = {}
local protectionLevels = self._cfg.protectionLevels
local action = self._protectionObj.action
local level = self._protectionObj.level
local msg
if protectionLevels[action][level] then
msg = protectionLevels[action][level]
elseif protectionLevels[action].default then
msg = protectionLevels[action].default
elseif protectionLevels.edit.default then
msg = protectionLevels.edit.default
else
error('no protection level defined for protectionLevels.edit.default', 8)
end
return self:_substituteParameters(msg)
end


function Blurb:_makeProtectionLogParameter()
  if small then
local pagename = self._protectionObj.title.prefixedText
    table.insert(out, padlockIndicator(padImg, link, alt))
if self._protectionObj.action == 'autoreview' then
  else
-- We need the pending changes log.
    local text = buildText(action, alt, reason, expiry, date, args.pagetype)
return makeFullUrl(
    local head = tag('div', {style='font-weight:bold;margin-bottom:2px;'}, esc(alt:gsub("^%l", string.upper)))
'Special:Log',
    local body = tag('div', nil, esc(text))
{type = 'stable', page = pagename},
    table.insert(out, messageBox(padImg, head .. body))
self:_getExpandedMessage('pc-log-display')
  end
)
else
-- We need the protection log.
return makeFullUrl(
'Special:Log',
{type = 'protect', page = pagename},
self:_getExpandedMessage('protection-log-display')
)
end
end
 
function Blurb:_makeTalkPageParameter()
return string.format(
'[[%s:%s#%s|%s]]',
mw.site.namespaces[self._protectionObj.title.namespace].talk.name,
self._protectionObj.title.text,
self._args.section or 'top',
self:_getExpandedMessage('talk-page-link-display')
)
end


function Blurb:_makeTooltipBlurbParameter()
  if putCat and level ~= '*' then
if self._protectionObj:isTemporary() then
    table.insert(out, buildCategory(cfg, action, level))
return self:_getExpandedMessage('tooltip-blurb-expiry')
  end
else
return self:_getExpandedMessage('tooltip-blurb-noexpiry')
end
end


function Blurb:_makeTooltipFragmentParameter()
  return table.concat(out)
if self._protectionObj:isTemporary() then
return self:_getExpandedMessage('tooltip-fragment-expiry')
else
return self:_getExpandedMessage('tooltip-fragment-noexpiry')
end
end
end


function Blurb:_makeVandalTemplateParameter()
function p.main(frame)
return mw.getCurrentFrame():expandTemplate{
  local cfg = getConfig()
title="vandal-m",
  local args = getArgs(frame)
args={self._args.user or self._protectionObj.title.baseText}
  return render(args, cfg)
}
end
end


-- Public methods --
-- For module testing:
 
function p._render(targs, tcfg) return render(targs or {}, tcfg or getConfig()) end
function Blurb:makeBannerText(key)
-- Validate input.
if not key or not Blurb.bannerTextFields[key] then
error(string.format(
'"%s" is not a valid banner config field',
tostring(key)
), 2)
end
 
-- Generate the text.
local msg = self._protectionObj.bannerConfig[key]
if type(msg) == 'string' then
return self:_substituteParameters(msg)
elseif type(msg) == 'function' then
msg = msg(self._protectionObj, self._args)
if type(msg) ~= 'string' then
error(string.format(
'bad output from banner config function with key "%s"'
.. ' (expected string, got %s)',
tostring(key),
type(msg)
), 4)
end
return self:_substituteParameters(msg)
end
end
 
--------------------------------------------------------------------------------
-- BannerTemplate class
--------------------------------------------------------------------------------
 
local BannerTemplate = {}
BannerTemplate.__index = BannerTemplate
 
function BannerTemplate.new(protectionObj, cfg)
local obj = {}
obj._cfg = cfg
 
-- Set the image filename.
local imageFilename = protectionObj.bannerConfig.image
if imageFilename then
obj._imageFilename = imageFilename
else
-- If an image filename isn't specified explicitly in the banner config,
-- generate it from the protection status and the namespace.
local action = protectionObj.action
local level = protectionObj.level
local namespace = protectionObj.title.namespace
local reason = protectionObj.reason
-- Deal with special cases first.
if (
namespace == 10
or namespace == 828
or reason and obj._cfg.indefImageReasons[reason]
)
and action == 'edit'
and level == 'sysop'
and not protectionObj:isTemporary()
then
-- Fully protected modules and templates get the special red "indef"
-- padlock.
obj._imageFilename = obj._cfg.msg['image-filename-indef']
else
-- Deal with regular protection types.
local images = obj._cfg.images
if images[action] then
if images[action][level] then
obj._imageFilename = images[action][level]
elseif images[action].default then
obj._imageFilename = images[action].default
end
end
end
end
return setmetatable(obj, BannerTemplate)
end
 
function BannerTemplate:renderImage()
local filename = self._imageFilename
or self._cfg.msg['image-filename-default']
or 'Transparent.gif'
return makeFileLink{
file = filename,
size = (self.imageWidth or 20) .. 'px',
alt = self._imageAlt,
link = self._imageLink,
caption = self.imageCaption
}
end
 
--------------------------------------------------------------------------------
-- Banner class
--------------------------------------------------------------------------------
 
local Banner = setmetatable({}, BannerTemplate)
Banner.__index = Banner
 
function Banner.new(protectionObj, blurbObj, cfg)
local obj = BannerTemplate.new(protectionObj, cfg) -- This doesn't need the blurb.
obj.imageWidth = 40
obj.imageCaption = blurbObj:makeBannerText('alt') -- Large banners use the alt text for the tooltip.
obj._reasonText = blurbObj:makeBannerText('text')
obj._explanationText = blurbObj:makeBannerText('explanation')
obj._page = protectionObj.title.prefixedText -- Only makes a difference in testing.
return setmetatable(obj, Banner)
end
 
function Banner:__tostring()
-- Renders the banner.
makeMessageBox = makeMessageBox or require('Module:Message box').main
local reasonText = self._reasonText or error('no reason text set', 2)
local explanationText = self._explanationText
local mbargs = {
page = self._page,
type = 'protection',
image = self:renderImage(),
text = string.format(
"'''%s'''%s",
reasonText,
explanationText and '<br />' .. explanationText or ''
)
}
return makeMessageBox('mbox', mbargs)
end
 
--------------------------------------------------------------------------------
-- Padlock class
--------------------------------------------------------------------------------
 
local Padlock = setmetatable({}, BannerTemplate)
Padlock.__index = Padlock
 
function Padlock.new(protectionObj, blurbObj, cfg)
local obj = BannerTemplate.new(protectionObj, cfg) -- This doesn't need the blurb.
obj.imageWidth = 20
obj.imageCaption = blurbObj:makeBannerText('tooltip')
obj._imageAlt = blurbObj:makeBannerText('alt')
obj._imageLink = blurbObj:makeBannerText('link')
obj._indicatorName = cfg.padlockIndicatorNames[protectionObj.action]
or cfg.padlockIndicatorNames.default
or 'pp-default'
return setmetatable(obj, Padlock)
end
 
function Padlock:__tostring()
local frame = mw.getCurrentFrame()
-- The nowiki tag helps prevent whitespace at the top of articles.
return frame:extensionTag{name = 'nowiki'} .. frame:extensionTag{
name = 'indicator',
args = {name = self._indicatorName},
content = self:renderImage()
}
end
 
--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------
 
local p = {}
 
function p._exportClasses()
-- This is used for testing purposes.
return {
Protection = Protection,
Blurb = Blurb,
BannerTemplate = BannerTemplate,
Banner = Banner,
Padlock = Padlock,
}
end
 
function p._main(args, cfg, title)
args = args or {}
cfg = cfg or require(CONFIG_MODULE)
 
local protectionObj = Protection.new(args, cfg, title)
 
local ret = {}
 
-- If a page's edit protection is equally or more restrictive than its
-- protection from some other action, then don't bother displaying anything
-- for the other action (except categories).
if not yesno(args.catonly) and (protectionObj.action == 'edit' or
args.demolevel or
not getReachableNodes(
cfg.hierarchy,
protectionObj.level
)[effectiveProtectionLevel('edit', protectionObj.title)])
then
-- Initialise the blurb object
local blurbObj = Blurb.new(protectionObj, args, cfg)
-- Render the banner
if protectionObj:shouldShowLock() then
ret[#ret + 1] = tostring(
(yesno(args.small) and Padlock or Banner)
.new(protectionObj, blurbObj, cfg)
)
end
end
 
-- Render the categories
if yesno(args.category) ~= false then
ret[#ret + 1] = protectionObj:makeCategoryLinks()
end
-- For arbitration enforcement, flagging [[WP:PIA]] pages to enable [[Special:AbuseFilter/1339]] to flag edits to them
if protectionObj.level == "extendedconfirmed" then
if require("Module:TableTools").inArray(protectionObj.title.talkPageTitle.categories, "Wikipedia pages subject to the extended confirmed restriction related to the Arab-Israeli conflict") then
ret[#ret + 1] = "<p class='PIA-flag' style='display:none; visibility:hidden;' title='This page is subject to the extended confirmed restriction related to the Arab-Israeli conflict.'></p>"
end
end
return table.concat(ret)
end
 
function p.main(frame, cfg)
cfg = cfg or require(CONFIG_MODULE)
 
-- Find default args, if any.
local parent = frame.getParent and frame:getParent()
local defaultArgs = parent and cfg.wrappers[parent:getTitle():gsub('/sandbox$', '')]
 
-- Find user args, and use the parent frame if we are being called from a
-- wrapper template.
getArgs = getArgs or require('Module:Arguments').getArgs
local userArgs = getArgs(frame, {
parentOnly = defaultArgs,
frameOnly = not defaultArgs
})
 
-- Build the args table. User-specified args overwrite default args.
local args = {}
for k, v in pairs(defaultArgs or {}) do
args[k] = v
end
for k, v in pairs(userArgs) do
args[k] = v
end
return p._main(args, cfg)
end


return p
return p

Latest revision as of 07:34, 29 September 2025

This page documents the Chalo Chatu–only protection banner module. It provides a lightweight, dependency-free implementation for protection notices and padlock indicators. It is declarative: pages call the module (usually via Template:PP-meta) and pass action, level, reason, and dates. The module does not query actual MediaWiki protection state.

Overview

  • Designed for Chalo Chatu only
  • No external dependencies or WMF modules
  • Outputs either a full banner or a small padlock
  • Adds simple, predictable protection categories
  • Icons and category names configurable in Module:Protection banner/config

How it works

Most pages should not call the module directly. Use the wrapper template:

  • Template:PP-meta invokes the module with your parameters
  • Module:Protection banner renders the banner/padlock and adds categories

Quick start (recommended wrapper)

Place this in the article or template needing a notice:

{{pp-meta|vandalism|action=edit|level=autoconfirmed|expiry=indef|date=2025-09-29}}

Small padlock indicator:

{{pp-meta|dispute|action=edit|level=sysop|small=yes|link=Chalo Chatu:Protection policy}}

Parameters (via Template:PP-meta)

All parameters are passed through to the module.

1 or reason
Free-text reason shown in the banner line. Example: vandalism, dispute, sock.
action
edit (default), move, upload.
level
sysop, templateeditor, extendedconfirmed, autoconfirmed, * (unprotected). Determines icon, wording, and category.
expiry
indef for indefinite; or free text such as 2026-12-31. Shown as provided.
date
Free text such as 2025-09-29. Shown as Protected on 2025-09-29.
small
yes to output a small padlock indicator instead of a full banner.
category
yes (default) to add a protection category; no to suppress.
image
Optional file name override for the padlock icon. Example: Padlock-custom.svg
link
Optional page to link the padlock to when small=yes. Example: Chalo Chatu:Protection policy
pagetype
Defaults to page. Used in phrasing (This page is semi-protected...). You may set article, template, module, etc.

Levels and default wording

The module maps levels to human-readable phrases.

edit/sysop
fully protected
edit/templateeditor
template-protected
edit/extendedconfirmed
extended confirmed protected
edit/autoconfirmed
semi-protected
*
unprotected

Move and upload actions use simple default wording (move-protected, upload-protected).

Categories

When category=yes and level is not *, the module adds one category based on action/level. Defaults are set in Module:Protection banner/config:

  • Fully protected pages
  • Template-protected pages
  • Extended-confirmed protected pages
  • Semi-protected pages
  • Move-protected pages
  • Upload-protected pages
  • Protected pages (fallback)

Rename or localize categories by editing Module:Protection banner/config.

Icons

Icons are file names stored in Module:Protection banner/config. Replace with your local files as needed:

  • Padlock-red.svg, Padlock-orange.svg, Padlock-amber.svg, Padlock-yellow.svg, Padlock-blue.svg, Padlock-grey.svg

You can override per-call with image=.

Examples

Full banner, indefinite semi-protection for vandalism:

{{pp-meta|vandalism|action=edit|level=autoconfirmed|expiry=indef|date=2025-09-29}}

Full banner, extended-confirmed protection with expiry:

{{pp-meta|dispute|action=edit|level=extendedconfirmed|expiry=2026-06-30}}

Small padlock on a template:

{{pp-meta|reason=high-risk template|action=edit|level=templateeditor|small=yes|link=Chalo Chatu:Template protection}}

Suppress category (maintenance only):

{{pp-meta|reason=test|action=edit|level=autoconfirmed|category=no}}

Daughter templates (optional)

You may create simple wrappers that prefill parameters, for example:

Template:PP-vandalism

<includeonly>{{pp-meta|vandalism|action={{{
action|edit}}}|level={{{level|autoconfirmed}}}|expiry={{{expiry|indef}}}|small={{{small|}}}|date={{{date|}}}}}</includeonly>
<noinclude>Wrapper for vandalism protection. [[Category:Protection templates]]</noinclude>

Template:PP-dispute

<includeonly>{{pp-meta|dispute|action={{{
action|edit}}}|level={{{level|extendedconfirmed}}}|expiry={{{expiry|}}}|small={{{small|}}}|date={{{date|}}}}}</includeonly>
<noinclude>Wrapper for dispute-driven protection. [[Category:Protection templates]]</noinclude>

Configuration

Site-wide configuration lives in Module:Protection banner/config:

  • images: per-action, per-level icon filenames
  • protectionLevels: phrases used in banner text
  • categories: mapping from action/level to category names

Update that file to adjust look, wording, and categories globally.

Limitations

  • Declarative only; does not read MediaWiki protection state
  • Age and date strings are displayed as provided; no automatic date parsing
  • Minimal CSS. For consistent styling, add optional CSS in MediaWiki:Common.css for .cc-mbox and .cc-padlock

See also


-- Module:Protection banner (Chalo Chatu)
-- Lightweight, dependency-free. Declarative protection only.
-- Implements {{pp-meta}}-style usage via p.main().
-- Params (typical):
--   action=edit/move/upload
--   level=autoconfirmed/extendedconfirmed/templateeditor/sysop/* (unprotected)
--   reason=vandalism/dispute/sock/etc (free text)
--   expiry=indef | YYYY-MM-DD | free text (shown as-is)
--   date=YYYY-MM-DD (shown as "Protected on …")
--   small=yes -> padlock indicator; else banner
--   category=yes/no (default yes)
--   section=anchor on talk link (optional)
--   image=File name override (optional)
--   link=target page for padlock click (optional)

local p = {}

-- Basic yes/no parser
local function yesno(v, default)
  if v == nil then return default end
  if type(v) == 'boolean' then return v end
  local s = tostring(v):lower()
  if s == 'yes' or s == 'y' or s == 'true' or s == '1' then return true end
  if s == 'no'  or s == 'n' or s == 'false' or s == '0' then return false end
  return default
end

-- HTML builder helpers (simple, safe)
local function esc(s)
  s = tostring(s or '')
  return s:gsub('&','&amp;'):gsub('<','&lt;'):gsub('>','&gt;')
end

local function tag(name, attrs, content)
  local a = {}
  if attrs then
    for k,v in pairs(attrs) do
      if v ~= nil and v ~= '' then
        table.insert(a, string.format(' %s="%s"', k, esc(v)))
      end
    end
  end
  if content == nil then
    return string.format('<%s%s />', name, table.concat(a))
  end
  return string.format('<%s%s>%s</%s>', name, table.concat(a), content, name)
end

-- Minimal styling (relies on site Common.css, but works standalone)
local function messageBox(imageHtml, textHtml)
  local left = tag('div', {style='flex:0 0 auto; padding-right:8px;'}, imageHtml or '')
  local right = tag('div', {style='flex:1 1 auto;'}, textHtml or '')
  local row = tag('div', {style='display:flex; align-items:flex-start;'}, left .. right)
  return tag('div', {
    class='cc-mbox cc-mbox-protection',
    style='border:1px solid #aaa;background:#f9f9f9;padding:8px;margin:0 0 1em 0;'
  }, row)
end

local function padlockIndicator(imageHtml, linkTarget, alt)
  local content = imageHtml or ''
  if linkTarget and linkTarget ~= '' then
    content = string.format('[[%s|%s]]', linkTarget, content)
  end
  return tag('span', {class='cc-padlock', title=alt or 'Page is protected'}, content)
end

local function fileLink(filename, size, alt, link)
  if not filename or filename == '' then return '' end
  local core = string.format('[[File:%s|%s|%s]]',
    filename,
    size or '20px',
    (alt and ('alt='..alt)) or ''
  )
  if link and link ~= '' then
    return string.format('[[%s|%s]]', link, core)
  end
  return core
end

-- Default config (overrideable by Module:Protection banner/config if present)
local function getConfig()
  local ok, cfg = pcall(require, 'Module:Protection banner/config')
  if ok and type(cfg) == 'table' then return cfg end
  -- Built-in defaults for Chalo Chatu
  return {
    images = {
      edit = {
        sysop = 'Padlock-red.svg',
        templateeditor = 'Padlock-orange.svg',
        extendedconfirmed = 'Padlock-amber.svg',
        autoconfirmed = 'Padlock-yellow.svg',
        ['*'] = 'Padlock-grey.svg',
        default = 'Padlock-blue.svg'
      },
      move = { default = 'Padlock-blue.svg' },
      upload = { default = 'Padlock-blue.svg' },
      default = 'Padlock-blue.svg'
    },
    padlockIndicatorName = 'pp',
    -- Simple phrasing tables
    protectionLevels = {
      edit = {
        sysop='fully protected', templateeditor='template-protected',
        extendedconfirmed='extended confirmed protected',
        autoconfirmed='semi-protected', ['*']='unprotected', default='protected'
      },
      move = { default='move-protected' },
      upload = { default='upload-protected' }
    },
    categories = {
      -- Built from: action/level -> category name. Fallbacks used below.
      ['edit/sysop'] = 'Fully protected pages',
      ['edit/autoconfirmed'] = 'Semi-protected pages',
      ['edit/extendedconfirmed'] = 'Extended-confirmed protected pages',
      ['edit/templateeditor'] = 'Template-protected pages',
      ['move/sysop'] = 'Move-protected pages',
      ['upload/sysop'] = 'Upload-protected pages',
      default = 'Protected pages'
    },
    talkNsName = 'Talk'
  }
end

local function pickImage(cfg, action, level, override)
  if override and override ~= '' then return override end
  local imgs = cfg.images
  if imgs[action] then
    return imgs[action][level] or imgs[action].default or imgs.default
  end
  return imgs.default
end

local function pickLevelName(cfg, action, level)
  local t = cfg.protectionLevels[action] or {}
  return t[level] or t.default or 'protected'
end

local function buildCategory(cfg, action, level)
  local key = (action or 'edit') .. '/' .. (level or '*')
  local cat = cfg.categories[key] or cfg.categories.default
  return string.format('[[Category:%s]]', cat)
end

local function buildText(action, levelName, reason, expiry, date, pagetype)
  local parts = {}
  local ptype = pagetype or 'page'
  table.insert(parts, string.format("This %s is %s", ptype, levelName))
  if reason and reason ~= '' then
    table.insert(parts, string.format(" for %s", reason))
  end
  table.insert(parts, '.')
  if expiry and expiry ~= '' and expiry ~= 'indef' then
    table.insert(parts, string.format(" Protection expires: %s.", expiry))
  elseif expiry == 'indef' then
    table.insert(parts, " Protection is indefinite.")
  end
  if date and date ~= '' then
    table.insert(parts, string.format(" Protected on %s.", date))
  end
  return table.concat(parts)
end

-- Minimal args fetcher (works for template or module invocation)
local function getArgs(frame)
  local parent = frame:getParent()
  local src = parent or frame
  local args = {}
  for k,v in pairs(src.args) do
    if v ~= '' then args[k] = v end
  end
  return args
end

local function render(args, cfg)
  local action = (args.action or 'edit'):lower()
  local level  = (args.level or '*'):lower()
  local reason = args[1] or args.reason
  local expiry = args.expiry
  local date   = args.date
  local small  = yesno(args.small, false)
  local putCat = yesno(args.category, true)
  local image  = pickImage(cfg, action, level, args.image)
  local link   = args.link
  local alt    = pickLevelName(cfg, action, level)
  local padImg = fileLink(image, small and '20px' or '40px', alt, small and link or nil)

  local out = {}

  if small then
    table.insert(out, padlockIndicator(padImg, link, alt))
  else
    local text = buildText(action, alt, reason, expiry, date, args.pagetype)
    local head = tag('div', {style='font-weight:bold;margin-bottom:2px;'}, esc(alt:gsub("^%l", string.upper)))
    local body = tag('div', nil, esc(text))
    table.insert(out, messageBox(padImg, head .. body))
  end

  if putCat and level ~= '*' then
    table.insert(out, buildCategory(cfg, action, level))
  end

  return table.concat(out)
end

function p.main(frame)
  local cfg = getConfig()
  local args = getArgs(frame)
  return render(args, cfg)
end

-- For module testing:
function p._render(targs, tcfg) return render(targs or {}, tcfg or getConfig()) end

return p