| 1 | package memoria |
| 2 | |
| 3 | import ( |
| 4 | "encoding/base64" |
| 5 | "net/http" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // A notice is a one-shot sentence carried across a redirect in a cookie: |
| 10 | // rastrillo's flash, with one difference that is the reason this file |
| 11 | // exists. Each page that shows a notice has its own cookie. |
| 12 | // |
| 13 | // With one shared cookie, whichever page loaded first took the notice, |
| 14 | // whether or not it was the page the notice was about. After an upload, |
| 15 | // visiting the index spent the sentence naming the refused files, so /add |
| 16 | // was left with the bare counts in its URL. A failed delete was spent by |
| 17 | // a visit to /add, which has no place to show it. Worst of all, /add's |
| 18 | // own background refresh spent it, and that refresh only swaps the |
| 19 | // footer, so nobody saw it at all. |
| 20 | // |
| 21 | // A notice may name a file, and a filename is content. This cookie is the |
| 22 | // one place content travels outside a page body: HttpOnly, sixty |
| 23 | // seconds, spent on first display, and sent only to the browser that is |
| 24 | // about to be shown the same words. It never goes into a log line, an |
| 25 | // error string or a URL. |
| 26 | type notice string |
| 27 | |
| 28 | const ( |
| 29 | // addNotice is the outcome of a submission, shown on /add. |
| 30 | addNotice notice = "memoria_notice_add" |
| 31 | // indexNotice is a failed delete, shown on the index. |
| 32 | indexNotice notice = "memoria_notice_index" |
| 33 | ) |
| 34 | |
| 35 | // refreshHeader marks drop.js's background re-fetch of the page. The |
| 36 | // refresh swaps only the regions marked rst-refresh, and a notice is |
| 37 | // never inside one, so the re-fetch must leave the notice unspent. |
| 38 | const refreshHeader = "X-Memoria-Refresh" |
| 39 | |
| 40 | func (n notice) set(w http.ResponseWriter, kind, message string) { |
| 41 | http.SetCookie(w, &http.Cookie{ |
| 42 | Name: string(n), |
| 43 | Value: base64.RawURLEncoding.EncodeToString([]byte(kind + "\x00" + message)), |
| 44 | Path: "/", |
| 45 | HttpOnly: true, |
| 46 | SameSite: http.SameSiteLaxMode, |
| 47 | MaxAge: 60, |
| 48 | }) |
| 49 | } |
| 50 | |
| 51 | // take returns the notice and clears it. It returns nothing and clears |
| 52 | // nothing on a refresh request, because the refresh never shows a notice. |
| 53 | func (n notice) take(w http.ResponseWriter, r *http.Request) (kind, message string, ok bool) { |
| 54 | if r.Header.Get(refreshHeader) != "" { |
| 55 | return "", "", false |
| 56 | } |
| 57 | c, err := r.Cookie(string(n)) |
| 58 | if err != nil { |
| 59 | return "", "", false |
| 60 | } |
| 61 | http.SetCookie(w, &http.Cookie{ |
| 62 | Name: string(n), Value: "", Path: "/", |
| 63 | HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: -1, |
| 64 | }) |
| 65 | data, err := base64.RawURLEncoding.DecodeString(c.Value) |
| 66 | if err != nil { |
| 67 | return "", "", false |
| 68 | } |
| 69 | kind, message, found := strings.Cut(string(data), "\x00") |
| 70 | if !found || message == "" { |
| 71 | return "", "", false |
| 72 | } |
| 73 | return kind, message, true |
| 74 | } |
| 75 | |