package web
import (
"html/template"
"net/http"
"regexp"
"strings"
"munin/internal/auth"
"munin/internal/help"
)
// helpData sind die Daten einer Hilfeseite.
type helpData struct {
Page help.Page // die angezeigte Seite
Pages []help.Page // alle Seiten, für die Navigation
HTML template.HTML // der gerenderte Inhalt
}
// handleHelp zeigt eine Seite der eingebauten Onboarding-Dokumentation.
//
// Erreichbar für jede angemeldete Sitzung, unabhängig von Rolle und Projekt:
// Die Hilfe enthält nichts, was nicht jeder Nutzer wissen darf. Auch ein
// Plattform-Admin, der noch keinen Workspace gewählt hat, kommt hierher.
//
// /hilfe ohne Seitenangabe zeigt die Startseite. Deshalb kommt der Slug aus
// r.PathValue und fällt auf "start" zurück, wenn er leer ist.
func (s *Server) handleHelp(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("seite")
if slug == "" {
slug = "start"
}
sess, _ := SessionFrom(r)
// Eine Admin-Seite antwortet für alle anderen mit 404, genau wie eine
// Seite, die es nicht gibt — sie steht für sie ja auch nirgends verlinkt.
reader := help.Reader{PlatformAdmin: sess.IsPlatformAdmin, WorkspaceAdmin: sess.IsWorkspaceAdmin}
page, ok := help.Find(slug)
if !ok || !page.CanRead(reader) {
http.NotFound(w, r)
return
}
markdown, err := page.Markdown()
if err != nil {
s.serverError(w, "Hilfeseite konnte nicht geladen werden", err)
return
}
// Platzhalter ersetzen, damit die Beispiele direkt kopierbar sind: mit der
// Adresse dieses Servers und dem Bezeichner des eigenen Projekts statt
// mit "DEIN-SERVER". Das passiert vor dem Rendern — goldmark sieht nur
// noch fertigen Text und maskiert ihn wie jeden anderen.
projectSlug := "mein-projekt"
if sess.Project.Slug != "" {
projectSlug = sess.Project.Slug
}
markdown = strings.NewReplacer(
"{{SERVER}}", serverURL(r),
"{{PROJEKT}}", projectSlug,
).Replace(markdown)
content, err := help.Render(markdown)
if err != nil {
s.serverError(w, "Hilfeseite konnte nicht dargestellt werden", err)
return
}
data := pageData{
Title: page.Title,
Section: "hilfe",
Data: helpData{
Page: page,
Pages: help.Visible(reader),
HTML: template.HTML(content),
},
}
// Ein Projekt-Key soll auch auf der Hilfe seine gewohnte Seitenleiste
// sehen (Einträge, Graph, Schlagworte). render() übernimmt das Projekt
// sonst nur aus der URL, und /hilfe hat keins.
if !sess.IsPlatformAdmin {
data.Project = sess.Project
}
s.render(w, r, "help.html", http.StatusOK, data)
}
// validHost erlaubt nur, was in einem Hostnamen mit Port vorkommen kann:
// Buchstaben, Ziffern, Punkt, Bindestrich, Doppelpunkt und die eckigen
// Klammern einer IPv6-Adresse.
var validHost = regexp.MustCompile(`^[A-Za-z0-9.\-:\[\]]+$`)
// serverURL errät die Adresse, unter der dieser Server von außen erreichbar
// ist — für die Beispiele in der Hilfe.
//
// # Warum nur "erraten"?
//
// Der Host-Header kommt vom Browser und damit vom Nutzer selbst. Das ist hier
// unbedenklich: Das Ergebnis landet nur in der Seite, die derselbe Nutzer
// gerade abruft, und wird nirgends gespeichert. Trotzdem wird der Wert
// geprüft, und bei allem Ungewöhnlichen steht ein Platzhalter da.
//
// Das Schema ist schwerer zu erkennen: Hinter einem Reverse-Proxy, der TLS
// beendet, kommt die Anfrage hier als http an. Deshalb gilt: https, außer bei
// localhost ohne TLS — dieselbe Regel wie beim Secure-Flag der Cookies, und
// deshalb auch dieselbe Funktion (auth.IsLocalhost).
func serverURL(r *http.Request) string {
host := r.Host
if host == "" || !validHost.MatchString(host) {
return "https://DEIN-SERVER"
}
schema := "https"
if r.TLS == nil && auth.IsLocalhost(r) {
schema = "http"
}
return schema + "://" + host
}