Jump to content

Module:Protection banner

Permanently protected module
From Wikipedia, the free encyclopedia

-- This module implements {{pp-meta}} and its daughter templates such as-- {{pp-dispute}}, {{pp-vandalism}} and {{pp-sock}}.-- Initialise necessary modules.require('strict')localmakeFileLink=require('Module:File link')._mainlocaleffectiveProtectionLevel=require('Module:Effective protection level')._mainlocaleffectiveProtectionExpiry=require('Module:Effective protection expiry')._mainlocalyesno=require('Module:Yesno')-- Lazily initialise modules and objects we don't always need.localgetArgs,makeMessageBox,lang-- Set constants.localCONFIG_MODULE='Module:Protection banner/config'---------------------------------------------------------------------------------- Helper functions--------------------------------------------------------------------------------localfunctionmakeCategoryLink(cat,sort)ifcatthenreturnstring.format('[[%s:%s|%s]]',mw.site.namespaces[14].name,cat,sort)endend-- Validation function for the expiry and the protection datelocalfunctionvalidateDate(dateString,dateType)ifnotlangthenlang=mw.language.getContentLanguage()endlocalsuccess,result=pcall(lang.formatDate,lang,'U',dateString)ifsuccessthenresult=tonumber(result)ifresultthenreturnresultendenderror(string.format('invalid %s: %s',dateType,tostring(dateString)),4)endlocalfunctionmakeFullUrl(page,query,display)returnstring.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).localfunctiongetReachableNodes(graph,start)localtoWalk,retval={[start]=true},{}whiletruedo-- Can't use pairs() since we're adding and removing things as we're iteratinglocalk=next(toWalk)-- This always gets the "first" keyifk==nilthenreturnretvalendtoWalk[k]=nilretval[k]=truefor_,vinipairs(graph[k])doifnotretval[v]thentoWalk[v]=trueendendendend---------------------------------------------------------------------------------- Protection class--------------------------------------------------------------------------------localProtection={}Protection.__index=ProtectionProtection.supportedActions={edit=true,move=true,autoreview=true,upload=true}Protection.bannerConfigFields={'text','explanation','tooltip','alt','link','image'}functionProtection.new(args,cfg,title)localobj={}obj._cfg=cfgobj.title=titleormw.title.getCurrentTitle()-- Set actionifnotargs.actionthenobj.action='edit'elseifProtection.supportedActions[args.action]thenobj.action=args.actionelseerror(string.format('invalid action: %s',tostring(args.action)),3)end-- Set levelobj.level=args.demoleveloreffectiveProtectionLevel(obj.action,obj.title)ifnotobj.levelor(obj.action=='move'andobj.level=='autoconfirmed')then-- Users need to be autoconfirmed to move pages anyway, so treat-- semi-move-protected pages as unprotected.obj.level='*'end-- Set expirylocaleffectiveExpiry=effectiveProtectionExpiry(obj.action,obj.title)ifeffectiveExpiry=='infinity'thenobj.expiry='indef'elseifeffectiveExpiry~='unknown'thenobj.expiry=validateDate(effectiveExpiry,'expiry date')end-- Set reasonifargs[1]thenobj.reason=mw.ustring.lower(args[1])ifobj.reason:find('|')thenerror('reasons cannot contain the pipe character ("|")',3)endend-- Set protection dateifargs.datethenobj.protectionDate=validateDate(args.date,'protection date')end-- Set banner configdoobj.bannerConfig={}localconfigTables={}ifcfg.banners[obj.action]thenconfigTables[#configTables+1]=cfg.banners[obj.action][obj.reason]endifcfg.defaultBanners[obj.action]thenconfigTables[#configTables+1]=cfg.defaultBanners[obj.action][obj.level]configTables[#configTables+1]=cfg.defaultBanners[obj.action].defaultendconfigTables[#configTables+1]=cfg.masterBannerfori,fieldinipairs(Protection.bannerConfigFields)doforj,tinipairs(configTables)doift[field]thenobj.bannerConfig[field]=t[field]breakendendendendreturnsetmetatable(obj,Protection)endfunctionProtection:isUserScript()-- Whether the page is a user JavaScript or CSS page.localtitle=self.titlereturntitle.namespace==2and(title.contentModel=='javascript'ortitle.contentModel=='css')endfunctionProtection:isProtected()returnself.level~='*'endfunctionProtection:shouldShowLock()-- Whether we should output a banner/padlockreturnself:isProtected()andnotself:isUserScript()end-- Whether this page needs a protection category.Protection.shouldHaveProtectionCategory=Protection.shouldShowLockfunctionProtection:isTemporary()returntype(self.expiry)=='number'endfunctionProtection:makeProtectionCategory()ifnotself:shouldHaveProtectionCategory()thenreturn''endlocalcfg=self._cfglocaltitle=self.title-- Get the expiry key fragment.localexpiryFragmentifself.expiry=='indef'thenexpiryFragment=self.expiryelseiftype(self.expiry)=='number'thenexpiryFragment='temp'end-- Get the namespace key fragment.localnamespaceFragment=cfg.categoryNamespaceKeys[title.namespace]ifnotnamespaceFragmentandtitle.namespace%2==1thennamespaceFragment='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.localorder={{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.reasonandcfg.reasonsWithNamespacePriority[self.reason]and2or3))--[[ -- 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. --]]localnoActive,attemptOrderdolocalactive,inactive={},{}fori,tinipairs(order)doift.valthenactive[#active+1]=telseinactive[#inactive+1]=tendendnoActive=#activeattemptOrder=activefori,tinipairs(inactive)doattemptOrder[#attemptOrder+1]=tendend--[[ -- 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. --]]localcats=cfg.protectionCategoriesfori=1,2^noActivedolocalkey={}forj,tinipairs(attemptOrder)doifj>noActivethenkey[t.keypos]='all'elselocalquotient=i/2^(j-1)quotient=math.ceil(quotient)ifquotient%2==1thenkey[t.keypos]=t.valelsekey[t.keypos]='all'endendendkey=table.concat(key,'|')localattempt=cats[key]ifattemptthenreturnmakeCategoryLink(attempt,title.text)endendreturn''endfunctionProtection:isIncorrect()localexpiry=self.expiryreturnnotself:shouldHaveProtectionCategory()ortype(expiry)=='number'andexpiry<os.time()endfunctionProtection:isTemplateProtectedNonTemplate()localaction,namespace=self.action,self.title.namespacereturnself.level=='templateeditor'and((action~='edit'andaction~='move')or(namespace~=10andnamespace~=828))endfunctionProtection:makeCategoryLinks()localmsg=self._cfg.msglocalret={self:makeProtectionCategory()}ifself:isIncorrect()thenret[#ret+1]=makeCategoryLink(msg['tracking-category-incorrect'],self.title.text)endifself:isTemplateProtectedNonTemplate()thenret[#ret+1]=makeCategoryLink(msg['tracking-category-template'],self.title.text)endreturntable.concat(ret)end---------------------------------------------------------------------------------- Blurb class--------------------------------------------------------------------------------localBlurb={}Blurb.__index=BlurbBlurb.bannerTextFields={text=true,explanation=true,tooltip=true,alt=true,link=true}functionBlurb.new(protectionObj,args,cfg)returnsetmetatable({_cfg=cfg,_protectionObj=protectionObj,_args=args},Blurb)end-- Private methods --functionBlurb:_formatDate(num)-- Formats a Unix timestamp into dd Month, YYYY format.lang=langormw.language.getContentLanguage()localsuccess,date=pcall(lang.formatDate,lang,self._cfg.msg['expiry-date-format']or'j F Y','@'..tostring(num))ifsuccessthenreturndateendendfunctionBlurb:_getExpandedMessage(msgKey)returnself:_substituteParameters(self._cfg.msg[msgKey])endfunctionBlurb:_substituteParameters(msg)ifnotself._paramsthenlocalparameterFuncs={}parameterFuncs.CURRENTVERSION=self._makeCurrentVersionParameterparameterFuncs.EDITREQUEST=self._makeEditRequestParameterparameterFuncs.EXPIRY=self._makeExpiryParameterparameterFuncs.EXPLANATIONBLURB=self._makeExplanationBlurbParameterparameterFuncs.IMAGELINK=self._makeImageLinkParameterparameterFuncs.INTROBLURB=self._makeIntroBlurbParameterparameterFuncs.INTROFRAGMENT=self._makeIntroFragmentParameterparameterFuncs.PAGETYPE=self._makePagetypeParameterparameterFuncs.PROTECTIONBLURB=self._makeProtectionBlurbParameterparameterFuncs.PROTECTIONDATE=self._makeProtectionDateParameterparameterFuncs.PROTECTIONLEVEL=self._makeProtectionLevelParameterparameterFuncs.PROTECTIONLOG=self._makeProtectionLogParameterparameterFuncs.TALKPAGE=self._makeTalkPageParameterparameterFuncs.TOOLTIPBLURB=self._makeTooltipBlurbParameterparameterFuncs.TOOLTIPFRAGMENT=self._makeTooltipFragmentParameterparameterFuncs.VANDAL=self._makeVandalTemplateParameterself._params=setmetatable({},{__index=function(t,k)localparamifparameterFuncs[k]thenparam=parameterFuncs[k](self)endparam=paramor''t[k]=paramreturnparamend})endmsg=msg:gsub('${(%u+)}',self._params)returnmsgendfunctionBlurb:_makeCurrentVersionParameter()-- A link to the page history or the move log, depending on the kind of-- protection.localpagename=self._protectionObj.title.prefixedTextifself._protectionObj.action=='move'then-- We need the move log link.returnmakeFullUrl('Special:Log',{type='move',page=pagename},self:_getExpandedMessage('current-version-move-display'))else-- We need the history link.returnmakeFullUrl(pagename,{action='history'},self:_getExpandedMessage('current-version-edit-display'))endendfunctionBlurb:_makeEditRequestParameter()localmEditRequest=require('Module:Submit an edit request')localaction=self._protectionObj.actionlocallevel=self._protectionObj.level-- Get the edit request type.localrequestTypeifaction=='edit'theniflevel=='autoconfirmed'thenrequestType='semi'elseiflevel=='extendedconfirmed'thenrequestType='extended'elseiflevel=='templateeditor'thenrequestType='template'endendrequestType=requestTypeor'full'-- Get the display value.localdisplay=self:_getExpandedMessage('edit-request-display')returnmEditRequest._link{type=requestType,display=display}endfunctionBlurb:_makeExpiryParameter()localexpiry=self._protectionObj.expiryiftype(expiry)=='number'thenreturnself:_formatDate(expiry)elsereturnexpiryendendfunctionBlurb:_makeExplanationBlurbParameter()-- Cover special cases first.ifself._protectionObj.title.namespace==8then-- MediaWiki namespacereturnself:_getExpandedMessage('explanation-blurb-nounprotect')end-- Get explanation blurb table keyslocalaction=self._protectionObj.actionlocallevel=self._protectionObj.levellocaltalkKey=self._protectionObj.title.isTalkPageand'talk'or'subject'-- Find the message in the explanation blurb table and substitute any-- parameters.localexplanations=self._cfg.explanationBlurbslocalmsgifexplanations[action][level]andexplanations[action][level][talkKey]thenmsg=explanations[action][level][talkKey]elseifexplanations[action][level]andexplanations[action][level].defaultthenmsg=explanations[action][level].defaultelseifexplanations[action].defaultandexplanations[action].default[talkKey]thenmsg=explanations[action].default[talkKey]elseifexplanations[action].defaultandexplanations[action].default.defaultthenmsg=explanations[action].default.defaultelseerror(string.format('could not find explanation blurb for action "%s", level "%s" and talk key "%s"',action,level,talkKey),8)endreturnself:_substituteParameters(msg)endfunctionBlurb:_makeImageLinkParameter()localimageLinks=self._cfg.imageLinkslocalaction=self._protectionObj.actionlocallevel=self._protectionObj.levellocalmsgifimageLinks[action][level]thenmsg=imageLinks[action][level]elseifimageLinks[action].defaultthenmsg=imageLinks[action].defaultelsemsg=imageLinks.edit.defaultendreturnself:_substituteParameters(msg)endfunctionBlurb:_makeIntroBlurbParameter()ifself._protectionObj:isTemporary()thenreturnself:_getExpandedMessage('intro-blurb-expiry')elsereturnself:_getExpandedMessage('intro-blurb-noexpiry')endendfunctionBlurb:_makeIntroFragmentParameter()ifself._protectionObj:isTemporary()thenreturnself:_getExpandedMessage('intro-fragment-expiry')elsereturnself:_getExpandedMessage('intro-fragment-noexpiry')endendfunctionBlurb:_makePagetypeParameter()localpagetypes=self._cfg.pagetypesreturnpagetypes[self._protectionObj.title.namespace]orpagetypes.defaultorerror('no default pagetype defined',8)endfunctionBlurb:_makeProtectionBlurbParameter()localprotectionBlurbs=self._cfg.protectionBlurbslocalaction=self._protectionObj.actionlocallevel=self._protectionObj.levellocalmsgifprotectionBlurbs[action][level]thenmsg=protectionBlurbs[action][level]elseifprotectionBlurbs[action].defaultthenmsg=protectionBlurbs[action].defaultelseifprotectionBlurbs.edit.defaultthenmsg=protectionBlurbs.edit.defaultelseerror('no protection blurb defined for protectionBlurbs.edit.default',8)endreturnself:_substituteParameters(msg)endfunctionBlurb:_makeProtectionDateParameter()localprotectionDate=self._protectionObj.protectionDateiftype(protectionDate)=='number'thenreturnself:_formatDate(protectionDate)elsereturnprotectionDateendendfunctionBlurb:_makeProtectionLevelParameter()localprotectionLevels=self._cfg.protectionLevelslocalaction=self._protectionObj.actionlocallevel=self._protectionObj.levellocalmsgifprotectionLevels[action][level]thenmsg=protectionLevels[action][level]elseifprotectionLevels[action].defaultthenmsg=protectionLevels[action].defaultelseifprotectionLevels.edit.defaultthenmsg=protectionLevels.edit.defaultelseerror('no protection level defined for protectionLevels.edit.default',8)endreturnself:_substituteParameters(msg)endfunctionBlurb:_makeProtectionLogParameter()localpagename=self._protectionObj.title.prefixedTextifself._protectionObj.action=='autoreview'then-- We need the pending changes log.returnmakeFullUrl('Special:Log',{type='stable',page=pagename},self:_getExpandedMessage('pc-log-display'))else-- We need the protection log.returnmakeFullUrl('Special:Log',{type='protect',page=pagename},self:_getExpandedMessage('protection-log-display'))endendfunctionBlurb:_makeTalkPageParameter()returnstring.format('[[%s:%s#%s|%s]]',mw.site.namespaces[self._protectionObj.title.namespace].talk.name,self._protectionObj.title.text,self._args.sectionor'top',self:_getExpandedMessage('talk-page-link-display'))endfunctionBlurb:_makeTooltipBlurbParameter()ifself._protectionObj:isTemporary()thenreturnself:_getExpandedMessage('tooltip-blurb-expiry')elsereturnself:_getExpandedMessage('tooltip-blurb-noexpiry')endendfunctionBlurb:_makeTooltipFragmentParameter()ifself._protectionObj:isTemporary()thenreturnself:_getExpandedMessage('tooltip-fragment-expiry')elsereturnself:_getExpandedMessage('tooltip-fragment-noexpiry')endendfunctionBlurb:_makeVandalTemplateParameter()returnmw.getCurrentFrame():expandTemplate{title="vandal-m",args={self._args.userorself._protectionObj.title.baseText}}end-- Public methods --functionBlurb:makeBannerText(key)-- Validate input.ifnotkeyornotBlurb.bannerTextFields[key]thenerror(string.format('"%s" is not a valid banner config field',tostring(key)),2)end-- Generate the text.localmsg=self._protectionObj.bannerConfig[key]iftype(msg)=='string'thenreturnself:_substituteParameters(msg)elseiftype(msg)=='function'thenmsg=msg(self._protectionObj,self._args)iftype(msg)~='string'thenerror(string.format('bad output from banner config function with key "%s"'..' (expected string, got %s)',tostring(key),type(msg)),4)endreturnself:_substituteParameters(msg)endend---------------------------------------------------------------------------------- BannerTemplate class--------------------------------------------------------------------------------localBannerTemplate={}BannerTemplate.__index=BannerTemplatefunctionBannerTemplate.new(protectionObj,cfg)localobj={}obj._cfg=cfg-- Set the image filename.localimageFilename=protectionObj.bannerConfig.imageifimageFilenamethenobj._imageFilename=imageFilenameelse-- If an image filename isn't specified explicitly in the banner config,-- generate it from the protection status and the namespace.localaction=protectionObj.actionlocallevel=protectionObj.levellocalnamespace=protectionObj.title.namespacelocalreason=protectionObj.reason-- Deal with special cases first.if(namespace==10ornamespace==828orreasonandobj._cfg.indefImageReasons[reason])andaction=='edit'andlevel=='sysop'andnotprotectionObj: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.localimages=obj._cfg.imagesifimages[action]thenifimages[action][level]thenobj._imageFilename=images[action][level]elseifimages[action].defaultthenobj._imageFilename=images[action].defaultendendendendreturnsetmetatable(obj,BannerTemplate)endfunctionBannerTemplate:renderImage()localfilename=self._imageFilenameorself._cfg.msg['image-filename-default']or'Transparent.gif'returnmakeFileLink{file=filename,size=(self.imageWidthor20)..'px',alt=self._imageAlt,link=self._imageLink,caption=self.imageCaption}end---------------------------------------------------------------------------------- Banner class--------------------------------------------------------------------------------localBanner=setmetatable({},BannerTemplate)Banner.__index=BannerfunctionBanner.new(protectionObj,blurbObj,cfg)localobj=BannerTemplate.new(protectionObj,cfg)-- This doesn't need the blurb.obj.imageWidth=40obj.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.returnsetmetatable(obj,Banner)endfunctionBanner:__tostring()-- Renders the banner.makeMessageBox=makeMessageBoxorrequire('Module:Message box').mainlocalreasonText=self._reasonTextorerror('no reason text set',2)localexplanationText=self._explanationTextlocalmbargs={page=self._page,type='protection',image=self:renderImage(),text=string.format("'''%s'''%s",reasonText,explanationTextand'<br />'..explanationTextor'')}returnmakeMessageBox('mbox',mbargs)end---------------------------------------------------------------------------------- Padlock class--------------------------------------------------------------------------------localPadlock=setmetatable({},BannerTemplate)Padlock.__index=PadlockfunctionPadlock.new(protectionObj,blurbObj,cfg)localobj=BannerTemplate.new(protectionObj,cfg)-- This doesn't need the blurb.obj.imageWidth=20obj.imageCaption=blurbObj:makeBannerText('tooltip')obj._imageAlt=blurbObj:makeBannerText('alt')obj._imageLink=blurbObj:makeBannerText('link')obj._indicatorName=cfg.padlockIndicatorNames[protectionObj.action]orcfg.padlockIndicatorNames.defaultor'pp-default'returnsetmetatable(obj,Padlock)endfunctionPadlock:__tostring()localframe=mw.getCurrentFrame()-- The nowiki tag helps prevent whitespace at the top of articles.returnframe:extensionTag{name='nowiki'}..frame:extensionTag{name='indicator',args={name=self._indicatorName},content=self:renderImage()}end---------------------------------------------------------------------------------- Exports--------------------------------------------------------------------------------localp={}functionp._exportClasses()-- This is used for testing purposes.return{Protection=Protection,Blurb=Blurb,BannerTemplate=BannerTemplate,Banner=Banner,Padlock=Padlock,}endfunctionp._main(args,cfg,title)args=argsor{}cfg=cfgorrequire(CONFIG_MODULE)localprotectionObj=Protection.new(args,cfg,title)localret={}-- 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).ifnotyesno(args.catonly)and(protectionObj.action=='edit'orargs.demolevelornotgetReachableNodes(cfg.hierarchy,protectionObj.level)[effectiveProtectionLevel('edit',protectionObj.title)])then-- Initialise the blurb objectlocalblurbObj=Blurb.new(protectionObj,args,cfg)-- Render the bannerifprotectionObj:shouldShowLock()thenret[#ret+1]=tostring((yesno(args.small)andPadlockorBanner).new(protectionObj,blurbObj,cfg))endend-- Render the categoriesifyesno(args.category)~=falsethenret[#ret+1]=protectionObj:makeCategoryLinks()end-- For arbitration enforcement, flagging [[WP:PIA]] pages to enable [[Special:AbuseFilter/1339]] to flag edits to themifprotectionObj.level=="extendedconfirmed"thenifrequire("Module:TableTools").inArray(protectionObj.title.talkPageTitle.categories,"Wikipedia pages subject to the extended confirmed restriction related to the Arab-Israeli conflict")thenret[#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>"endendreturntable.concat(ret)endfunctionp.main(frame,cfg)cfg=cfgorrequire(CONFIG_MODULE)-- Find default args, if any.localparent=frame.getParentandframe:getParent()localdefaultArgs=parentandcfg.wrappers[parent:getTitle():gsub('/sandbox$','')]-- Find user args, and use the parent frame if we are being called from a-- wrapper template.getArgs=getArgsorrequire('Module:Arguments').getArgslocaluserArgs=getArgs(frame,{parentOnly=defaultArgs,frameOnly=notdefaultArgs})-- Build the args table. User-specified args overwrite default args.localargs={}fork,vinpairs(defaultArgsor{})doargs[k]=vendfork,vinpairs(userArgs)doargs[k]=vendreturnp._main(args,cfg)endreturnp
close