internal/httpd/snippets.go
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}