106 lines
2.6 KiB
Go
106 lines
2.6 KiB
Go
package web
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"net/http"
|
||
"strings"
|
||
)
|
||
|
||
// Flash is a one-shot notification shown after a redirect.
|
||
//
|
||
// The interface uses HTTP Basic authentication and therefore has no session to
|
||
// hang messages off, so a flash travels in a short-lived cookie that is
|
||
// cleared as soon as it is rendered. This keeps the post/redirect/get pattern
|
||
// intact: a refresh after saving never re-submits the form.
|
||
type Flash struct {
|
||
Level string `json:"l"` // success, danger, warning, info
|
||
Message string `json:"m"`
|
||
}
|
||
|
||
const flashCookie = "vibedns_flash"
|
||
|
||
// maxFlashCookie bounds the cookie so a very long error message cannot exceed
|
||
// what browsers accept.
|
||
const maxFlashCookie = 3500
|
||
|
||
// setFlash queues a message for the next page render.
|
||
func setFlash(w http.ResponseWriter, r *http.Request, level, message string) {
|
||
f := Flash{Level: level, Message: message}
|
||
raw, err := json.Marshal([]Flash{f})
|
||
if err != nil {
|
||
return
|
||
}
|
||
value := base64.RawURLEncoding.EncodeToString(raw)
|
||
if len(value) > maxFlashCookie {
|
||
short := Flash{Level: level, Message: truncate(600, message)}
|
||
raw, _ = json.Marshal([]Flash{short})
|
||
value = base64.RawURLEncoding.EncodeToString(raw)
|
||
}
|
||
http.SetCookie(w, &http.Cookie{
|
||
Name: flashCookie,
|
||
Value: value,
|
||
Path: "/",
|
||
HttpOnly: true,
|
||
Secure: r.TLS != nil,
|
||
SameSite: http.SameSiteLaxMode,
|
||
MaxAge: 60,
|
||
})
|
||
}
|
||
|
||
// takeFlashes reads and clears any queued messages.
|
||
func takeFlashes(w http.ResponseWriter, r *http.Request) []Flash {
|
||
c, err := r.Cookie(flashCookie)
|
||
if err != nil || c.Value == "" {
|
||
return nil
|
||
}
|
||
// Clear it immediately so a refresh does not show the message twice.
|
||
http.SetCookie(w, &http.Cookie{
|
||
Name: flashCookie,
|
||
Value: "",
|
||
Path: "/",
|
||
HttpOnly: true,
|
||
Secure: r.TLS != nil,
|
||
SameSite: http.SameSiteLaxMode,
|
||
MaxAge: -1,
|
||
})
|
||
|
||
raw, err := base64.RawURLEncoding.DecodeString(c.Value)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var out []Flash
|
||
if err := json.Unmarshal(raw, &out); err != nil {
|
||
return nil
|
||
}
|
||
for i := range out {
|
||
switch out[i].Level {
|
||
case "success", "danger", "warning", "info":
|
||
default:
|
||
out[i].Level = "info"
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// jsonMarshal encodes a value for embedding inside a <script> block.
|
||
//
|
||
// Go's html/template will not escape inside a script context, so the sequences
|
||
// that could terminate the element or be reinterpreted by a JavaScript parser
|
||
// are escaped here.
|
||
func jsonMarshal(v any) ([]byte, error) {
|
||
b, err := json.Marshal(v)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
s := string(b)
|
||
r := strings.NewReplacer(
|
||
"<", `<`,
|
||
">", `>`,
|
||
"&", `&`,
|
||
"
", `
`,
|
||
"
", `
`,
|
||
)
|
||
return []byte(r.Replace(s)), nil
|
||
}
|