Jump to content

Module:Protected edit request

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Jackmcbarn (talk | contribs) at 23:44, 19 February 2014 (Put ID for anchor on the same element as the actual banner is. This anchor system is still really bad and should probably be rethought at some point.). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

local yesno = require('Module:Yesno')
local makeMessageBox = require('Module:Message box').main

-- may not need these. lazily initalize
local makeToolbar, getPagetype, effectiveProtectionLevel
local modulesLoaded = false

----------------------------------------------------------------------
-- Helper functions
----------------------------------------------------------------------

local function makeWikilink(page, display)
	if display then
		return mw.ustring.format('[[%s|%s]]', page, display)
	else
		return mw.ustring.format('[[%s]]', page)
	end
end

----------------------------------------------------------------------
-- Title class
----------------------------------------------------------------------

-- This is basically the mw.title class with some extras thrown in.

local title = {}
title.__index = title

function title.getProtectionLevelText(protectionLevel)
	-- Gets the text to use in anchors and urn links.
	local levels = {unprotected = 'editunprotected', autoconfirmed = 'editsemiprotected', templateeditor = 'edittemplateprotected', sysop = 'editprotected'}
	return levels[protectionLevel]
end

function title.new(...)
	local success, obj = pcall(mw.title.new, ...)
	if not (success and obj) then return end

	-- Add a protectionLevel property.
	obj.protectionLevel = effectiveProtectionLevel(obj.exists and 'edit' or 'create', obj)
	if obj.protectionLevel == '*' then
		-- Make unprotected pages return "unprotected".
		obj.protectionLevel = 'unprotected'
	elseif obj.protectionLevel == 'user' then
		-- If we just need to be registered, pretend we need to be autoconfirmed, since it's the closest thing we have.
		obj.protectionLevel = 'autoconfirmed'
	elseif obj.protectionLevel == 'accountcreator' then
		-- Lump titleblacklisted pages in with template-protected pages, since templateeditors can do both.
		obj.protectionLevel = 'templateeditor'
	end

	-- Add a pagetype property.
	obj.pagetype = getPagetype{page = obj.prefixedText, defaultns = 'all'}
	
	-- Add link-making methods.
	function obj:makeUrlLink(query, display)
		return mw.ustring.format('[%s %s]', self:fullUrl(query), display)
	end

	function obj:makeViewLink(display)
		return self:makeUrlLink({redirect = 'no'}, display)
	end

	function obj:makeEditLink(display)
		return self:makeUrlLink({action = 'edit'}, display)
	end

	function obj:makeHistoryLink(display)
		return self:makeUrlLink({action = 'history'}, display)
	end

	function obj:makeLastEditLink(display)
		return self:makeUrlLink({diff = 'cur', oldid = 'prev'}, display)
	end

	function obj:makeWhatLinksHereLink(display)
		return makeWikilink('Special:WhatLinksHere/' .. self.prefixedText, display)
	end

	function obj:makeCompareLink(otherTitle, display)
		display = display or 'diff'
		local comparePagesTitle = title.new('Special:ComparePages')
		return comparePagesTitle:makeUrlLink({page1 = self.prefixedText, page2 = otherTitle.prefixedText}, display)
	end

	function obj:makeLogLink(logType, display)
		local logTitle = title.new('Special:Log')
		return logTitle:makeUrlLink({type = logType, page = self.prefixedText}, display)
	end

	function obj:urlEncode()
		return mw.uri.encode(self.prefixedText, 'WIKI')
	end

	function obj:makeUrnLink(boxProtectionLevel)
		-- Outputs a urn link. The protection level is taken from the template, rather than detected from page itself,
		-- as the detection may be inaccurate for cascade-protected and title-blacklisted pages as of Nov 2013.
		local protectionLinkText = title.getProtectionLevelText(boxProtectionLevel)
		return mw.ustring.format('[urn:x-wp-%s:%s <span/>]', protectionLinkText, self:urlEncode())
	end

	-- Get a subpage title object, but go through pcall rather than use the unprotected mw.title:subPageTitle.
	function obj:getSubpageTitle(subpage)
		return title.new(self.prefixedText .. '/' .. subpage)
	end
	
	return obj
end

----------------------------------------------------------------------
-- TitleTable class
----------------------------------------------------------------------

local titleTable = {}
titleTable.__index = titleTable

function titleTable.new(args)
	-- Get numerical arguments and make title objects for each of them. 
	local nums = {}
	for k, v in pairs(args) do
		if type(k) == 'number' then
			table.insert(nums, k)
		end
	end
	table.sort(nums)
	local titles = {}
	for _, num in ipairs(nums) do
		local title = title.new(args[num])
		table.insert(titles, title)
	end
	-- Get the current title, and get the subject title if no titles were specified.
	titles.currentTitle = mw.title.getCurrentTitle()
	if #titles < 1 then
		local currentTitle = titles.currentTitle
		local subjectNs = currentTitle.subjectNsText
		if subjectNs ~= '' then
			subjectNs = subjectNs .. ':'
		end
		titles.subjectTitle = title.new(subjectNs .. currentTitle.text)
	end
	-- Set the metatable.
	setmetatable(titles, titleTable)
	return titles
end

function titleTable:memoize(memoField, func, ...)
	if self[memoField] ~= nil then
		return self[memoField]
	else
		self[memoField] = func(...)
		return self[memoField]
	end
end

function titleTable:titleIterator()
	local i = 0
	local n = #self
	return function()
		i = i + 1
		if i == 1 and n < 1 then
			return self.subjectTitle
		else
			if i <= n then
				return self[i]
			end
		end
	end
end

function titleTable:hasSameProperty(memoField, getPropertyFunc)
	-- If the titles table has more than one title in it, check if they have the same property.
	-- The property is found using the getPropertyFunc function, which takes a title object as its single argument.
	if #self <= 1 then return end
	
	local function hasSameProperty(getPropertyFunc)
		local properties = {}
		for i, obj in ipairs(self) do
			local property = getPropertyFunc(obj)
			if i == 1 or property ~= properties[#properties] then
				table.insert(properties, property)
			end
		end
		if #properties == 1 then
			return true
		elseif #properties > 1 then
			return false
		end
	end

	return self:memoize(memoField, hasSameProperty, getPropertyFunc)
end	

function titleTable:hasSameExistenceStatus()
	-- Returns true if all the titles exist, or if they all don't exist. Returns false if there is a mixture of existence statuses,
	-- and returns nil if there is one title or less.
	return self:hasSameProperty('sameExistenceStatus', function (title) return title.exists end)
end

function titleTable:hasSameProtectionStatus()
	-- Checks if all the titles have the same protection status (either for creation protection or for edit-protection - the two are not mixed).
	local sameExistenceStatus = self:hasSameExistenceStatus()
	if sameExistenceStatus then
		return self:hasSameProperty('sameProtectionStatus', function (title) return title.protectionLevel end)
	else
		return sameExistenceStatus
	end
end

function titleTable:hasSamePagetype()
	-- Checks if all the titles have the same pagetype.
	return self:hasSameProperty('samePagetype', function (title) return title.pagetype end)
end

function titleTable:propertyExists(memoField, getPropertyFunc)
	-- Checks if a title with a certain property exists.
	-- The property is found using the getPropertyFunc function, which takes a title object as its single argument
	-- and should return a boolean value.
	local function propertyExists(getPropertyFunc)
		for titleObj in self:titleIterator() do
			if getPropertyFunc(titleObj) then
				return true
			end
		end
		return false
	end
	return self:memoize(memoField, propertyExists, getPropertyFunc)
end

function titleTable:hasNonInterfacePage()
	return self:propertyExists('nonInterfacePage', function (titleObj) return titleObj.namespace ~= 8 end)
end

function titleTable:hasTemplateOrModule()
	return self:propertyExists('templateOrModule', function (titleObj) return titleObj.namespace == 10 or titleObj.namespace == 828 end)
end

function titleTable:hasNonTemplateOrModule()
	return self:propertyExists('nontemplateormodule', function (titleobj) return titleobj.namespace ~= 10 and titleobj.namespace ~= 828 end)
end

function titleTable:hasOtherProtectionLevel(level)
	for titleObj in self:titleIterator() do
		if titleObj.protectionLevel ~= level then
			return true
		end
	end
	return false
end

function titleTable:getProtectionLevels()
	local function getProtectionLevels()
		local levels = {}
		for titleObj in self:titleIterator() do
			local level = titleObj.protectionLevel
			levels[level] = true
		end
		return levels
	end
	return self:memoize('protectionLevels', getProtectionLevels)
end

----------------------------------------------------------------------
-- Blurb class definition
----------------------------------------------------------------------

local blurb = {}
blurb.__index = blurb

function blurb.new(titleTable, boxProtectionLevel)
	local obj = {}
	obj.titles = titleTable
	obj.boxProtectionLevel = boxProtectionLevel
	obj.linkCount = 0 -- Counter for the number of total items in the object's link lists. 
	setmetatable(obj, blurb)
	return obj
end

-- Static methods --

function blurb.makeParaText(name, val)
	local pipe = mw.text.nowiki('|')
	local equals = mw.text.nowiki('=')
	val = val and ("''" .. val .. "''") or ''
	return mw.ustring.format('<code style="white-space: nowrap;">%s%s%s%s</code>', pipe, name, equals, val)
end

function blurb.makeTemplateLink(s)
	return mw.ustring.format('%s[[Template:%s|%s]]%s', mw.text.nowiki('{{'), s,	s, mw.text.nowiki('}}'))
end

function blurb:makeProtectionText()
	local boxProtectionLevel = self.boxProtectionLevel
	local levels = {unprotected = 'unprotected', autoconfirmed = 'semi-protected', templateeditor = 'template-protected', sysop = 'fully protected'}
	for level, protectionText in pairs(levels) do
		if level == boxProtectionLevel then
			return mw.ustring.format('[[Help:Protection|%s]]', protectionText)
		end
	end
end

function blurb.getPagetypePlural(title)
	local pagetype = title.pagetype
	if pagetype == 'category' then
		return 'categories'
	else
		return pagetype .. 's'
	end
end

-- Normal methods --

function blurb:makeLinkList(title, showViewLink)
	local tbargs = {} -- The argument list to pass to Module:Toolbar
	tbargs.style = 'font-size: smaller;'
	tbargs.separator = 'dot'
	-- Show view link if the option is set.
	if showViewLink then
		table.insert(tbargs, title:makeViewLink('view'))
	end
	-- Page links.
	table.insert(tbargs, title:makeEditLink('edit'))
	table.insert(tbargs, title:makeHistoryLink('history'))
	table.insert(tbargs, title:makeLastEditLink('last'))
	table.insert(tbargs, title:makeWhatLinksHereLink('links'))
	-- Sandbox links.
	local sandboxTitle = title:getSubpageTitle('sandbox')
	if sandboxTitle and sandboxTitle.exists then
		table.insert(tbargs, sandboxTitle:makeViewLink('sandbox'))
		table.insert(tbargs, sandboxTitle:makeEditLink('edit sandbox'))
		table.insert(tbargs, sandboxTitle:makeHistoryLink('sandbox history'))
		table.insert(tbargs, sandboxTitle:makeLastEditLink('sandbox last edit'))
		table.insert(tbargs, title:makeCompareLink(sandboxTitle, 'sandbox diff'))
	end
	-- Test cases links.
	local testcasesTitle = title:getSubpageTitle('testcases')
	if testcasesTitle and testcasesTitle.exists then
		table.insert(tbargs, testcasesTitle:makeViewLink('test cases'))
	end
	-- Transclusion count link.
	if title.namespace == 10 or title.namespace == 828 then -- Only add the transclusion count link for templates and modules.
		title.fragment = 'mw-pageinfo-transclusions'
		table.insert(tbargs, title:makeUrlLink({action='info'}, 'transclusion count'))
		title.fragment = ''
	end
	-- Protection log link.
	if title.namespace ~= 8 then -- MediaWiki pages don't have protection log entries.
		table.insert(tbargs, title:makeLogLink('protect', 'protection log'))
	end
	self.linkCount = self.linkCount + #tbargs -- Keep track of the number of total links created by the object.
	return makeToolbar(tbargs)
end

function blurb:makeLinkLists()
	local titles = self.titles
	if #titles < 1 then
		return self:makeLinkList(titles.subjectTitle, true) 
	elseif #titles == 1 then
		return self:makeLinkList(titles[1]) -- The page name is included in the "at" link, so we don't need to include the view link here.
	else
		local ret = {}
		table.insert(ret, '<ul>')
		for i, titleObj in ipairs(titles) do
			table.insert(ret, mw.ustring.format('<li>%s %s</li>', titleObj:makeViewLink(titleObj.prefixedText), self:makeLinkList(titleObj)))
		end
		table.insert(ret, '</ul>')
		return table.concat(ret)
	end
end

function blurb:makeIntro()
	local titles = self.titles
	local requested = 'It is [[Wikipedia:Edit requests|requested]] that'
	local protectionText
	if titles:hasNonInterfacePage() then
		protectionText = ' ' .. self:makeProtectionText()
	else
		protectionText = '' -- Interface pages cannot be unprotected, so we don't need to explicitly say they are protected.
	end
	-- Deal with cases where we are passed multiple titles.
	if #titles > 1 then
		local pagetype
		if titles:hasSamePagetype() then
			pagetype = blurb.getPagetypePlural(titles[1])
		else
			pagetype = 'pages'
		end
		return mw.ustring.format("'''%s edits be made to the following%s %s''':", requested, protectionText, pagetype)
	end
	-- Deal with cases where we are passed only one title.
	local title = titles[1]
	if not title then
		title = titles.subjectTitle
	end
	local stringToFormat
	if title.exists then
		stringToFormat = '%s an edit be made to %s%s %s%s.'
	else
		stringToFormat = '%s %s%s %s%s be created.'
	end
	stringToFormat = "'''" .. stringToFormat .. "'''"
	local atLink, theOrThis
	if #titles == 1 then
		atLink = mw.ustring.format(' at %s', title:makeViewLink(title.prefixedText))
		theOrThis = 'the'
	else -- #titles < 1 
		atLink = ''
		theOrThis = 'this'
	end
	return mw.ustring.format(stringToFormat, requested, theOrThis, protectionText, title.pagetype, atLink)
end

function blurb:makeBody()
	local titles = self.titles
	local protectionLevels = titles:getProtectionLevels()
	local boxProtectionLevel = self.boxProtectionLevel
	local hasNonInterfacePage = titles:hasNonInterfacePage()
	local isPlural = false
	if #titles > 1 then
		isPlural = true
	end

	local descriptionText = "This template must be followed by a '''complete and specific description''' of the request, "
	if boxProtectionLevel == 'sysop' or boxProtectionLevel == 'templateeditor' then
		local editText = 'edit'
		if isPlural then
			editText = editText .. 's'
		end
		local descriptionCompleteText = mw.ustring.format('so that an editor unfamiliar with the subject matter could complete the requested %s immediately.', editText)
		descriptionText = descriptionText .. descriptionCompleteText
	else
		descriptionText = descriptionText .. 'that is, specify what text should be removed and a verbatim copy of the text that should replace it. '
			.. [["Please change ''X''" is '''not acceptable''' and will be rejected; the request '''must''' be of the form "please change ''X'' to ''Y''".]]
	end

	local smallText = ''
	if boxProtectionLevel == 'sysop' or boxProtectionLevel == 'templateeditor' then
		local templateFullText
		if boxProtectionLevel == 'sysop' then
			templateFullText = 'fully protected'
		elseif boxProtectionLevel == 'templateeditor' then
			templateFullText = 'template-protected'
		end
		smallText =	'Edit requests to ' .. templateFullText	.. " pages should only be used for edits that are either '''uncontroversial''' or supported by [[Wikipedia:Consensus|consensus]]."
			.. " If the proposed edit might be controversial, discuss it on the protected page's talk page '''before''' using this template."
	else
		local userText
		if boxProtectionLevel == 'autoconfirmed' then
			userText = '[[Wikipedia:User access levels#Autoconfirmed|autoconfirmed]] user'
		else
			userText = 'user'
		end
		local answeredPara = blurb.makeParaText('answered', 'no')
		local stringToFormat =	'The edit may be made by any %s. '
			.. [[Remember to change the %s parameter to "'''yes'''" when the request has been accepted, rejected or on hold awaiting user input. ]]
			.. "This is so that inactive or completed requests don't needlessly fill up the edit requests category. "
			.. 'You may also wish to use the %s template in the response.'
		smallText = mw.ustring.format(stringToFormat, userText, answeredPara, blurb.makeTemplateLink('ESp'))
	end
	if hasNonInterfacePage then
		smallText = smallText .. ' To request that a page be protected or unprotected, make a [[Wikipedia:Requests for page protection|protection request]].'
	end
	if boxProtectionLevel == 'sysop' or boxProtectionLevel == 'templateeditor' then
		smallText = smallText .. ' When the request has been completed or denied, please add the ' .. blurb.makeParaText('answered', 'yes') .. ' parameter to deactivate the template.'
	end
	return mw.ustring.format('%s\n<p style="font-size:smaller; line-height:1.3em;">\n%s\n</p>', descriptionText, smallText)
end

function blurb:export()
	local intro = self:makeIntro()
	local linkLists = self:makeLinkLists()
	local body = self:makeBody()
	-- Start long links lists on a new line.
	local linkListSep = ' '
	if self.linkCount > 5 then
		linkListSep = '<br />'
	end
	return mw.ustring.format('%s%s%s\n\n%s', intro, linkListSep, linkLists, body)
end

----------------------------------------------------------------------
-- Box class definition
----------------------------------------------------------------------

local box = {}
box.__index = box

function box.new(protectionType, args)
	local obj = {}
	obj.tmboxArgs = {} -- Used to store arguments to be passed to tmbox by the box:export method.
	-- Set data fields.
	local answered = args.answered or args.ans
	obj.answered = yesno(answered, true) or false
	if not obj.answered then
		if not modulesLoaded then
			-- We know we'll need these now, so go ahead and load them
			modulesLoaded = true
			makeToolbar = require('Module:Toolbar')._main
			getPagetype = require('Module:Pagetype')._main
			effectiveProtectionLevel = require('Module:Effective protection level').main
		end
		local boxProtectionLevels = {semi = 'autoconfirmed', template = 'templateeditor', full = 'sysop'}
		obj.boxProtectionLevel = boxProtectionLevels[protectionType]
		obj.demo = yesno(args.demo)
		-- Set dependent objects.
		obj.titles = titleTable.new(args)
		if not yesno(args.force) and obj.titles:hasSameProperty('sameProtectionStatus', function (title) return title.protectionLevel end) ~= false and (obj.titles[1] or obj.titles.subjectTitle).protectionLevel ~= 'unprotected' then
			obj.boxProtectionLevel = (obj.titles[1] or obj.titles.subjectTitle).protectionLevel
		end
		obj.blurb = blurb.new(obj.titles, obj.boxProtectionLevel)
	end
	setmetatable(obj, box)
	return obj
end

function box:setArg(key, value)
	-- This sets a value to be passed to tmbox.
	if key then
		self.tmboxArgs[key] = value
	end
end

function box:setImage()
	local titles = self.titles
	local boxProtectionLevel = self.boxProtectionLevel
	local padlock
	if boxProtectionLevel == 'sysop' and not titles:hasNonTemplateOrModule() then
		padlock = 'Padlock-red.svg'
	elseif boxProtectionLevel == 'sysop' then
		padlock = 'Padlock.svg'
	elseif boxProtectionLevel == 'templateeditor' then
		padlock = 'Padlock-pink.svg'
	elseif boxProtectionLevel == 'autoconfirmed' then
		padlock = 'Padlock-silver.svg'
	else
		padlock = 'Padlock-bronze-open.svg'
	end
	local stringToFormat = '[[File:%s|%dpx|alt=|link=]]'
	local smallPadlock = mw.ustring.format(stringToFormat, padlock, 25)
	local largePadlock = mw.ustring.format(stringToFormat, padlock, 60)
	self:setArg('smallimage', smallPadlock)
	self:setArg('image', largePadlock)
end

function box:setBlurbText()
	local blurbText = self.blurb:export()
	self:setArg('text', blurbText)
end

function box:setAnsweredText()
	local answeredText = mw.ustring.format(
		"This [[Wikipedia:Edit requests|edit request]] has been answered. Set the %s or %s parameter to '''no''' to reactivate your request.",
		blurb.makeParaText('answered'), blurb.makeParaText('ans')
	)
	self:setArg('smalltext', answeredText)
end

function box:exportRequestTmbox()
	self:setImage()
	self:setBlurbText()
	self:setArg('class', 'editrequest')
	self:setArg('id', title.getProtectionLevelText(self.boxProtectionLevel)) -- for anchor. yes, this leads to multiple elements with the same ID. we should probably fix this at some point
	return makeMessageBox('tmbox', self.tmboxArgs)
end

function box:exportAnsweredTmbox()
	self:setAnsweredText()
	self:setArg('small', true)
	self:setArg('class', 'editrequest')
	return makeMessageBox('tmbox', self.tmboxArgs)
end	

function box:exportRequestCategories()
	local cats = {}
	local boxProtectionLevel = self.boxProtectionLevel
	local function addCat(cat)
		table.insert(cats, mw.ustring.format('[[Category:%s]]', cat))
	end
	local protectionCats = {
		autoconfirmed = 'Wikipedia semi-protected edit requests',
		templateeditor = 'Wikipedia template-protected edit requests',
		sysop = 'Wikipedia protected edit requests'
	}
	addCat(protectionCats[boxProtectionLevel])
	if self.titles:hasOtherProtectionLevel(boxProtectionLevel) then
		addCat('Wikipedia edit requests possibly using incorrect templates')
	end
	return table.concat(cats)
end

function box:exportUrnLinks()
	local ret = {}
	local boxProtectionLevel = self.boxProtectionLevel
	for titleObj in self.titles:titleIterator() do
		table.insert(ret, titleObj:makeUrnLink(boxProtectionLevel))
	end
	return mw.ustring.format('<span class="plainlinks" style="display:none">%s</span>', table.concat(ret))
end

function box:export()
	local ret = {}
	if self.answered then
		table.insert(ret, self:exportAnsweredTmbox())
	elseif self.titles.currentTitle.isTalkPage or self.demo then
		table.insert(ret, self:exportRequestTmbox())
		table.insert(ret, self:exportUrnLinks())
		if not self.demo then
			table.insert(ret, self:exportRequestCategories())
		end
	else
		table.insert(ret, '<span class="error">Error: Protected edit requests can only be made on the talk page.</span>[[Category:Non-talk pages requesting an edit to a protected page]]')
	end
	return table.concat(ret)
end

----------------------------------------------------------------------
-- Process arguments and initialise objects
----------------------------------------------------------------------

local p = {}

function p.main(protectionType, args)
	local requestBox = box.new(protectionType, args)
	return requestBox:export()
end

local function makeWrapper(protectionType)
	return function (frame)
		-- If called via #invoke, use the args passed into the invoking template, or the args passed to #invoke if any exist.
		-- Otherwise assume args are being passed directly in from the debug console or from another Lua module.
		local origArgs
		if frame == mw.getCurrentFrame() then
			origArgs = frame:getParent().args
			for k, v in pairs(frame.args) do
				origArgs = frame.args
				break
			end
		else
			origArgs = frame
		end
		-- Trim whitespace and remove blank arguments.
		local args = {}
		for k, v in pairs(origArgs) do
			if type(v) == 'string' then
				v = mw.text.trim(v)
			end
			if v ~= '' then
				args[k] = v
			end
		end
		return p.main(protectionType, args)
	end
end

local funcNames = {'semi', 'template', 'full'}
for _, funcName in ipairs(funcNames) do
	p[funcName] = makeWrapper(funcName)
end

return p