internal/httpd/snippets.go

0a69a8facd64e67e42ec04fa11dd4a0304e40d79
gitbay/internal/httpd/snippets.go history · blame · raw

199 lines · 6136 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}
 75
 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	views := make([]snippetFileView, 0, len(files))
 87	for _, f := range files {
 88		lines := bytes.Count(f.Content, []byte("\n"))
 89		if len(f.Content) > 0 && f.Content[len(f.Content)-1] != '\n' {
 90			lines++
 91		}
 92		views = append(views, snippetFileView{f.Name, f.Size, lines, string(f.Content), highlight(f.Name, f.Content)})
 93	}
 94	s.render(w, "snippet.html", struct {
 95		basePage
 96		Owner    string
 97		Snippet  store.Snippet
 98		Files    []snippetFileView
 99		CanWrite bool
100		Notice   string
101	}{s.baseFor(viewer), sn.OwnerName, sn, views, policy.CanWriteSnippet(viewer, sn), s.takeFlash(w, r)})
102}
103
104// snippetRaw serves one file as text, inert on the forge's origin.
105func (s *Server) snippetRaw(w http.ResponseWriter, r *http.Request) {
106	sn, _, ok := s.snippetScope(w, r)
107	if !ok {
108		return
109	}
110	f, err := s.st.SnippetFile(sn.ID, r.PathValue("name"))
111	if err != nil {
112		s.notFound(w, r)
113		return
114	}
115	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
116	w.Header().Set("X-Content-Type-Options", "nosniff")
117	w.Write(f.Content)
118}
119
120// snippetNewForm is the owner's own page only: the URL names the owner
121// and a snippet cannot be created for someone else.
122func (s *Server) snippetNewForm(w http.ResponseWriter, r *http.Request, u store.User) {
123	if r.PathValue("owner") != u.Username {
124		s.notFound(w, r)
125		return
126	}
127	s.render(w, "snippetnew.html", struct {
128		basePage
129		Owner string
130	}{s.baseFor(u), u.Username})
131}
132
133func (s *Server) snippetNewSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
134	if r.PathValue("owner") != u.Username {
135		s.notFound(w, r)
136		return
137	}
138	argv := []string{"snippet", "create", strings.TrimSpace(r.FormValue("name")),
139		"--description", strings.TrimSpace(r.FormValue("description")),
140		"--visibility", r.FormValue("visibility")}
141	var out control.SnippetOut
142	code, msg := s.dispatchIntoStdin(u, argv, r.FormValue("content"), &out)
143	if code != protocol.ExitOK {
144		http.Error(w, msg, statusForExit(code))
145		return
146	}
147	http.Redirect(w, r, "/"+u.Username+"/-/snippets/"+out.ID, http.StatusSeeOther)
148}
149
150// snippetAction runs a write on the snippet in the URL and returns to
151// its page with the message, or to the list after a delete. A snippet
152// the viewer may not read is the 404 page, as on every read.
153func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.User, argv []string, stdin string, dest string) {
154	sn, _, ok := s.snippetScope(w, r)
155	if !ok {
156		return
157	}
158	if dest == "" {
159		dest = "/" + sn.OwnerName + "/-/snippets/" + sn.PublicID
160	}
161	back := func(w http.ResponseWriter, r *http.Request, msg string) {
162		s.setFlash(w, msg)
163		http.Redirect(w, r, dest, http.StatusSeeOther)
164	}
165	var msg string
166	var code int
167	if stdin == "" {
168		_, msg, code = s.runControlCode(u, argv)
169	} else {
170		msg, code = s.runControlStdinCode(u, argv, stdin)
171	}
172	if code == protocol.ExitDenied {
173		http.Error(w, msg, http.StatusForbidden)
174		return
175	}
176	s.done(w, r, code, msg, back)
177}
178
179func (s *Server) snippetEditSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
180	s.snippetAction(w, r, u, []string{"snippet", "edit", r.PathValue("id"),
181		"--description", strings.TrimSpace(r.FormValue("description")),
182		"--visibility", r.FormValue("visibility")}, "", "")
183}
184
185func (s *Server) snippetDeleteSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
186	s.snippetAction(w, r, u, []string{"snippet", "delete", r.PathValue("id")}, "",
187		"/"+r.PathValue("owner")+"/-/snippets")
188}
189
190// An empty textarea reaches the command as empty stdin, which it refuses;
191// the message lands on the page like any other.
192func (s *Server) snippetFileSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
193	s.snippetAction(w, r, u, []string{"snippet", "file", "set", r.PathValue("id"), strings.TrimSpace(r.FormValue("name"))},
194		r.FormValue("content"), "")
195}
196
197func (s *Server) snippetFileRemoveSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
198	s.snippetAction(w, r, u, []string{"snippet", "file", "remove", r.PathValue("id"), strings.TrimSpace(r.FormValue("name"))}, "", "")
199}