internal/httpd/snippets.go

v1.31.0
gitbay/internal/httpd/snippets.go history · blame · raw

254 lines · 7796 bytes

  1package httpd
  2
  3import (
  4	"bytes"
  5	"html/template"
  6	"net/http"
  7	"strings"
  8
  9	"gitbay.org/gitbay/internal/control"
 10	"gitbay.org/gitbay/internal/policy"
 11	"gitbay.org/gitbay/internal/protocol"
 12	"gitbay.org/gitbay/internal/store"
 13)
 14
 15// snippetScope resolves the owner and id in the URL for the viewer. A
 16// missing owner, an id under another owner, and a private snippet the
 17// viewer may not read are all the same 404.
 18func (s *Server) snippetScope(w http.ResponseWriter, r *http.Request) (store.Snippet, store.User, bool) {
 19	viewer := s.viewer(r)
 20	sn, err := s.st.SnippetByPublicID(r.PathValue("id"))
 21	if err != nil || sn.OwnerName != r.PathValue("owner") || !policy.CanReadSnippet(viewer, sn) {
 22		s.notFound(w, r)
 23		return sn, viewer, false
 24	}
 25	return sn, viewer, true
 26}
 27
 28type snippetRow struct {
 29	store.Snippet
 30	Names string
 31}
 32
 33func (s *Server) snippetsPage(w http.ResponseWriter, r *http.Request) {
 34	viewer := s.viewer(r)
 35	owner, err := s.st.UserByUsername(r.PathValue("owner"))
 36	if err != nil {
 37		s.notFound(w, r)
 38		return
 39	}
 40	self := viewer.ID != 0 && viewer.ID == owner.ID
 41	all := self || viewer.IsAdmin
 42	list, err := s.st.ListSnippets(owner.ID, all, 0, 0)
 43	if err != nil {
 44		http.Error(w, "internal error", http.StatusInternalServerError)
 45		return
 46	}
 47	rows := make([]snippetRow, 0, len(list))
 48	for _, sn := range list {
 49		var names bytes.Buffer
 50		for i, f := range sn.Files {
 51			if i > 0 {
 52				names.WriteString(", ")
 53			}
 54			names.WriteString(f.Name)
 55		}
 56		rows = append(rows, snippetRow{sn, names.String()})
 57	}
 58	s.render(w, "snippets.html", struct {
 59		basePage
 60		Owner    string
 61		Self     bool
 62		All      bool
 63		Snippets []snippetRow
 64		Notice   string
 65	}{s.baseFor(viewer), owner.Username, self, all, rows, s.takeFlash(w, r)})
 66}
 67
 68type snippetFileView struct {
 69	Name     string
 70	Size     int64
 71	Lines    int
 72	Content  string
 73	HTML     template.HTML
 74	TooLarge bool
 75}
 76
 77// snippetPage highlights files up to a shared budget across the page: a
 78// snippet with many or large files does not make one request highlight
 79// megabytes of markup. Content is filled only for the owner, whose edit
 80// textarea needs the raw text regardless of the budget.
 81func (s *Server) snippetPage(w http.ResponseWriter, r *http.Request) {
 82	sn, viewer, ok := s.snippetScope(w, r)
 83	if !ok {
 84		return
 85	}
 86	files, err := s.st.SnippetFiles(sn.ID)
 87	if err != nil {
 88		http.Error(w, "internal error", http.StatusInternalServerError)
 89		return
 90	}
 91	canWrite := policy.CanWriteSnippet(viewer, sn)
 92	budget := int64(maxRenderBytes)
 93	views := make([]snippetFileView, 0, len(files))
 94	for _, f := range files {
 95		lines := bytes.Count(f.Content, []byte("\n"))
 96		if len(f.Content) > 0 && f.Content[len(f.Content)-1] != '\n' {
 97			lines++
 98		}
 99		view := snippetFileView{Name: f.Name, Size: f.Size, Lines: lines}
100		if canWrite {
101			view.Content = string(f.Content)
102		}
103		if f.Size <= budget {
104			view.HTML = highlightPlain(f.Name, f.Content)
105			budget -= f.Size
106		} else {
107			view.TooLarge = true
108		}
109		views = append(views, view)
110	}
111	s.render(w, "snippet.html", struct {
112		basePage
113		Owner    string
114		Snippet  store.Snippet
115		Files    []snippetFileView
116		CanWrite bool
117		Notice   string
118	}{s.baseFor(viewer), sn.OwnerName, sn, views, canWrite, s.takeFlash(w, r)})
119}
120
121// snippetRaw serves one file as text, inert on the forge's origin.
122func (s *Server) snippetRaw(w http.ResponseWriter, r *http.Request) {
123	sn, _, ok := s.snippetScope(w, r)
124	if !ok {
125		return
126	}
127	f, err := s.st.SnippetFile(sn.ID, r.PathValue("name"))
128	if err != nil {
129		s.notFound(w, r)
130		return
131	}
132	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
133	w.Header().Set("X-Content-Type-Options", "nosniff")
134	w.Write(f.Content)
135}
136
137type snippetNewPage struct {
138	basePage
139	Owner       string
140	Name        string
141	Description string
142	Visibility  string
143	Content     string
144	Error       string
145}
146
147// snippetNewForm is the owner's own page only: the URL names the owner
148// and a snippet cannot be created for someone else.
149func (s *Server) snippetNewForm(w http.ResponseWriter, r *http.Request, u store.User) {
150	if r.PathValue("owner") != u.Username {
151		s.notFound(w, r)
152		return
153	}
154	s.render(w, "snippetnew.html", snippetNewPage{basePage: s.baseFor(u), Owner: u.Username})
155}
156
157// snippetNewSubmit re-renders the form with the submitted values on a
158// refusal, so a typo in the name does not throw away a pasted body.
159func (s *Server) snippetNewSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
160	if r.PathValue("owner") != u.Username {
161		s.notFound(w, r)
162		return
163	}
164	name := strings.TrimSpace(r.FormValue("name"))
165	description := strings.TrimSpace(r.FormValue("description"))
166	visibility := r.FormValue("visibility")
167	content := r.FormValue("content")
168	argv := []string{"snippet", "create", name, "--description", description, "--visibility", visibility}
169	var out control.SnippetOut
170	code, msg := s.dispatchIntoStdin(u, argv, content, &out)
171	if code != protocol.ExitOK {
172		s.render(w, "snippetnew.html", snippetNewPage{
173			basePage: s.baseFor(u), Owner: u.Username,
174			Name: name, Description: description, Visibility: visibility, Content: content, Error: msg,
175		})
176		return
177	}
178	http.Redirect(w, r, "/"+u.Username+"/-/snippets/"+out.ID, http.StatusSeeOther)
179}
180
181// snippetAction runs a write on an already-resolved snippet and returns to
182// its page with the message, or to dest (the list, for a delete) on
183// success. Callers resolve the snippet with snippetScope first, so a
184// snippet the viewer may not read is the 404 page before any confirmation
185// or write is considered.
186func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.User, sn store.Snippet, argv []string, stdin string, dest string) {
187	page := "/" + sn.OwnerName + "/-/snippets/" + sn.PublicID
188	if dest == "" {
189		dest = page
190	}
191	back := func(w http.ResponseWriter, r *http.Request, msg string) {
192		s.setFlash(w, msg)
193		to := dest
194		if msg != "" {
195			to = page
196		}
197		http.Redirect(w, r, to, http.StatusSeeOther)
198	}
199	msg, code := s.runControlStdinCode(u, argv, stdin)
200	if code == protocol.ExitDenied {
201		http.Error(w, msg, http.StatusForbidden)
202		return
203	}
204	s.done(w, r, code, msg, back)
205}
206
207func (s *Server) snippetEditSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
208	sn, _, ok := s.snippetScope(w, r)
209	if !ok {
210		return
211	}
212	s.snippetAction(w, r, u, sn, []string{"snippet", "edit", r.PathValue("id"),
213		"--description", strings.TrimSpace(r.FormValue("description")),
214		"--visibility", r.FormValue("visibility")}, "", "")
215}
216
217func (s *Server) snippetDeleteSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
218	sn, _, ok := s.snippetScope(w, r)
219	if !ok {
220		return
221	}
222	if ok, msg := confirmed(r, sn.PublicID); !ok {
223		s.setFlash(w, msg)
224		http.Redirect(w, r, "/"+sn.OwnerName+"/-/snippets/"+sn.PublicID, http.StatusSeeOther)
225		return
226	}
227	s.snippetAction(w, r, u, sn, []string{"snippet", "delete", sn.PublicID}, "",
228		"/"+sn.OwnerName+"/-/snippets")
229}
230
231// An empty textarea reaches the command as empty stdin, which it refuses;
232// the message lands on the page like any other.
233func (s *Server) snippetFileSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
234	sn, _, ok := s.snippetScope(w, r)
235	if !ok {
236		return
237	}
238	s.snippetAction(w, r, u, sn, []string{"snippet", "file", "set", r.PathValue("id"), strings.TrimSpace(r.FormValue("name"))},
239		r.FormValue("content"), "")
240}
241
242func (s *Server) snippetFileRemoveSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
243	sn, _, ok := s.snippetScope(w, r)
244	if !ok {
245		return
246	}
247	name := strings.TrimSpace(r.FormValue("name"))
248	if ok, msg := confirmed(r, name); !ok {
249		s.setFlash(w, msg)
250		http.Redirect(w, r, "/"+sn.OwnerName+"/-/snippets/"+sn.PublicID, http.StatusSeeOther)
251		return
252	}
253	s.snippetAction(w, r, u, sn, []string{"snippet", "file", "remove", r.PathValue("id"), name}, "", "")
254}