internal/httpd/milestoneactions.go
183 lines · 5973 bytes
1package httpd
2
3import (
4 "errors"
5 "fmt"
6 "net/http"
7 "path/filepath"
8 "strings"
9
10 "gitbay.org/gitbay/internal/policy"
11 "gitbay.org/gitbay/internal/store"
12)
13
14// Milestone, org label and release asset forms. Each dispatches the
15// command the CLI runs; who may do it is the command's decision, and the
16// pages only show the forms to those it will accept.
17
18// milestoneCreateArgs builds the create argv: the flags, then the
19// positionals after "--" so a title starting with "-" is not a flag.
20func milestoneCreateArgs(r *http.Request, head []string, target string) []string {
21 argv := head
22 if d := strings.TrimSpace(r.FormValue("description")); d != "" {
23 argv = append(argv, "--description", d)
24 }
25 if d := strings.TrimSpace(r.FormValue("due")); d != "" {
26 argv = append(argv, "--due", d)
27 }
28 return append(argv, "--", target, strings.TrimSpace(r.FormValue("title")))
29}
30
31func (s *Server) milestoneSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
32 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
33 back := func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "milestones", msg) }
34 title := strings.TrimSpace(r.FormValue("title"))
35 if title == "" {
36 back(w, r, "name the milestone")
37 return
38 }
39 var argv []string
40 switch r.FormValue("action") {
41 case "close":
42 argv = []string{"milestone", "close", repo, title}
43 case "reopen":
44 argv = []string{"milestone", "reopen", repo, title}
45 default:
46 argv = milestoneCreateArgs(r, []string{"milestone", "create"}, repo)
47 }
48 _, msg, code := s.runControlCode(u, argv)
49 s.done(w, r, code, msg, back)
50}
51
52func (s *Server) orgBack(w http.ResponseWriter, r *http.Request, page, msg string) {
53 s.setFlash(w, msg)
54 http.Redirect(w, r, "/"+r.PathValue("owner")+"/-/"+page, http.StatusSeeOther)
55}
56
57func (s *Server) orgLabelSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
58 org := r.PathValue("owner")
59 back := func(w http.ResponseWriter, r *http.Request, msg string) { s.orgBack(w, r, "labels", msg) }
60 name := strings.TrimSpace(r.FormValue("name"))
61 if name == "" {
62 back(w, r, "name the label")
63 return
64 }
65 argv := []string{"org", "label", "set", "--color", strings.TrimSpace(r.FormValue("color")), "--", org, name}
66 if r.FormValue("action") == "remove" {
67 if ok, msg := confirmed(r, name); !ok {
68 back(w, r, msg)
69 return
70 }
71 argv = []string{"org", "label", "remove", org, name}
72 }
73 _, msg, code := s.runControlCode(u, argv)
74 s.done(w, r, code, msg, back)
75}
76
77func (s *Server) orgMilestoneSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
78 org := r.PathValue("owner")
79 back := func(w http.ResponseWriter, r *http.Request, msg string) { s.orgBack(w, r, "milestones", msg) }
80 title := strings.TrimSpace(r.FormValue("title"))
81 if title == "" {
82 back(w, r, "name the milestone")
83 return
84 }
85 var argv []string
86 switch r.FormValue("action") {
87 case "close":
88 argv = []string{"org", "milestone", "close", org, title}
89 case "reopen":
90 argv = []string{"org", "milestone", "reopen", org, title}
91 default:
92 argv = milestoneCreateArgs(r, []string{"org", "milestone", "create"}, org)
93 }
94 _, msg, code := s.runControlCode(u, argv)
95 s.done(w, r, code, msg, back)
96}
97
98// releaseAssetSubmit uploads or removes a release asset. The upload is
99// parsed with a small memory budget, so the rest of the file spills to a
100// temporary file, and reaches release asset add as a stream on stdin.
101// The body is capped a little above max_asset_bytes so an oversized file
102// is refused without being read to the end; the command applies the
103// exact limit.
104func (s *Server) releaseAssetSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
105 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
106 back := func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "releases", msg) }
107 // Authorise before reading a byte: a body nobody may upload is never
108 // spooled to disk.
109 rp, err := s.st.RepoByPath(repo)
110 if err == nil {
111 grant, _ := s.st.AccessRole(rp.ID, u.ID)
112 if !policyCanRead(u, rp, grant) {
113 err = store.ErrNotFound
114 } else if !policy.CanWrite(u, rp, grant) {
115 back(w, r, "you need write access to change releases")
116 return
117 } else if rp.Settings.Archived {
118 back(w, r, rp.Path()+" is archived and read-only; unarchive it first")
119 return
120 }
121 }
122 if err != nil {
123 s.notFound(w, r)
124 return
125 }
126 limit := s.cfg.Limits.MaxAssetBytes
127 if r.ContentLength > limit+1<<20 {
128 back(w, r, fmt.Sprintf("asset exceeds max_asset_bytes (%d)", limit))
129 return
130 }
131 if _, busy := s.uploads.LoadOrStore(u.ID, struct{}{}); busy {
132 back(w, r, "another upload of yours is still running; wait for it to finish")
133 return
134 }
135 defer s.uploads.Delete(u.ID)
136 r.Body = http.MaxBytesReader(w, r.Body, limit+1<<20)
137 if err := r.ParseMultipartForm(1 << 20); err != nil && !errors.Is(err, http.ErrNotMultipart) {
138 var tooBig *http.MaxBytesError
139 if errors.As(err, &tooBig) {
140 back(w, r, fmt.Sprintf("asset exceeds max_asset_bytes (%d)", limit))
141 return
142 }
143 back(w, r, "unreadable upload")
144 return
145 }
146 if r.MultipartForm != nil {
147 defer r.MultipartForm.RemoveAll()
148 }
149 tag := strings.TrimSpace(r.FormValue("tag"))
150 if tag == "" {
151 back(w, r, "pick a release")
152 return
153 }
154 if r.FormValue("action") == "remove" {
155 name := strings.TrimSpace(r.FormValue("name"))
156 if ok, msg := confirmed(r, name); !ok || name == "" {
157 if name == "" {
158 msg = "name the asset"
159 }
160 back(w, r, msg)
161 return
162 }
163 _, msg, code := s.runControlCode(u, []string{"release", "asset", "remove", repo, tag, name})
164 s.done(w, r, code, msg, back)
165 return
166 }
167 f, hdr, err := r.FormFile("file")
168 if err != nil {
169 back(w, r, "choose a file")
170 return
171 }
172 defer f.Close()
173 if hdr.Size == 0 {
174 back(w, r, "the file is empty")
175 return
176 }
177 name := strings.TrimSpace(r.FormValue("name"))
178 if name == "" {
179 name = filepath.Base(hdr.Filename)
180 }
181 msg, code := s.runControlReader(u, []string{"release", "asset", "add", repo, tag, name}, f)
182 s.done(w, r, code, msg, back)
183}