Commit 467699a1ed
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
e2e/webwrites_test.go +6
| @@ -50,6 +50,12 @@ func TestWebWritesGoThroughRegistry(t *testing.T) { | |||
| 50 | t.Fatalf("repo created past the quota from the web: exit %d, want 3", code) | 50 | t.Fatalf("repo created past the quota from the web: exit %d, want 3", code) |
| 51 | } | 51 | } |
| 52 | 52 | ||
| 53 | // A form action on an issue that does not exist is the 404 page, not a | ||
| 54 | // redirect carrying a message (#106). | ||
| 55 | if status, _ := browserPost(t, browser, inst.base()+"/alice/first/issues/999/state", url.Values{"action": {"close"}}); status != 404 { | ||
| 56 | t.Fatalf("closing a missing issue from the web: %d, want 404", status) | ||
| 57 | } | ||
| 58 | |||
| 53 | // An archived repository refuses a comment from the web as it does | 59 | // An archived repository refuses a comment from the web as it does |
| 54 | // over ssh. | 60 | // over ssh. |
| 55 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "archive", "alice/first"); code != 0 { | 61 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "archive", "alice/first"); code != 0 { |
internal/httpd/control.go +35 −4
| @@ -20,6 +20,14 @@ import ( | |||
| 20 | // ViaAPI is set, which refuses SSHOnly commands: anything whose input is a | 20 | // ViaAPI is set, which refuses SSHOnly commands: anything whose input is a |
| 21 | // credential (secrets, mirror tokens, session minting) stays on SSH. | 21 | // credential (secrets, mirror tokens, session minting) stays on SSH. |
| 22 | func (s *Server) runControl(u store.User, argv []string) (out string, msg string, ok bool) { | 22 | func (s *Server) runControl(u store.User, argv []string) (out string, msg string, ok bool) { |
| 23 | out, msg, code := s.runControlCode(u, argv) | ||
| 24 | return out, msg, code == protocol.ExitOK | ||
| 25 | } | ||
| 26 | |||
| 27 | // runControlCode is runControl with the exit code, for handlers that | ||
| 28 | // answer a form: not-found and denied deserve their own statuses rather | ||
| 29 | // than a redirect carrying the message (#106). | ||
| 30 | func (s *Server) runControlCode(u store.User, argv []string) (out string, msg string, code int) { | ||
| 23 | var stdout, stderr bytes.Buffer | 31 | var stdout, stderr bytes.Buffer |
| 24 | ctx := &control.Ctx{ | 32 | ctx := &control.Ctx{ |
| 25 | User: u, | 33 | User: u, |
| @@ -32,12 +40,30 @@ func (s *Server) runControl(u store.User, argv []string) (out string, msg string | |||
| 32 | Stderr: &stderr, | 40 | Stderr: &stderr, |
| 33 | ViaAPI: true, | 41 | ViaAPI: true, |
| 34 | } | 42 | } |
| 35 | code := control.Dispatch(ctx, argv) | 43 | code = control.Dispatch(ctx, argv) |
| 36 | m := strings.TrimSpace(stderr.String()) | 44 | m := strings.TrimSpace(stderr.String()) |
| 37 | if m == "" { | 45 | if m == "" { |
| 38 | m = strings.TrimSpace(stdout.String()) | 46 | m = strings.TrimSpace(stdout.String()) |
| 39 | } | 47 | } |
| 40 | return stdout.String(), m, code == protocol.ExitOK | 48 | return stdout.String(), m, code |
| 49 | } | ||
| 50 | |||
| 51 | // done finishes a form action by exit code: back to the page on success, | ||
| 52 | // the 404 page when the thing does not exist, and back to the page with | ||
| 53 | // the message for anything else. A refusal is feedback on the page a | ||
| 54 | // person was looking at, whether it is a merge gate, a permission they | ||
| 55 | // lack, or a field they got wrong; only a thing that does not exist has | ||
| 56 | // no page to go back to. | ||
| 57 | func (s *Server) done(w http.ResponseWriter, r *http.Request, code int, msg string, | ||
| 58 | redirect func(http.ResponseWriter, *http.Request, string)) { | ||
| 59 | switch code { | ||
| 60 | case protocol.ExitOK: | ||
| 61 | redirect(w, r, "") | ||
| 62 | case protocol.ExitNotFound: | ||
| 63 | s.notFound(w, r) | ||
| 64 | default: | ||
| 65 | redirect(w, r, msg) | ||
| 66 | } | ||
| 41 | } | 67 | } |
| 42 | 68 | ||
| 43 | // runControlStdin is runControl for the handful of commands whose input | 69 | // runControlStdin is runControl for the handful of commands whose input |
| @@ -46,6 +72,11 @@ func (s *Server) runControl(u store.User, argv []string) (out string, msg string | |||
| 46 | // tokens and mirror credentials remain SSHOnly and are refused by the | 72 | // tokens and mirror credentials remain SSHOnly and are refused by the |
| 47 | // dispatcher. | 73 | // dispatcher. |
| 48 | func (s *Server) runControlStdin(u store.User, argv []string, stdin string) (msg string, ok bool) { | 74 | func (s *Server) runControlStdin(u store.User, argv []string, stdin string) (msg string, ok bool) { |
| 75 | msg, code := s.runControlStdinCode(u, argv, stdin) | ||
| 76 | return msg, code == protocol.ExitOK | ||
| 77 | } | ||
| 78 | |||
| 79 | func (s *Server) runControlStdinCode(u store.User, argv []string, stdin string) (msg string, code int) { | ||
| 49 | var stdout, stderr bytes.Buffer | 80 | var stdout, stderr bytes.Buffer |
| 50 | ctx := &control.Ctx{ | 81 | ctx := &control.Ctx{ |
| 51 | User: u, | 82 | User: u, |
| @@ -58,12 +89,12 @@ func (s *Server) runControlStdin(u store.User, argv []string, stdin string) (msg | |||
| 58 | Stderr: &stderr, | 89 | Stderr: &stderr, |
| 59 | ViaAPI: true, | 90 | ViaAPI: true, |
| 60 | } | 91 | } |
| 61 | code := control.Dispatch(ctx, argv) | 92 | code = control.Dispatch(ctx, argv) |
| 62 | m := strings.TrimSpace(stderr.String()) | 93 | m := strings.TrimSpace(stderr.String()) |
| 63 | if m == "" { | 94 | if m == "" { |
| 64 | m = strings.TrimSpace(stdout.String()) | 95 | m = strings.TrimSpace(stdout.String()) |
| 65 | } | 96 | } |
| 66 | return m, code == protocol.ExitOK | 97 | return m, code |
| 67 | } | 98 | } |
| 68 | 99 | ||
| 69 | // runControlInto runs a command in JSON mode and decodes its data into | 100 | // runControlInto runs a command in JSON mode and decodes its data into |
internal/httpd/issueactions.go +8 −20
| @@ -36,11 +36,8 @@ func (s *Server) issueStateSubmit(w http.ResponseWriter, r *http.Request, u stor | |||
| 36 | if r.FormValue("action") == "reopen" { | 36 | if r.FormValue("action") == "reopen" { |
| 37 | verb = "reopen" | 37 | verb = "reopen" |
| 38 | } | 38 | } |
| 39 | _, msg, ok := s.runControl(u, issueArgs(r, verb)) | 39 | _, msg, code := s.runControlCode(u, issueArgs(r, verb)) |
| 40 | if ok { | 40 | s.done(w, r, code, msg, s.issueRedirect) |
| 41 | msg = "" | ||
| 42 | } | ||
| 43 | s.issueRedirect(w, r, msg) | ||
| 44 | } | 41 | } |
| 45 | 42 | ||
| 46 | // fieldArgs turns a space-separated form value into repeated flags, the | 43 | // fieldArgs turns a space-separated form value into repeated flags, the |
| @@ -59,11 +56,8 @@ func (s *Server) issueLabelSubmit(w http.ResponseWriter, r *http.Request, u stor | |||
| 59 | s.issueRedirect(w, r, "name at least one label") | 56 | s.issueRedirect(w, r, "name at least one label") |
| 60 | return | 57 | return |
| 61 | } | 58 | } |
| 62 | _, msg, ok := s.runControl(u, issueArgs(r, "label", args...)) | 59 | _, msg, code := s.runControlCode(u, issueArgs(r, "label", args...)) |
| 63 | if ok { | 60 | s.done(w, r, code, msg, s.issueRedirect) |
| 64 | msg = "" | ||
| 65 | } | ||
| 66 | s.issueRedirect(w, r, msg) | ||
| 67 | } | 61 | } |
| 68 | 62 | ||
| 69 | func (s *Server) issueAssignSubmit(w http.ResponseWriter, r *http.Request, u store.User) { | 63 | func (s *Server) issueAssignSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -72,11 +66,8 @@ func (s *Server) issueAssignSubmit(w http.ResponseWriter, r *http.Request, u sto | |||
| 72 | s.issueRedirect(w, r, "name at least one person") | 66 | s.issueRedirect(w, r, "name at least one person") |
| 73 | return | 67 | return |
| 74 | } | 68 | } |
| 75 | _, msg, ok := s.runControl(u, issueArgs(r, "assign", args...)) | 69 | _, msg, code := s.runControlCode(u, issueArgs(r, "assign", args...)) |
| 76 | if ok { | 70 | s.done(w, r, code, msg, s.issueRedirect) |
| 77 | msg = "" | ||
| 78 | } | ||
| 79 | s.issueRedirect(w, r, msg) | ||
| 80 | } | 71 | } |
| 81 | 72 | ||
| 82 | func (s *Server) issueMilestoneSubmit(w http.ResponseWriter, r *http.Request, u store.User) { | 73 | func (s *Server) issueMilestoneSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -84,9 +75,6 @@ func (s *Server) issueMilestoneSubmit(w http.ResponseWriter, r *http.Request, u | |||
| 84 | if title == "" { | 75 | if title == "" { |
| 85 | title = "none" | 76 | title = "none" |
| 86 | } | 77 | } |
| 87 | _, msg, ok := s.runControl(u, issueArgs(r, "milestone", title)) | 78 | _, msg, code := s.runControlCode(u, issueArgs(r, "milestone", title)) |
| 88 | if ok { | 79 | s.done(w, r, code, msg, s.issueRedirect) |
| 89 | msg = "" | ||
| 90 | } | ||
| 91 | s.issueRedirect(w, r, msg) | ||
| 92 | } | 80 | } |
internal/httpd/mractions.go +12 −30
| @@ -59,11 +59,8 @@ func (s *Server) mrReviewSubmit(w http.ResponseWriter, r *http.Request, u store. | |||
| 59 | s.mrRedirect(w, r, "pick approve, request changes, or comment") | 59 | s.mrRedirect(w, r, "pick approve, request changes, or comment") |
| 60 | return | 60 | return |
| 61 | } | 61 | } |
| 62 | _, msg, ok := s.runControl(u, mrArgs(r, "review", flag)) | 62 | _, msg, code := s.runControlCode(u, mrArgs(r, "review", flag)) |
| 63 | if ok { | 63 | s.done(w, r, code, msg, s.mrRedirect) |
| 64 | msg = "" | ||
| 65 | } | ||
| 66 | s.mrRedirect(w, r, msg) | ||
| 67 | } | 64 | } |
| 68 | 65 | ||
| 69 | func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.User) { | 66 | func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -71,19 +68,13 @@ func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.U | |||
| 71 | if st := strings.TrimSpace(r.FormValue("strategy")); st != "" && st != "auto" { | 68 | if st := strings.TrimSpace(r.FormValue("strategy")); st != "" && st != "auto" { |
| 72 | args = append(args, "--strategy", st) | 69 | args = append(args, "--strategy", st) |
| 73 | } | 70 | } |
| 74 | _, msg, ok := s.runControl(u, mrArgs(r, "merge", args...)) | 71 | _, msg, code := s.runControlCode(u, mrArgs(r, "merge", args...)) |
| 75 | if ok { | 72 | s.done(w, r, code, msg, s.mrRedirect) |
| 76 | msg = "" | ||
| 77 | } | ||
| 78 | s.mrRedirect(w, r, msg) | ||
| 79 | } | 73 | } |
| 80 | 74 | ||
| 81 | func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) { | 75 | func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 82 | _, msg, ok := s.runControl(u, mrArgs(r, "close")) | 76 | _, msg, code := s.runControlCode(u, mrArgs(r, "close")) |
| 83 | if ok { | 77 | s.done(w, r, code, msg, s.mrRedirect) |
| 84 | msg = "" | ||
| 85 | } | ||
| 86 | s.mrRedirect(w, r, msg) | ||
| 87 | } | 78 | } |
| 88 | 79 | ||
| 89 | // mrDiffCommentSubmit opens a review thread on a diff line, or replies to | 80 | // mrDiffCommentSubmit opens a review thread on a diff line, or replies to |
| @@ -114,11 +105,8 @@ func (s *Server) mrDiffCommentSubmit(w http.ResponseWriter, r *http.Request, u s | |||
| 114 | extra = append(extra, "--old") | 105 | extra = append(extra, "--old") |
| 115 | } | 106 | } |
| 116 | } | 107 | } |
| 117 | msg, ok := s.runControlStdin(u, mrArgs(r, "diff-comment", append(extra, "--file", "-")...), body) | 108 | msg, code := s.runControlStdinCode(u, mrArgs(r, "diff-comment", append(extra, "--file", "-")...), body) |
| 118 | if ok { | 109 | s.done(w, r, code, msg, s.mrDiffRedirect) |
| 119 | msg = "" | ||
| 120 | } | ||
| 121 | s.mrDiffRedirect(w, r, msg) | ||
| 122 | } | 110 | } |
| 123 | 111 | ||
| 124 | // mrRetargetSubmit moves the merge request onto another branch. | 112 | // mrRetargetSubmit moves the merge request onto another branch. |
| @@ -128,11 +116,8 @@ func (s *Server) mrRetargetSubmit(w http.ResponseWriter, r *http.Request, u stor | |||
| 128 | s.mrRedirect(w, r, "pick a branch to retarget onto") | 116 | s.mrRedirect(w, r, "pick a branch to retarget onto") |
| 129 | return | 117 | return |
| 130 | } | 118 | } |
| 131 | _, msg, ok := s.runControl(u, mrArgs(r, "retarget", target)) | 119 | _, msg, code := s.runControlCode(u, mrArgs(r, "retarget", target)) |
| 132 | if ok { | 120 | s.done(w, r, code, msg, s.mrRedirect) |
| 133 | msg = "" | ||
| 134 | } | ||
| 135 | s.mrRedirect(w, r, msg) | ||
| 136 | } | 121 | } |
| 137 | 122 | ||
| 138 | // mrThreadSubmit resolves or reopens one review thread. | 123 | // mrThreadSubmit resolves or reopens one review thread. |
| @@ -146,11 +131,8 @@ func (s *Server) mrThreadSubmit(w http.ResponseWriter, r *http.Request, u store. | |||
| 146 | s.mrRedirect(w, r, "bad thread id") | 131 | s.mrRedirect(w, r, "bad thread id") |
| 147 | return | 132 | return |
| 148 | } | 133 | } |
| 149 | _, msg, ok := s.runControl(u, mrArgs(r, verb, id)) | 134 | _, msg, code := s.runControlCode(u, mrArgs(r, verb, id)) |
| 150 | if ok { | 135 | s.done(w, r, code, msg, s.mrRedirect) |
| 151 | msg = "" | ||
| 152 | } | ||
| 153 | s.mrRedirect(w, r, msg) | ||
| 154 | } | 136 | } |
| 155 | 137 | ||
| 156 | // mrNewPage is the create form: branches to choose from, plus whatever | 138 | // mrNewPage is the create form: branches to choose from, plus whatever |
internal/httpd/releaseactions.go +4 −10
| @@ -45,11 +45,8 @@ func (s *Server) releaseSubmit(w http.ResponseWriter, r *http.Request, u store.U | |||
| 45 | if notes != "" || verb == "edit" { | 45 | if notes != "" || verb == "edit" { |
| 46 | argv = append(argv, "--notes", notes) | 46 | argv = append(argv, "--notes", notes) |
| 47 | } | 47 | } |
| 48 | _, msg, ok := s.runControl(u, argv) | 48 | _, msg, code := s.runControlCode(u, argv) |
| 49 | if ok { | 49 | s.done(w, r, code, msg, func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "releases", msg) }) |
| 50 | msg = "" | ||
| 51 | } | ||
| 52 | s.backTo(w, r, "releases", msg) | ||
| 53 | } | 50 | } |
| 54 | 51 | ||
| 55 | func (s *Server) buildTriggerSubmit(w http.ResponseWriter, r *http.Request, u store.User) { | 52 | func (s *Server) buildTriggerSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -59,9 +56,6 @@ func (s *Server) buildTriggerSubmit(w http.ResponseWriter, r *http.Request, u st | |||
| 59 | s.backTo(w, r, "builds", "pick a job") | 56 | s.backTo(w, r, "builds", "pick a job") |
| 60 | return | 57 | return |
| 61 | } | 58 | } |
| 62 | _, msg, ok := s.runControl(u, []string{"build", "trigger", repo, job}) | 59 | _, msg, code := s.runControlCode(u, []string{"build", "trigger", repo, job}) |
| 63 | if ok { | 60 | s.done(w, r, code, msg, func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "builds", msg) }) |
| 64 | msg = "" | ||
| 65 | } | ||
| 66 | s.backTo(w, r, "builds", msg) | ||
| 67 | } | 61 | } |