internal/httpd/milestoneactions.go

v1.40.1
gitbay/internal/httpd/milestoneactions.go history · blame · raw

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}