web: a failed form action's reason rides a one-shot cookie, not the URL !219

merged merged by cmc on 2026-09-04 02:56 UTC · krz/gitbay:flash-notice into main

9 files changed, +63 −52

Layout: unified · split

internal/httpd/account.go +3 −5
@@ -56,7 +56,7 @@ func (s *Server) accountForm(w http.ResponseWriter, r *http.Request, u store.Use
5656 Notice string
5757 Message string
5858 }{s.baseFor(u), "account", keys, pgp, emails, s.cfg.SiteHost(),
59 r.URL.Query().Get("e"), r.URL.Query().Get("m")})
59 s.takeFlash(w, r), r.URL.Query().Get("m")})
6060}
6161
6262// accountSubmit routes the account forms to their commands. Everything
@@ -64,12 +64,10 @@ func (s *Server) accountForm(w http.ResponseWriter, r *http.Request, u store.Use
6464func (s *Server) accountSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
6565 back := func(msg, note string) {
6666 q := ""
67 switch {
68 case msg != "":
69 q = "?e=" + url.QueryEscape(msg)
70 case note != "":
67 if note != "" {
7168 q = "?m=" + url.QueryEscape(note)
7269 }
70 s.setFlash(w, msg)
7371 http.Redirect(w, r, "/settings"+q, http.StatusSeeOther)
7472 }
7573
internal/httpd/builds.go +1 −1
@@ -45,7 +45,7 @@ func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
4545 Jobs []jobView
4646 CanWrite bool
4747 Notice string
48 }{p, builds, jobs, s.canWriteRepo(r, p.Repo), r.URL.Query().Get("e")})
48 }{p, builds, jobs, s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
4949}
5050
5151func (s *Server) build(w http.ResponseWriter, r *http.Request) {
internal/httpd/flash.go added +44
@@ -0,0 +1,44 @@
1package httpd
2
3import (
4 "net/http"
5 "net/url"
6)
7
8// A form action that fails redirects back to the page it came from with
9// the reason. The reason used to ride the URL as ?e=, so it survived a
10// reload and landed in history and bookmarks. It rides a one-shot cookie
11// now: set on the redirect, read and cleared by the page that renders it
12// (#119).
13const flashCookie = "gitbay_notice"
14
15// setFlash queues msg for the next page render. An empty msg sets
16// nothing.
17func (s *Server) setFlash(w http.ResponseWriter, msg string) {
18 if msg == "" {
19 return
20 }
21 if len(msg) > 300 {
22 msg = msg[:300]
23 }
24 http.SetCookie(w, &http.Cookie{
25 Name: flashCookie, Value: url.QueryEscape(msg), Path: "/",
26 HttpOnly: true, SameSite: http.SameSiteLaxMode,
27 Secure: s.cfg.HTTP.TLS != "off",
28 MaxAge: 60,
29 })
30}
31
32// takeFlash returns the queued message, if any, and clears it.
33func (s *Server) takeFlash(w http.ResponseWriter, r *http.Request) string {
34 c, err := r.Cookie(flashCookie)
35 if err != nil || c.Value == "" {
36 return ""
37 }
38 http.SetCookie(w, &http.Cookie{Name: flashCookie, Value: "", Path: "/", MaxAge: -1})
39 msg, err := url.QueryUnescape(c.Value)
40 if err != nil {
41 return ""
42 }
43 return msg
44}
internal/httpd/issueactions.go +1 −7
@@ -3,7 +3,6 @@ package httpd
33import (
44 "fmt"
55 "net/http"
6 "net/url"
76 "strings"
87
98 "gitbay.org/gitbay/internal/store"
@@ -16,12 +15,7 @@ import (
1615func (s *Server) issueRedirect(w http.ResponseWriter, r *http.Request, msg string) {
1716 dest := fmt.Sprintf("/%s/%s/issues/%s",
1817 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
19 if msg != "" {
20 if len(msg) > 300 {
21 msg = msg[:300]
22 }
23 dest += "?e=" + url.QueryEscape(msg)
24 }
18 s.setFlash(w, msg)
2519 http.Redirect(w, r, dest, http.StatusSeeOther)
2620}
2721
internal/httpd/mractions.go +5 −14
@@ -22,12 +22,7 @@ import (
2222func (s *Server) mrRedirect(w http.ResponseWriter, r *http.Request, msg string) {
2323 dest := fmt.Sprintf("/%s/%s/mrs/%s",
2424 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
25 if msg != "" {
26 if len(msg) > 300 {
27 msg = msg[:300]
28 }
29 dest += "?e=" + url.QueryEscape(msg)
30 }
25 s.setFlash(w, msg)
3126 http.Redirect(w, r, dest, http.StatusSeeOther)
3227}
3328
@@ -35,12 +30,7 @@ func (s *Server) mrRedirect(w http.ResponseWriter, r *http.Request, msg string)
3530func (s *Server) mrDiffRedirect(w http.ResponseWriter, r *http.Request, msg string) {
3631 dest := fmt.Sprintf("/%s/%s/mrs/%s?view=diff",
3732 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
38 if msg != "" {
39 if len(msg) > 300 {
40 msg = msg[:300]
41 }
42 dest += "&e=" + url.QueryEscape(msg)
43 }
33 s.setFlash(w, msg)
4434 http.Redirect(w, r, dest, http.StatusSeeOther)
4535}
4636
@@ -163,7 +153,7 @@ func (s *Server) mrCreateForm(w http.ResponseWriter, r *http.Request, u store.Us
163153 s.render(w, "mrnew.html", mrNewPage{
164154 repoPage: p, Branches: branches,
165155 Source: q.Get("source"), Target: target,
166 Title: q.Get("title"), Body: q.Get("body"), Notice: q.Get("e"),
156 Title: q.Get("title"), Body: q.Get("body"), Notice: s.takeFlash(w, r),
167157 })
168158}
169159
@@ -178,7 +168,8 @@ func (s *Server) mrCreateSubmit(w http.ResponseWriter, r *http.Request, u store.
178168 body := strings.TrimSpace(r.FormValue("body"))
179169
180170 back := func(msg string) {
181 q := url.Values{"source": {source}, "target": {target}, "title": {title}, "body": {body}, "e": {msg}}
171 q := url.Values{"source": {source}, "target": {target}, "title": {title}, "body": {body}}
172 s.setFlash(w, msg)
182173 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/new?%s", p.Repo.Path(), q.Encode()), http.StatusSeeOther)
183174 }
184175 if source == "" || title == "" {
internal/httpd/orgweb.go +2 −6
@@ -2,7 +2,6 @@ package httpd
22
33import (
44 "net/http"
5 "net/url"
65 "strings"
76
87 "gitbay.org/gitbay/internal/store"
@@ -45,11 +44,8 @@ func (s *Server) orgAdminView(viewer store.User, kind, name string) (teams []tea
4544func (s *Server) orgSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
4645 owner := r.PathValue("owner")
4746 back := func(msg string) {
48 dest := "/" + owner
49 if msg != "" {
50 dest += "?e=" + url.QueryEscape(msg)
51 }
52 http.Redirect(w, r, dest, http.StatusSeeOther)
47 s.setFlash(w, msg)
48 http.Redirect(w, r, "/"+owner, http.StatusSeeOther)
5349 }
5450 field := r.FormValue("field")
5551 team := strings.TrimSpace(r.FormValue("team"))
internal/httpd/releaseactions.go +1 −7
@@ -3,7 +3,6 @@ package httpd
33import (
44 "fmt"
55 "net/http"
6 "net/url"
76 "strings"
87
98 "gitbay.org/gitbay/internal/store"
@@ -15,12 +14,7 @@ import (
1514
1615func (s *Server) backTo(w http.ResponseWriter, r *http.Request, page, msg string) {
1716 dest := fmt.Sprintf("/%s/%s/%s", r.PathValue("owner"), r.PathValue("repo"), page)
18 if msg != "" {
19 if len(msg) > 300 {
20 msg = msg[:300]
21 }
22 dest += "?e=" + url.QueryEscape(msg)
23 }
17 s.setFlash(w, msg)
2418 http.Redirect(w, r, dest, http.StatusSeeOther)
2519}
2620
internal/httpd/settings.go +2 −8
@@ -3,7 +3,6 @@ package httpd
33import (
44 "fmt"
55 "net/http"
6 "net/url"
76 "strings"
87
98 "gitbay.org/gitbay/internal/gitutil"
@@ -39,18 +38,13 @@ func (s *Server) settingsForm(w http.ResponseWriter, r *http.Request, u store.Us
3938 s.render(w, "settings.html", settingsPage{
4039 repoPage: p, Topics: topics, Branches: branches,
4140 DepsEnabled: depsErr == nil,
42 Notice: r.URL.Query().Get("e"),
41 Notice: s.takeFlash(w, r),
4342 })
4443}
4544
4645func (s *Server) settingsRedirect(w http.ResponseWriter, r *http.Request, msg string) {
4746 dest := fmt.Sprintf("/%s/%s/settings", r.PathValue("owner"), r.PathValue("repo"))
48 if msg != "" {
49 if len(msg) > 300 {
50 msg = msg[:300]
51 }
52 dest += "?e=" + url.QueryEscape(msg)
53 }
47 s.setFlash(w, msg)
5448 http.Redirect(w, r, dest, http.StatusSeeOther)
5549}
5650
internal/httpd/web.go +4 −4
@@ -469,7 +469,7 @@ func (s *Server) ownerPage(w http.ResponseWriter, r *http.Request) {
469469 Notice string
470470 }{s.baseFor(viewer), name, d.Kind, profile, aboutHTML(profile),
471471 d.Repos, d.Members, d.Orgs,
472 weeks, activityTotal, teams, canAdmin, r.URL.Query().Get("e")})
472 weeks, activityTotal, teams, canAdmin, s.takeFlash(w, r)})
473473}
474474
475475func (s *Server) repoHome(w http.ResponseWriter, r *http.Request) {
@@ -665,7 +665,7 @@ func (s *Server) releases(w http.ResponseWriter, r *http.Request) {
665665 FreeTags []string
666666 CanWrite bool
667667 Notice string
668 }{p, views, freeTags, s.canWriteRepo(r, p.Repo), r.URL.Query().Get("e")})
668 }{p, views, freeTags, s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
669669}
670670
671671// releaseAsset streams one uploaded asset. Tags containing '/' are not
@@ -1640,7 +1640,7 @@ func (s *Server) issue(w http.ResponseWriter, r *http.Request) {
16401640 LabelColors map[string]template.CSS
16411641 }{p, iss, md(iss.Body, iss.BodyFormat), renderComments(comments, md),
16421642 s.canEditItem(r, p.Repo, iss.Author), s.canWriteRepo(r, p.Repo),
1643 milestones, r.URL.Query().Get("e"), s.labelColors(p.Repo.ID)})
1643 milestones, s.takeFlash(w, r), s.labelColors(p.Repo.ID)})
16441644}
16451645
16461646// canEditItem: the author or anyone with write access may edit.
@@ -1817,7 +1817,7 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
18171817 Stacked []store.MR
18181818 }{p, m, view, md(m.Body, m.BodyFormat), checks, combined, renderComments(comments, md),
18191819 reviews, files, diffTruncated, stat, commits, commitsTotal, branches, s.canEditItem(r, p.Repo, m.Author),
1820 canWrite, unresolved, r.URL.Query().Get("e"), detachedThreads, stackedOn, stacked})
1820 canWrite, unresolved, s.takeFlash(w, r), detachedThreads, stackedOn, stacked})
18211821}
18221822
18231823func (s *Server) refs(w http.ResponseWriter, r *http.Request) {