From 4de7f7dca7485c9a6c94f6f86a53d01a974fce54 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 23 Jul 2026 14:39:31 +0200 Subject: web: HTMX server + lectio-web binary NewServer wires B1's RenderReadings/Themes/themeCSS/embedded static+ templates FS into an http.ServeMux: GET / (full page), GET /readings (HTMX reading-pane fragment), GET /lookup (bible.Lookup passage search fragment), GET /theme.css (theme stylesheet, falling back through cfg.WebTheme to the built-in default on an unknown name), GET /static/. Run listens on cfg.WebPort (0 = OS-picked free port), prints the URL, best-effort opens a browser, then serves. cmd/lectio-web is the binary entry point (config.Load -> web.Run). Fold-in from the B1 review: hardened themeCSS's name guard to an explicit ^[A-Za-z0-9_-]+$ allowlist (the old filepath.Base/ContainsAny check let ".." through), plus guard-rejection and HTML-escaping regression tests -- B2 is what makes /theme.css?name= reachable from the network, so it owns closing this out. --- internal/web/render_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'internal/web/render_test.go') diff --git a/internal/web/render_test.go b/internal/web/render_test.go index c42e1b1..87ee62d 100644 --- a/internal/web/render_test.go +++ b/internal/web/render_test.go @@ -30,6 +30,30 @@ func TestBuiltinThemes(t *testing.T) { } } +func TestThemeCSSGuardRejectsInvalidNames(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) // no user themes + for _, name := range []string{"../../etc/passwd", "..", "a/b", ""} { + if _, err := themeCSS(name); err == nil { + t.Errorf("themeCSS(%q): expected error, got nil", name) + } + } +} + +func TestRenderReadingsEscapesScriptText(t *testing.T) { + secs := []liturgy.Section{{ + Heading: "Test", + PartID: "pierwsze_czytanie", + Paragraphs: [][]string{{""}}, + }} + html := string(RenderReadings(secs, []string{"pl"}, "new")) + if strings.Contains(html, "") { + t.Errorf("raw