package web // What the operator does on the mail page (novox/hq ADR 0517 §1, §2, §4, §5; issue 1859): tick an item done, // undo it, remind it tomorrow, choose a mode. Each is a post RequireFormToken has already let through (the // session's own token, csrf.go), calling its one mail-reading verb for the signed-in operator; the page keeps // no state of its own (ADR 0440 §4). Posted by the page's script (X-Requested-With: fetch), it answers JSON: // done, where to go, or failed with the cause. Posted by a plain form, it goes back to the list, or shows the // list again saying the act failed and why — never dropped silently. import ( "context" "encoding/json" "net/http" "net/url" "strconv" "time" ) // byScript says whether the page's own script posted the form. func byScript(r *http.Request) bool { return r.Header.Get("X-Requested-With") == "fetch" } func answerJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } // actFailed says an act failed, with its cause: JSON to the script, the list page with the cause otherwise. func (b *Bundle) actFailed(w http.ResponseWriter, r *http.Request, mode Mode, cause string) { if byScript(r) { answerJSON(w, http.StatusBadGateway, map[string]any{"failed": cause}) return } ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() data := b.listDataQuiet(ctx, r, mode) data.Notice = cause renderNamed(w, http.StatusBadGateway, "list", data) } // actDone answers an act that succeeded: done to the script, or back to the page at back. func actDone(w http.ResponseWriter, r *http.Request, back string) { if byScript(r) { answerJSON(w, http.StatusOK, map[string]any{"done": true}) return } http.Redirect(w, r, back, http.StatusSeeOther) } // listAddress is the list page's own address for mode. func listAddress(mode Mode) string { return "/?" + url.Values{"mode": {mode.Name}}.Encode() } // itemHandler is POST /item (1859/AC5): act handle, unhandle or remind, on the item (a task, a promise, or // the mail itself) of the mail entry id, in the mode the page is in. func (b *Bundle) itemHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "post only", http.StatusMethodNotAllowed) return } mode := modeNamed(r.PostFormValue("mode")) id, err := strconv.Atoi(r.PostFormValue("id")) if err != nil || id <= 0 { http.Error(w, "an item needs its mail entry's id", http.StatusBadRequest) return } ask := map[string]func(context.Context, map[string]any) (json.RawMessage, error){ "handle": b.mailHandleAsk, "unhandle": b.mailUnhandleAsk, "remind": b.mailRemindAsk, }[r.PostFormValue("act")] if ask == nil { http.Error(w, "the act is none of handle, unhandle, remind", http.StatusBadRequest) return } args := map[string]any{"id": float64(id), "mode": mode.Name} if item := r.PostFormValue("item"); item != "" { args["item"] = item } ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() if _, err := ask(ctx, args); err != nil { b.actFailed(w, r, mode, "That did not happen: "+err.Error()) return } back := r.PostFormValue("back") if back == "" || back[0] != '/' || (len(back) > 1 && back[1] == '/') { back = listAddress(mode) // only an address of this page itself } actDone(w, r, back) } // modeHandler is POST /mode (1859/AC2, AC5): the mode chosen, recorded (ADR 0517 §4), then its list. func (b *Bundle) modeHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "post only", http.StatusMethodNotAllowed) return } mode := modeNamed(r.PostFormValue("mode")) ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() if _, err := b.mailRecordAsk(ctx, map[string]any{"what": "mode", "mode": mode.Name}); err != nil { b.actFailed(w, r, mode, "The mode chosen could not be kept: "+err.Error()) return } if byScript(r) { answerJSON(w, http.StatusOK, map[string]any{"done": true, "go": listAddress(mode)}) return } http.Redirect(w, r, listAddress(mode), http.StatusSeeOther) }