blob: 645c5176b51d9f011afeeaef33d236df1c96c8dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
-- cgit trailer-filter that turns the value of a commit trailer into a link,
-- named by the trailer-filter or repo.trailer-filter setting in cgitrc. cgit
-- opens it once per trailer with the trailer key and the page name as its
-- arguments and hands over the value already HTML-escaped, so all this does
-- is wrap the value, or the part of it a rule captures, in an anchor. Runs on
-- Lua 5.1 through 5.4 and LuaJIT with nothing outside the standard library.
--
--     trailer-filter=lua:/path/to/link-trailers.lua
--
-- Trailers whose value is a person, such as Signed-off-by, never reach this
-- filter. cgit writes those through the email filter instead.


-- Rules are tried in order and the first whose key matches the trailer, case
-- insensitively, and whose pattern matches the value wins. Each pattern is a
-- Lua pattern with a single capture and a URL where %s is replaced by that
-- capture, percent-encoded. The matched run is what gets wrapped and the
-- capture is only what goes into the URL. A rule without a key applies to
-- every trailer.
--
-- Lua patterns are not regular expressions. The reference is
-- https://www.lua.org/manual/5.1/manual.html#5.4.1
--
-- Patterns run against the escaped value, so match the entity spellings
-- '&', '<' and '>' rather than the bare characters, and keep a
-- pattern from ending part way through one.
local rules = {
	-- Fixes: 1234567 ("subject") and Reverts: 1234567 link the object name
	-- to its commit page. The relative form is resolved against the
	-- current page and works for the common virtual-root layout.
	{ key = "Fixes", pattern = "^(%x%x%x%x%x%x%x+)", url = "./?id=%s" },
	{ key = "Reverts", pattern = "^(%x%x%x%x%x%x%x+)", url = "./?id=%s" },
	-- Closes: #123 and Bug: 123 point at the tracker.
	{ key = "Closes", pattern = "^#?(%d+)$", url = "https://bugs.example.com/?bug=%s" },
	{ key = "Bug", pattern = "^#?(%d+)$", url = "https://bugs.example.com/?bug=%s" },
	-- { key = "CVE", pattern = "^CVE%-(%d%d%d%d%-%d+)$",
	--   url = "https://www.cve.org/CVERecord?id=CVE-%s" },
}

-- A value that is nothing but one http or https URL, under any key, links to
-- itself. Set false to leave such values as text.
local link_urls = true


local key = ""
local chunks = {}

-- Percent-encode everything but the URL unreserved characters, so a captured
-- value cannot break out of the href attribute or out of the URL itself.
local function url_encode(s)
	return (string.gsub(s, "[^%w._~-]", function(c)
		return string.format("%%%02X", string.byte(c))
	end))
end

-- Build one anchor from a URL template holding %s and the text to show. The
-- replacement is a function so that a '%' in the encoded value is not taken
-- for a gsub reference.
local function make_link(url_template, capture, display)
	local encoded = url_encode(capture)
	local href = string.gsub(url_template, "%%s", function()
		return encoded
	end)
	return "<a href='" .. href .. "'>" .. display .. "</a>"
end

-- The value is already escaped, which is also what an attribute wants, so a
-- URL only has to be kept away from the quote that closes the attribute.
local function link_url(text)
	if not link_urls or not string.find(text, "^https?://[^%s'\"]+$") then
		return nil
	end
	return "<a href='" .. text .. "'>" .. text .. "</a>"
end

local function link_rule(text)
	local wanted = string.lower(key)
	for _, rule in ipairs(rules) do
		if rule.key == nil or string.lower(rule.key) == wanted then
			-- A malformed pattern is an operator error, so skip
			-- that rule rather than fail the whole page.
			local ok, start, stop, capture = pcall(string.find, text, rule.pattern)
			if ok and start then
				if capture == nil then
					capture = string.sub(text, start, stop)
				end
				return string.sub(text, 1, start - 1) ..
					make_link(rule.url, capture, string.sub(text, start, stop)) ..
					string.sub(text, stop + 1)
			end
		end
	end
	return nil
end

function filter_open(trailer_key, page)
	key = trailer_key or ""
	chunks = {}
end

function filter_write(str)
	chunks[#chunks + 1] = str
end

function filter_close()
	local text = table.concat(chunks)
	html(link_url(text) or link_rule(text) or text)
	return 0
end