internal/httpd/snippets.go

bd49b87fce895e9f0a7588152548fb6e1821ac7d
gitbay/internal/httpd/snippets.go history · blame · raw

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