Every template and handler message in internal/web goes through a JSON
catalogue under internal/web/locales/<code>/, with English as the fallback.
The language is the person's choice (a cookie set by /_lang/{code}), else
Accept-Language, else landing.language. Tests refuse a key some language
lacks, a translation with other arguments than English, and template text
left in English.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
60 lines
1.9 KiB
Go
60 lines
1.9 KiB
Go
package web
|
|
|
|
import (
|
|
"html/template"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// contactLink decides how to render the configured contact.
|
|
//
|
|
// An address becomes a mailto:, an http(s) URL stays a link, and anything else
|
|
// — a channel name, an internal ticket queue, a person — is shown as text.
|
|
//
|
|
// The scheme allowlist is the point. This string comes from configuration, but
|
|
// configuration is not always written by the person who runs the gateway, and a
|
|
// javascript: or data: href on a page served to every visitor is a stored XSS
|
|
// with a very short path to it. Templates escape the text; they do not make a
|
|
// scheme safe.
|
|
func contactLink(contact string) (text, href string) {
|
|
c := strings.TrimSpace(contact)
|
|
if c == "" {
|
|
return "", ""
|
|
}
|
|
|
|
if u, err := url.Parse(c); err == nil {
|
|
switch strings.ToLower(u.Scheme) {
|
|
case "http", "https", "mailto":
|
|
return c, c
|
|
case "":
|
|
// No scheme. A bare address is the common case and worth linking.
|
|
if looksLikeEmail(c) {
|
|
return c, "mailto:" + c
|
|
}
|
|
}
|
|
}
|
|
return c, ""
|
|
}
|
|
|
|
// contactMarkup is the contact as a link, or as emphasised text when it is not
|
|
// one. Both parts are escaped here, because the sentence it goes into is
|
|
// markup; the href has already passed contactLink's scheme allowlist.
|
|
func contactMarkup(text, href string) template.HTML {
|
|
esc := template.HTMLEscapeString
|
|
if href != "" {
|
|
return template.HTML(`<a href="` + esc(href) + `">` + esc(text) + `</a>`) //nolint:gosec // escaped, and the scheme is allowlisted
|
|
}
|
|
return template.HTML(`<strong>` + esc(text) + `</strong>`) //nolint:gosec // escaped
|
|
}
|
|
|
|
// looksLikeEmail is deliberately crude: it decides whether to add a mailto:,
|
|
// and being wrong means a link that does not open a mail client rather than
|
|
// anything worse.
|
|
func looksLikeEmail(s string) bool {
|
|
at := strings.IndexByte(s, '@')
|
|
if at <= 0 || at == len(s)-1 || strings.ContainsAny(s, " \t/") {
|
|
return false
|
|
}
|
|
return strings.Contains(s[at+1:], ".")
|
|
}
|