blob: c694d848dd9e8138393809ebb730d7e9edcf86a7 (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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
-- cgit email-filter that puts a Gravatar icon next to an author name. Enable
-- it with the email-filter or repo.email-filter setting and the lua: prefix,
-- so it runs in cgit's embedded interpreter with no per-request process. The
-- companion email-libravatar.lua is the same filter pointed at Libravatar
-- instead.
--
--     email-filter=lua:/path/to/email-gravatar.lua
--
-- Runs on Lua 5.1, 5.2, 5.3, 5.4 and LuaJIT. Lua 5.5 is not supported, because
-- luaossl has no 5.5 build.
--
-- The one dependency is luaossl, the OpenSSL binding that provides
-- openssl.digest, from <https://github.com/wahern/luaossl>. The reliable
-- cross-platform install is LuaRocks, matched to the Lua version cgit is built
-- against, and it needs the OpenSSL development headers present. Without it
-- the filter draws no icons and leaves the names as they are.
--
--     # Debian and Ubuntu
--     sudo apt install luarocks libssl-dev
--     sudo luarocks --lua-version 5.1 install luaossl
--
--     # Fedora
--     sudo dnf install luarocks openssl-devel
--     sudo luarocks --lua-version 5.1 install luaossl
--
--     # macOS with Homebrew
--     brew install luarocks openssl
--     luarocks install luaossl OPENSSL_DIR="$(brew --prefix openssl)"
--
-- Every page view sends the visitor's IP address and a hash of each
-- committer's email to a third-party service, so leave this filter off if
-- that is not acceptable for your instance. Addresses are hashed with
-- SHA-256, the form Gravatar documents.

local has_digest, digest = pcall(require, "openssl.digest")


-- The size is in pixels and serves both as the image asked of the service and
-- as the width and height attributes, while a screen of double density is
-- offered an image twice the size. The default image is what Gravatar draws for
-- an address it has never seen. The endpoint is https so the icon is not
-- blocked as mixed content on an https page.
local avatar_size = 13
local default_image = "retro"
local base_url = "https://gravatar.com/avatar/"


-- One name reaches this filter as an open, writes and a close, so what the
-- open works out is kept here for the close.
local buffer = ""
local avatar_hash = nil

local function hash_hex(input)
	local raw = digest.new("sha256"):final(input)
	local hex = ""
	for i = 1, #raw do
		hex = hex .. string.format("%.2x", string.byte(raw, i))
	end
	return hex
end

-- Lua patterns backtrack, so "^%s*(.-)%s*$" would cost the square of a long
-- run of spaces inside the address. Finding each end directly stays linear.
local function trim(text)
	local first = text:find("%S")
	if first == nil then
		return ""
	end
	local last = #text
	while text:find("^%s", last) do
		last = last - 1
	end
	return text:sub(first, last)
end

-- cgit can hand over the address still wrapped in angle brackets, and the
-- service hashes the trimmed lowercase form, so an address that skipped this
-- would hash to something the service has never heard of. A missing or empty
-- address becomes nil, which is how the caller knows to draw no icon.
local function normalize_email(email)
	if email == nil then
		return nil
	end
	-- The first "<" and the last ">" are found directly, because "<(.*)>"
	-- backtracks to the end of the address from every "<" in it.
	local open = email:find("<", 1, true)
	local close = email:find(">[^>]*$")
	if open and close and close > open then
		email = email:sub(open + 1, close - 1)
	end
	email = trim(email):lower()
	if email == "" then
		return nil
	end
	return email
end

function filter_open(email, page)
	buffer = ""
	local address = normalize_email(email)
	if address == nil or not has_digest then
		avatar_hash = nil
	else
		avatar_hash = hash_hex(address)
	end
end

function filter_write(text)
	buffer = buffer .. text
end

local function avatar_url(size)
	return base_url .. avatar_hash .. "?s=" .. size .. "&amp;d=" .. default_image
end

-- A log page can carry one icon per commit, so the attributes let the
-- browser defer the fetches for icons still off screen and keep decoding
-- off the critical path. The icon only decorates the name beside it, so its
-- alt text is empty and a screen reader goes straight to the name.
function filter_close()
	if avatar_hash == nil then
		html(buffer)
	else
		html("<img src='" .. avatar_url(avatar_size) ..
			"' srcset='" .. avatar_url(avatar_size * 2) .. " 2x" ..
			"' alt='' width='" .. avatar_size ..
			"' height='" .. avatar_size ..
			"' loading='lazy' decoding='async'> " .. buffer)
	end
	return 0
end