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 }