package handler import ( "html/template" "log/slog" "math" "net/http" "strconv" "github.com/go-chi/chi/v5" ) // parseID writes the 400 itself and reports false, so callers can just return. func parseID(w http.ResponseWriter, r *http.Request) (int64, bool) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return 0, false } return id, true } // sortFromRequest remembers an explicit ?sort= in a cookie and restores it // otherwise, so the order survives the auto-refresh that reloads these pages. func sortFromRequest(w http.ResponseWriter, r *http.Request, cookie, defaultSort string) string { sortBy := r.URL.Query().Get("sort") if sortBy == "" { return getCookie(r, cookie, defaultSort) } setCookie(w, cookie, sortBy) return sortBy } // pageSize: the library page fires one thumbnail request per media file, so an // unpaginated large archive is both a slow render and a request storm. const pageSize = 50 // Query holds the request's other parameters (filter, sort, path) with a // trailing '&', so a link keeps them. It is template.URL because Encode already // percent-encoded it: as a plain string the template escaper would treat the // whole query as one value and re-encode its '&' and '='. type Pagination struct { Page int Prev int Next int HasNext bool Query template.URL } // pageFromRequest reads the 1-based ?page=. Unparseable, below 1, or large // enough to overflow page*pageSize all mean page 1: a hand-edited URL must not // produce a negative offset. func pageFromRequest(r *http.Request) int { p, err := strconv.Atoi(r.URL.Query().Get("page")) if err != nil || p < 1 || p > math.MaxInt/pageSize { return 1 } return p } // hasNext comes from the caller: each listing determines it differently. func newPagination(r *http.Request, page int, hasNext bool) Pagination { q := r.URL.Query() q.Del("page") query := q.Encode() if query != "" { query += "&" } return Pagination{ Page: page, Prev: page - 1, Next: page + 1, HasNext: hasNext, Query: template.URL(query), } } // serverError keeps the error out of the response: internal error strings can // carry filesystem paths and SQL text. func (h *Handler) serverError(w http.ResponseWriter, r *http.Request, context string, err error) { slog.Error(context, "method", r.Method, "path", r.URL.Path, "err", err) http.Error(w, "Something went wrong. Please try again.", http.StatusInternalServerError) } // redirectWithError is the Post/Redirect/Get failure path for forms. The // underlying error is logged rather than shown. func redirectWithError(w http.ResponseWriter, r *http.Request, path, message string, err error) { if err != nil { slog.Error(message, "method", r.Method, "path", r.URL.Path, "err", err) } flashError(w, message) http.Redirect(w, r, path, http.StatusSeeOther) } func redirectWithSuccess(w http.ResponseWriter, r *http.Request, path, message string) { flashSuccess(w, message) http.Redirect(w, r, path, http.StatusSeeOther) } // parseForm writes a generic 400 itself: ParseForm's own error names the // offending bytes, which is noise to the user and detail not worth handing out. func parseForm(w http.ResponseWriter, r *http.Request) bool { if err := r.ParseForm(); err != nil { http.Error(w, "Invalid form", http.StatusBadRequest) return false } return true }