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

  1. -- This module implements {{pp-meta}} and its daughter templates such as
  2. -- {{pp-dispute}}, {{pp-vandalism}} and {{pp-sock}}.
  3.  
  4. -- Initialise necessary modules.
  5. require('Module:No globals')
  6. local makeFileLink = require('Module:File link')._main
  7. local effectiveProtectionLevel = require('Module:Effective protection level')._main
  8. local yesno = require('Module:Yesno')
  9.  
  10. -- Lazily initialise modules and objects we don't always need.
  11. local getArgs, makeMessageBox, lang
  12.  
  13. -- Set constants.
  14. local CONFIG_MODULE = 'Module:Protection banner/config'
  15.  
  16. --------------------------------------------------------------------------------
  17. -- Helper functions
  18. --------------------------------------------------------------------------------
  19.  
  20. local function makeCategoryLink(cat, sort)
  21. local nsText = mw.site.namespaces[14].name
  22. if cat and sort then
  23. return string.format(
  24. '[[%s:%s|%s]]',
  25. nsText,
  26. cat,
  27. sort
  28. )
  29. elseif cat then
  30. return string.format(
  31. '[[%s:%s]]',
  32. nsText,
  33. cat
  34. )
  35. else
  36. return ''
  37. end
  38. end
  39.  
  40. -- Validation function for the expiry and the protection date
  41. local function validateDate(dateString, dateType)
  42. lang = lang or mw.language.getContentLanguage()
  43. local success, result = pcall(lang.formatDate, lang, 'U', dateString)
  44. if success then
  45. result = tonumber(result)
  46. if result then
  47. return result
  48. end
  49. end
  50. error(string.format(
  51. 'invalid %s ("%s")',
  52. dateType,
  53. tostring(dateString)
  54. ), 4)
  55. end
  56.  
  57. local function makeFullUrl(page, query, display)
  58. return string.format(
  59. '[%s %s]',
  60. tostring(mw.uri.fullUrl(page, query)),
  61. display
  62. )
  63. end
  64.  
  65. local function toTableEnd(t, pos)
  66. -- Sends the value at position pos to the end of array t, and shifts the
  67. -- other items down accordingly.
  68. return table.insert(t, table.remove(t, pos))
  69. end
  70.  
  71. local function walkHierarchy(hierarchy, start)
  72. local toWalk, retval = {[start] = true}, {}
  73. while true do
  74. -- Can't use pairs() since we're adding and removing things as we're iterating
  75. local k = next(toWalk)
  76. if k == nil then break end
  77. toWalk[k] = nil
  78. retval[k] = true
  79. for _,v in ipairs(hierarchy[k]) do
  80. if not retval[v] then
  81. toWalk[v] = true
  82. end
  83. end
  84. end
  85. return retval
  86. end
  87.  
  88. --------------------------------------------------------------------------------
  89. -- Protection class
  90. --------------------------------------------------------------------------------
  91.  
  92. local Protection = {}
  93. Protection.__index = Protection
  94.  
  95. Protection.supportedActions = {
  96. edit = true,
  97. move = true,
  98. autoreview = true
  99. }
  100.  
  101. Protection.bannerConfigFields = {
  102. 'text',
  103. 'explanation',
  104. 'tooltip',
  105. 'alt',
  106. 'link',
  107. 'image'
  108. }
  109.  
  110. function Protection.new(args, cfg, title)
  111. local obj = {}
  112. obj._cfg = cfg
  113. obj.title = title or mw.title.getCurrentTitle()
  114.  
  115. -- Set action
  116. if not args.action then
  117. obj.action = 'edit'
  118. elseif Protection.supportedActions[args.action] then
  119. obj.action = args.action
  120. else
  121. error(string.format(
  122. 'invalid action ("%s")',
  123. tostring(args.action)
  124. ), 3)
  125. end
  126.  
  127. -- Set level
  128. obj.level = args.demolevel or effectiveProtectionLevel(obj.action, obj.title)
  129. if not obj.level or (obj.action == 'move' and obj.level == 'autoconfirmed') then
  130. -- Users need to be autoconfirmed to move pages anyway, so treat
  131. -- semi-move-protected pages as unprotected.
  132. obj.level = '*'
  133. end
  134.  
  135. -- Set expiry
  136. if args.expiry then
  137. if cfg.indefStrings[args.expiry] then
  138. obj.expiry = 'indef'
  139. elseif type(args.expiry) == 'number' then
  140. obj.expiry = args.expiry
  141. else
  142. obj.expiry = validateDate(args.expiry, 'expiry date')
  143. end
  144. end
  145.  
  146. -- Set reason
  147. if args[1] then
  148. obj.reason = mw.ustring.lower(args[1])
  149. if obj.reason:find('|') then
  150. error('reasons cannot contain the pipe character ("|")', 3)
  151. end
  152. end
  153.  
  154. -- Set protection date
  155. if args.date then
  156. obj.protectionDate = validateDate(args.date, 'protection date')
  157. end
  158. -- Set banner config
  159. do
  160. obj.bannerConfig = {}
  161. local configTables = {}
  162. if cfg.banners[obj.action] then
  163. configTables[#configTables + 1] = cfg.banners[obj.action][obj.reason]
  164. end
  165. if cfg.defaultBanners[obj.action] then
  166. configTables[#configTables + 1] = cfg.defaultBanners[obj.action][obj.level]
  167. configTables[#configTables + 1] = cfg.defaultBanners[obj.action].default
  168. end
  169. configTables[#configTables + 1] = cfg.masterBanner
  170. for i, field in ipairs(Protection.bannerConfigFields) do
  171. for j, t in ipairs(configTables) do
  172. if t[field] then
  173. obj.bannerConfig[field] = t[field]
  174. break
  175. end
  176. end
  177. end
  178. end
  179. return setmetatable(obj, Protection)
  180. end
  181.  
  182. function Protection:isProtected()
  183. return self.level ~= '*'
  184. end
  185.  
  186. function Protection:isTemporary()
  187. return type(self.expiry) == 'number'
  188. end
  189.  
  190. function Protection:makeProtectionCategory()
  191. local cfg = self._cfg
  192. local title = self.title
  193. -- Exit if the page is not protected.
  194. if not self:isProtected() then
  195. return ''
  196. end
  197. -- Get the expiry key fragment.
  198. local expiryFragment
  199. if self.expiry == 'indef' then
  200. expiryFragment = self.expiry
  201. elseif type(self.expiry) == 'number' then
  202. expiryFragment = 'temp'
  203. end
  204.  
  205. -- Get the namespace key fragment.
  206. local namespaceFragment
  207. do
  208. namespaceFragment = cfg.categoryNamespaceKeys[title.namespace]
  209. if not namespaceFragment and title.namespace % 2 == 1 then
  210. namespaceFragment = 'talk'
  211. end
  212. end
  213. -- Define the order that key fragments are tested in. This is done with an
  214. -- array of tables containing the value to be tested, along with its
  215. -- position in the cfg.protectionCategories table.
  216. local order = {
  217. {val = expiryFragment, keypos = 1},
  218. {val = namespaceFragment, keypos = 2},
  219. {val = self.reason, keypos = 3},
  220. {val = self.level, keypos = 4},
  221. {val = self.action, keypos = 5}
  222. }
  223.  
  224. --[[
  225. -- The old protection templates used an ad-hoc protection category system,
  226. -- with some templates prioritising namespaces in their categories, and
  227. -- others prioritising the protection reason. To emulate this in this module
  228. -- we use the config table cfg.reasonsWithNamespacePriority to set the
  229. -- reasons for which namespaces have priority over protection reason.
  230. -- If we are dealing with one of those reasons, move the namespace table to
  231. -- the end of the order table, i.e. give it highest priority. If not, the
  232. -- reason should have highest priority, so move that to the end of the table
  233. -- instead.
  234. --]]
  235. if self.reason and cfg.reasonsWithNamespacePriority[self.reason] then
  236. -- table.insert(order, 3, table.remove(order, 2))
  237. toTableEnd(order, 2)
  238. else
  239. toTableEnd(order, 3)
  240. end
  241. --[[
  242. -- Define the attempt order. Inactive subtables (subtables with nil "value"
  243. -- fields) are moved to the end, where they will later be given the key
  244. -- "all". This is to cut down on the number of table lookups in
  245. -- cfg.protectionCategories, which grows exponentially with the number of
  246. -- non-nil keys. We keep track of the number of active subtables with the
  247. -- noActive parameter.
  248. --]]
  249. local noActive, attemptOrder
  250. do
  251. local active, inactive = {}, {}
  252. for i, t in ipairs(order) do
  253. if t.val then
  254. active[#active + 1] = t
  255. else
  256. inactive[#inactive + 1] = t
  257. end
  258. end
  259. noActive = #active
  260. attemptOrder = active
  261. for i, t in ipairs(inactive) do
  262. attemptOrder[#attemptOrder + 1] = t
  263. end
  264. end
  265. --[[
  266. -- Check increasingly generic key combinations until we find a match. If a
  267. -- specific category exists for the combination of key fragments we are
  268. -- given, that match will be found first. If not, we keep trying different
  269. -- key fragment combinations until we match using the key
  270. -- "all-all-all-all-all".
  271. --
  272. -- To generate the keys, we index the key subtables using a binary matrix
  273. -- with indexes i and j. j is only calculated up to the number of active
  274. -- subtables. For example, if there were three active subtables, the matrix
  275. -- would look like this, with 0 corresponding to the key fragment "all", and
  276. -- 1 corresponding to other key fragments.
  277. --
  278. -- j 1 2 3
  279. -- i
  280. -- 1 1 1 1
  281. -- 2 0 1 1
  282. -- 3 1 0 1
  283. -- 4 0 0 1
  284. -- 5 1 1 0
  285. -- 6 0 1 0
  286. -- 7 1 0 0
  287. -- 8 0 0 0
  288. --
  289. -- Values of j higher than the number of active subtables are set
  290. -- to the string "all".
  291. --
  292. -- A key for cfg.protectionCategories is constructed for each value of i.
  293. -- The position of the value in the key is determined by the keypos field in
  294. -- each subtable.
  295. --]]
  296. local cats = cfg.protectionCategories
  297. for i = 1, 2^noActive do
  298. local key = {}
  299. for j, t in ipairs(attemptOrder) do
  300. if j > noActive then
  301. key[t.keypos] = 'all'
  302. else
  303. local quotient = i / 2 ^ (j - 1)
  304. quotient = math.ceil(quotient)
  305. if quotient % 2 == 1 then
  306. key[t.keypos] = t.val
  307. else
  308. key[t.keypos] = 'all'
  309. end
  310. end
  311. end
  312. key = table.concat(key, '|')
  313. local attempt = cats[key]
  314. if attempt then
  315. return makeCategoryLink(attempt, title.text)
  316. end
  317. end
  318. return ''
  319. end
  320.  
  321. function Protection:needsExpiry()
  322. local cfg = self._cfg
  323. local actionNeedsCheck = cfg.expiryCheckActions[self.action]
  324. return not self.expiry and (
  325. actionNeedsCheck or (
  326. actionNeedsCheck == nil
  327. and self.reason -- the old {{pp-protected}} didn't check for expiry
  328. and not cfg.reasonsWithoutExpiryCheck[self.reason]
  329. )
  330. )
  331. end
  332.  
  333. function Protection:isIncorrect()
  334. local expiry = self.expiry
  335. return not self:isProtected()
  336. or type(expiry) == 'number' and expiry < os.time()
  337. end
  338.  
  339. function Protection:isTemplateProtectedNonTemplate()
  340. local action, namespace = self.action, self.title.namespace
  341. return self.level == 'templateeditor'
  342. and (
  343. (action ~= 'edit' and action ~= 'move')
  344. or (namespace ~= 10 and namespace ~= 828)
  345. )
  346. end
  347.  
  348. function Protection:makeCategoryLinks()
  349. local msg = self._cfg.msg
  350. local ret = { self:makeProtectionCategory() }
  351. if self:needsExpiry() then
  352. ret[#ret + 1] = makeCategoryLink(
  353. msg['tracking-category-expiry'],
  354. self.title.text
  355. )
  356. end
  357. if self:isIncorrect() then
  358. ret[#ret + 1] = makeCategoryLink(
  359. msg['tracking-category-incorrect'],
  360. self.title.text
  361. )
  362. end
  363. if self:isTemplateProtectedNonTemplate() then
  364. ret[#ret + 1] = makeCategoryLink(
  365. msg['tracking-category-template'],
  366. self.title.text
  367. )
  368. end
  369. return table.concat(ret)
  370. end
  371.  
  372. --------------------------------------------------------------------------------
  373. -- Blurb class
  374. --------------------------------------------------------------------------------
  375.  
  376. local Blurb = {}
  377. Blurb.__index = Blurb
  378.  
  379. Blurb.bannerTextFields = {
  380. text = true,
  381. explanation = true,
  382. tooltip = true,
  383. alt = true,
  384. link = true
  385. }
  386.  
  387. function Blurb.new(protectionObj, args, cfg)
  388. return setmetatable({
  389. _cfg = cfg,
  390. _protectionObj = protectionObj,
  391. _args = args
  392. }, Blurb)
  393. end
  394.  
  395. -- Private methods --
  396.  
  397. function Blurb:_formatDate(num)
  398. -- Formats a Unix timestamp into dd Month, YYYY format.
  399. lang = lang or mw.language.getContentLanguage()
  400. local success, date = pcall(
  401. lang.formatDate,
  402. lang,
  403. self._cfg.msg['expiry-date-format'] or 'j F Y',
  404. '@' .. tostring(num)
  405. )
  406. if success then
  407. return date
  408. end
  409. end
  410.  
  411. function Blurb:_getExpandedMessage(msgKey)
  412. return self:_substituteParameters(self._cfg.msg[msgKey])
  413. end
  414.  
  415. function Blurb:_substituteParameters(msg)
  416. if not self._params then
  417. local parameterFuncs = {}
  418.  
  419. parameterFuncs.CURRENTVERSION = self._makeCurrentVersionParameter
  420. parameterFuncs.EDITREQUEST = self._makeEditRequestParameter
  421. parameterFuncs.EXPIRY = self._makeExpiryParameter
  422. parameterFuncs.EXPLANATIONBLURB = self._makeExplanationBlurbParameter
  423. parameterFuncs.IMAGELINK = self._makeImageLinkParameter
  424. parameterFuncs.INTROBLURB = self._makeIntroBlurbParameter
  425. parameterFuncs.INTROFRAGMENT = self._makeIntroFragmentParameter
  426. parameterFuncs.PAGETYPE = self._makePagetypeParameter
  427. parameterFuncs.PROTECTIONBLURB = self._makeProtectionBlurbParameter
  428. parameterFuncs.PROTECTIONDATE = self._makeProtectionDateParameter
  429. parameterFuncs.PROTECTIONLEVEL = self._makeProtectionLevelParameter
  430. parameterFuncs.PROTECTIONLOG = self._makeProtectionLogParameter
  431. parameterFuncs.TALKPAGE = self._makeTalkPageParameter
  432. parameterFuncs.TOOLTIPBLURB = self._makeTooltipBlurbParameter
  433. parameterFuncs.TOOLTIPFRAGMENT = self._makeTooltipFragmentParameter
  434. parameterFuncs.VANDAL = self._makeVandalTemplateParameter
  435. self._params = setmetatable({}, {
  436. __index = function (t, k)
  437. local param
  438. if parameterFuncs[k] then
  439. param = parameterFuncs[k](self)
  440. end
  441. param = param or ''
  442. t[k] = param
  443. return param
  444. end
  445. })
  446. end
  447. msg = msg:gsub('${(%u+)}', self._params)
  448. return msg
  449. end
  450.  
  451. function Blurb:_makeCurrentVersionParameter()
  452. -- A link to the page history or the move log, depending on the kind of
  453. -- protection.
  454. local pagename = self._protectionObj.title.prefixedText
  455. if self._protectionObj.action == 'move' then
  456. -- We need the move log link.
  457. return makeFullUrl(
  458. 'Special:Log',
  459. {type = 'move', page = pagename},
  460. self:_getExpandedMessage('current-version-move-display')
  461. )
  462. else
  463. -- We need the history link.
  464. return makeFullUrl(
  465. pagename,
  466. {action = 'history'},
  467. self:_getExpandedMessage('current-version-edit-display')
  468. )
  469. end
  470. end
  471.  
  472. function Blurb:_makeEditRequestParameter()
  473. local mEditRequest = require('Module:Submit an edit request')
  474. local action = self._protectionObj.action
  475. local level = self._protectionObj.level
  476. -- Get the edit request type.
  477. local requestType
  478. if action == 'edit' then
  479. if level == 'autoconfirmed' then
  480. requestType = 'semi'
  481. elseif level == 'templateeditor' then
  482. requestType = 'template'
  483. end
  484. end
  485. requestType = requestType or 'full'
  486. -- Get the display value.
  487. local display = self:_getExpandedMessage('edit-request-display')
  488.  
  489. return mEditRequest._link{type = requestType, display = display}
  490. end
  491.  
  492. function Blurb:_makeExpiryParameter()
  493. local expiry = self._protectionObj.expiry
  494. if type(expiry) == 'number' then
  495. return self:_formatDate(expiry)
  496. else
  497. return expiry
  498. end
  499. end
  500.  
  501. function Blurb:_makeExplanationBlurbParameter()
  502. -- Cover special cases first.
  503. if self._protectionObj.title.namespace == 8 then
  504. -- MediaWiki namespace
  505. return self:_getExpandedMessage('explanation-blurb-nounprotect')
  506. end
  507.  
  508. -- Get explanation blurb table keys
  509. local action = self._protectionObj.action
  510. local level = self._protectionObj.level
  511. local talkKey = self._protectionObj.title.isTalkPage and 'talk' or 'subject'
  512.  
  513. -- Find the message in the explanation blurb table and substitute any
  514. -- parameters.
  515. local explanations = self._cfg.explanationBlurbs
  516. local msg
  517. if explanations[action][level] and explanations[action][level][talkKey] then
  518. msg = explanations[action][level][talkKey]
  519. elseif explanations[action][level] and explanations[action][level].default then
  520. msg = explanations[action][level].default
  521. elseif explanations[action].default and explanations[action].default[talkKey] then
  522. msg = explanations[action].default[talkKey]
  523. elseif explanations[action].default and explanations[action].default.default then
  524. msg = explanations[action].default.default
  525. else
  526. error(string.format(
  527. 'could not find explanation blurb for action "%s", level "%s" and talk key "%s"',
  528. action,
  529. level,
  530. talkKey
  531. ), 8)
  532. end
  533. return self:_substituteParameters(msg)
  534. end
  535.  
  536. function Blurb:_makeImageLinkParameter()
  537. local imageLinks = self._cfg.imageLinks
  538. local action = self._protectionObj.action
  539. local level = self._protectionObj.level
  540. local msg
  541. if imageLinks[action][level] then
  542. msg = imageLinks[action][level]
  543. elseif imageLinks[action].default then
  544. msg = imageLinks[action].default
  545. else
  546. msg = imageLinks.edit.default
  547. end
  548. return self:_substituteParameters(msg)
  549. end
  550.  
  551. function Blurb:_makeIntroBlurbParameter()
  552. if self._protectionObj:isTemporary() then
  553. return self:_getExpandedMessage('intro-blurb-expiry')
  554. else
  555. return self:_getExpandedMessage('intro-blurb-noexpiry')
  556. end
  557. end
  558.  
  559. function Blurb:_makeIntroFragmentParameter()
  560. if self._protectionObj:isTemporary() then
  561. return self:_getExpandedMessage('intro-fragment-expiry')
  562. else
  563. return self:_getExpandedMessage('intro-fragment-noexpiry')
  564. end
  565. end
  566.  
  567. function Blurb:_makePagetypeParameter()
  568. local pagetypes = self._cfg.pagetypes
  569. return pagetypes[self._protectionObj.title.namespace]
  570. or pagetypes.default
  571. or error('no default pagetype defined', 8)
  572. end
  573.  
  574. function Blurb:_makeProtectionBlurbParameter()
  575. local protectionBlurbs = self._cfg.protectionBlurbs
  576. local action = self._protectionObj.action
  577. local level = self._protectionObj.level
  578. local msg
  579. if protectionBlurbs[action][level] then
  580. msg = protectionBlurbs[action][level]
  581. elseif protectionBlurbs[action].default then
  582. msg = protectionBlurbs[action].default
  583. elseif protectionBlurbs.edit.default then
  584. msg = protectionBlurbs.edit.default
  585. else
  586. error('no protection blurb defined for protectionBlurbs.edit.default', 8)
  587. end
  588. return self:_substituteParameters(msg)
  589. end
  590.  
  591. function Blurb:_makeProtectionDateParameter()
  592. local protectionDate = self._protectionObj.protectionDate
  593. if type(protectionDate) == 'number' then
  594. return self:_formatDate(protectionDate)
  595. else
  596. return protectionDate
  597. end
  598. end
  599.  
  600. function Blurb:_makeProtectionLevelParameter()
  601. local protectionLevels = self._cfg.protectionLevels
  602. local action = self._protectionObj.action
  603. local level = self._protectionObj.level
  604. local msg
  605. if protectionLevels[action][level] then
  606. msg = protectionLevels[action][level]
  607. elseif protectionLevels[action].default then
  608. msg = protectionLevels[action].default
  609. elseif protectionLevels.edit.default then
  610. msg = protectionLevels.edit.default
  611. else
  612. error('no protection level defined for protectionLevels.edit.default', 8)
  613. end
  614. return self:_substituteParameters(msg)
  615. end
  616.  
  617. function Blurb:_makeProtectionLogParameter()
  618. local pagename = self._protectionObj.title.prefixedText
  619. if self._protectionObj.action == 'autoreview' then
  620. -- We need the pending changes log.
  621. return makeFullUrl(
  622. 'Special:Log',
  623. {type = 'stable', page = pagename},
  624. self:_getExpandedMessage('pc-log-display')
  625. )
  626. else
  627. -- We need the protection log.
  628. return makeFullUrl(
  629. 'Special:Log',
  630. {type = 'protect', page = pagename},
  631. self:_getExpandedMessage('protection-log-display')
  632. )
  633. end
  634. end
  635.  
  636. function Blurb:_makeTalkPageParameter()
  637. return string.format(
  638. '[[%s:%s#%s|%s]]',
  639. mw.site.namespaces[self._protectionObj.title.namespace].talk.name,
  640. self._protectionObj.title.text,
  641. self._args.section or 'top',
  642. self:_getExpandedMessage('talk-page-link-display')
  643. )
  644. end
  645.  
  646. function Blurb:_makeTooltipBlurbParameter()
  647. if self._protectionObj:isTemporary() then
  648. return self:_getExpandedMessage('tooltip-blurb-expiry')
  649. else
  650. return self:_getExpandedMessage('tooltip-blurb-noexpiry')
  651. end
  652. end
  653.  
  654. function Blurb:_makeTooltipFragmentParameter()
  655. if self._protectionObj:isTemporary() then
  656. return self:_getExpandedMessage('tooltip-fragment-expiry')
  657. else
  658. return self:_getExpandedMessage('tooltip-fragment-noexpiry')
  659. end
  660. end
  661.  
  662. function Blurb:_makeVandalTemplateParameter()
  663. return require('Module:Vandal-m')._main{
  664. self._args.user or self._protectionObj.title.baseText
  665. }
  666. end
  667.  
  668. -- Public methods --
  669.  
  670. function Blurb:makeBannerText(key)
  671. -- Validate input.
  672. if not key or not Blurb.bannerTextFields[key] then
  673. error(string.format(
  674. '"%s" is not a valid banner config field',
  675. tostring(key)
  676. ), 2)
  677. end
  678.  
  679. -- Generate the text.
  680. local msg = self._protectionObj.bannerConfig[key]
  681. if type(msg) == 'string' then
  682. return self:_substituteParameters(msg)
  683. elseif type(msg) == 'function' then
  684. msg = msg(self._protectionObj, self._args)
  685. if type(msg) ~= 'string' then
  686. error(string.format(
  687. 'bad output from banner config function with key "%s"'
  688. .. ' (expected string, got %s)',
  689. tostring(key),
  690. type(msg)
  691. ), 4)
  692. end
  693. return self:_substituteParameters(msg)
  694. end
  695. end
  696.  
  697. --------------------------------------------------------------------------------
  698. -- BannerTemplate class
  699. --------------------------------------------------------------------------------
  700.  
  701. local BannerTemplate = {}
  702. BannerTemplate.__index = BannerTemplate
  703.  
  704. function BannerTemplate.new(protectionObj, cfg)
  705. local obj = {}
  706. obj._cfg = cfg
  707.  
  708. -- Set the image filename.
  709. local imageFilename = protectionObj.bannerConfig.image
  710. if imageFilename then
  711. obj._imageFilename = imageFilename
  712. else
  713. -- If an image filename isn't specified explicitly in the banner config,
  714. -- generate it from the protection status and the namespace.
  715. local action = protectionObj.action
  716. local level = protectionObj.level
  717. local namespace = protectionObj.title.namespace
  718. local reason = protectionObj.reason
  719. -- Deal with special cases first.
  720. if (
  721. namespace == 10
  722. or namespace == 828
  723. or reason and obj._cfg.indefImageReasons[reason]
  724. )
  725. and action == 'edit'
  726. and level == 'sysop'
  727. and not protectionObj:isTemporary()
  728. then
  729. -- Fully protected modules and templates get the special red "indef"
  730. -- padlock.
  731. obj._imageFilename = obj._cfg.msg['image-filename-indef']
  732. else
  733. -- Deal with regular protection types.
  734. local images = obj._cfg.images
  735. if images[action] then
  736. if images[action][level] then
  737. obj._imageFilename = images[action][level]
  738. elseif images[action].default then
  739. obj._imageFilename = images[action].default
  740. end
  741. end
  742. end
  743. end
  744. return setmetatable(obj, BannerTemplate)
  745. end
  746.  
  747. function BannerTemplate:setImageWidth(width)
  748. self._imageWidth = width
  749. end
  750.  
  751. function BannerTemplate:setImageTooltip(tooltip)
  752. self._imageCaption = tooltip
  753. end
  754.  
  755. function BannerTemplate:renderImage()
  756. local filename = self._imageFilename
  757. or self._cfg.msg['image-filename-default']
  758. or 'Transparent.gif'
  759. return makeFileLink{
  760. file = filename,
  761. size = (self._imageWidth or 20) .. 'px',
  762. alt = self._imageAlt,
  763. link = self._imageLink,
  764. caption = self._imageCaption
  765. }
  766. end
  767.  
  768. --------------------------------------------------------------------------------
  769. -- Banner class
  770. --------------------------------------------------------------------------------
  771.  
  772. local Banner = setmetatable({}, BannerTemplate)
  773. Banner.__index = Banner
  774.  
  775. function Banner.new(protectionObj, blurbObj, cfg)
  776. local obj = BannerTemplate.new(protectionObj, cfg) -- This doesn't need the blurb.
  777. obj:setImageWidth(40)
  778. obj:setImageTooltip(blurbObj:makeBannerText('alt')) -- Large banners use the alt text for the tooltip.
  779. obj._reasonText = blurbObj:makeBannerText('text')
  780. obj._explanationText = blurbObj:makeBannerText('explanation')
  781. obj._page = protectionObj.title.prefixedText -- Only makes a difference in testing.
  782. return setmetatable(obj, Banner)
  783. end
  784.  
  785. function Banner:__tostring()
  786. -- Renders the banner.
  787. makeMessageBox = makeMessageBox or require('Module:Message box').main
  788. local reasonText = self._reasonText or error('no reason text set', 2)
  789. local explanationText = self._explanationText
  790. local mbargs = {
  791. page = self._page,
  792. type = 'protection',
  793. image = self:renderImage(),
  794. text = string.format(
  795. "'''%s'''%s",
  796. reasonText,
  797. explanationText and '<br />' .. explanationText or ''
  798. )
  799. }
  800. return makeMessageBox('mbox', mbargs)
  801. end
  802.  
  803. --------------------------------------------------------------------------------
  804. -- Padlock class
  805. --------------------------------------------------------------------------------
  806.  
  807. local Padlock = setmetatable({}, BannerTemplate)
  808. Padlock.__index = Padlock
  809.  
  810. function Padlock.new(protectionObj, blurbObj, cfg)
  811. local obj = BannerTemplate.new(protectionObj, cfg) -- This doesn't need the blurb.
  812. obj:setImageWidth(20)
  813. obj:setImageTooltip(blurbObj:makeBannerText('tooltip'))
  814. obj._imageAlt = blurbObj:makeBannerText('alt')
  815. obj._imageLink = blurbObj:makeBannerText('link')
  816. obj._indicatorName = cfg.padlockIndicatorNames[protectionObj.action]
  817. or cfg.padlockIndicatorNames.default
  818. or 'pp-default'
  819. return setmetatable(obj, Padlock)
  820. end
  821.  
  822. function Padlock:__tostring()
  823. local frame = mw.getCurrentFrame()
  824. -- The nowiki tag helps prevent whitespace at the top of articles.
  825. local nowiki = frame:extensionTag{name = 'nowiki'}
  826. local indicator = frame:extensionTag{
  827. name = 'indicator',
  828. args = {name = self._indicatorName},
  829. content = self:renderImage()
  830. }
  831. return nowiki .. indicator
  832. end
  833.  
  834. --------------------------------------------------------------------------------
  835. -- Exports
  836. --------------------------------------------------------------------------------
  837.  
  838. local p = {}
  839.  
  840. function p._exportClasses()
  841. -- This is used for testing purposes.
  842. return {
  843. Protection = Protection,
  844. Blurb = Blurb,
  845. BannerTemplate = BannerTemplate,
  846. Banner = Banner,
  847. Padlock = Padlock,
  848. }
  849. end
  850.  
  851. function p._main(args, cfg, title)
  852. args = args or {}
  853. cfg = cfg or require(CONFIG_MODULE)
  854.  
  855. local protectionObj = Protection.new(args, cfg, title)
  856.  
  857. local ret = {}
  858.  
  859. -- If a page's edit protection is equally or more restrictive than its protection from some other action,
  860. -- then don't bother displaying anything for the other action (except categories).
  861. if protectionObj.action == 'edit' or args.demolevel or not walkHierarchy(cfg.hierarchy, protectionObj.level)[effectiveProtectionLevel('edit', protectionObj.title)] then
  862. -- Initialise the blurb object
  863. local blurbObj = Blurb.new(protectionObj, args, cfg)
  864. -- Render the banner
  865. if protectionObj:isProtected() then
  866. ret[#ret + 1] = tostring(
  867. (yesno(args.small) and Padlock or Banner)
  868. .new(protectionObj, blurbObj, cfg)
  869. )
  870. end
  871. end
  872.  
  873. -- Render the categories
  874. if yesno(args.category) ~= false then
  875. ret[#ret + 1] = protectionObj:makeCategoryLinks()
  876. end
  877. return table.concat(ret)
  878. end
  879.  
  880. function p.main(frame, cfg)
  881. cfg = cfg or require(CONFIG_MODULE)
  882.  
  883. -- Find default args, if any.
  884. local parent = frame.getParent and frame:getParent()
  885. local defaultArgs = parent and cfg.wrappers[parent:getTitle():gsub('/sandbox$', '')]
  886.  
  887. -- Find user args, and use the parent frame if we are being called from a
  888. -- wrapper template.
  889. getArgs = getArgs or require('Module:Arguments').getArgs
  890. local userArgs = getArgs(frame, {
  891. parentOnly = defaultArgs,
  892. frameOnly = not defaultArgs
  893. })
  894.  
  895. -- Build the args table. User-specified args overwrite default args.
  896. local args = {}
  897. for k, v in pairs(defaultArgs or {}) do
  898. args[k] = v
  899. end
  900. for k, v in pairs(userArgs) do
  901. args[k] = v
  902. end
  903. return p._main(args, cfg)
  904. end
  905.  
  906. return p