Files
llmgw/internal/web/contact.go
Ilker ManapandClaude Opus 5.5 11231d8c2b The portal in English and Turkish: a catalogue per language, a switcher at the foot of every page, landing.language, Turkish numbers and dates
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>
2026-10-05 00:12:53 +02:00

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:], ".")
}