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