Commit 5d1843ee18
Verified · cmc
e2e/snippetweb_test.go +16
| @@ -119,6 +119,14 @@ func TestSnippetsWeb(t *testing.T) { | ||
| 119 | 119 | t.Fatalf("new form under another owner: %d", status) |
| 120 | 120 | } |
| 121 | 121 | |
| 122 | // A refused create re-renders the form with the paste kept, not a | |
| 123 | // bare error page. | |
| 124 | _, body = browserPost(t, alice, inst.base()+"/alice/-/snippets/new", url.Values{ | |
| 125 | "name": {"../x"}, "content": {"kept content\n"}}) | |
| 126 | if !strings.Contains(body, `class="error"`) || !strings.Contains(body, "kept content") { | |
| 127 | t.Fatalf("refused create form:\n%s", body) | |
| 128 | } | |
| 129 | ||
| 122 | 130 | // The file form replaces a file and adds one; remove drops it. |
| 123 | 131 | page := inst.base() + "/alice/-/snippets/" + created |
| 124 | 132 | if status, _ := browserPost(t, alice, page+"/file", url.Values{"name": {"notes.md"}, "content": {"# changed\n"}}); status != 200 { |
| @@ -151,6 +159,14 @@ func TestSnippetsWeb(t *testing.T) { | ||
| 151 | 159 | } |
| 152 | 160 | // bob cannot write alice's snippet from the browser either. |
| 153 | 161 | bob := inst.login(t, bobKey) |
| 162 | // A logged-in stranger sees the same visibility rule as anonymous: | |
| 163 | // 404 for a private snippet, 200 for an unlisted one. | |
| 164 | if status, _ := browserGet(t, bob, inst.base()+"/alice/-/snippets/"+private); status != 404 { | |
| 165 | t.Fatalf("stranger on a private page: %d", status) | |
| 166 | } | |
| 167 | if status, _ := browserGet(t, bob, inst.base()+"/alice/-/snippets/"+unlisted); status != 200 { | |
| 168 | t.Fatalf("stranger on an unlisted page: %d", status) | |
| 169 | } | |
| 154 | 170 | if status, _ := browserPost(t, bob, inst.base()+"/alice/-/snippets/"+public+"/edit", url.Values{"description": {"x"}, "visibility": {"public"}}); status != 403 { |
| 155 | 171 | t.Fatalf("bob editing alice's snippet: %d", status) |
| 156 | 172 | } |
internal/httpd/snippets.go +23 −9
| @@ -134,6 +134,16 @@ func (s *Server) snippetRaw(w http.ResponseWriter, r *http.Request) { | ||
| 134 | 134 | w.Write(f.Content) |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | type snippetNewPage struct { | |
| 138 | basePage | |
| 139 | Owner string | |
| 140 | Name string | |
| 141 | Description string | |
| 142 | Visibility string | |
| 143 | Content string | |
| 144 | Error string | |
| 145 | } | |
| 146 | ||
| 137 | 147 | // snippetNewForm is the owner's own page only: the URL names the owner |
| 138 | 148 | // and a snippet cannot be created for someone else. |
| 139 | 149 | func (s *Server) snippetNewForm(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -141,24 +151,28 @@ func (s *Server) snippetNewForm(w http.ResponseWriter, r *http.Request, u store. | ||
| 141 | 151 | s.notFound(w, r) |
| 142 | 152 | return |
| 143 | 153 | } |
| 144 | s.render(w, "snippetnew.html", struct { | |
| 145 | basePage | |
| 146 | Owner string | |
| 147 | }{s.baseFor(u), u.Username}) | |
| 154 | s.render(w, "snippetnew.html", snippetNewPage{basePage: s.baseFor(u), Owner: u.Username}) | |
| 148 | 155 | } |
| 149 | 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. | |
| 150 | 159 | func (s *Server) snippetNewSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 151 | 160 | if r.PathValue("owner") != u.Username { |
| 152 | 161 | s.notFound(w, r) |
| 153 | 162 | return |
| 154 | 163 | } |
| 155 | argv := []string{"snippet", "create", strings.TrimSpace(r.FormValue("name")), | |
| 156 | "--description", strings.TrimSpace(r.FormValue("description")), | |
| 157 | "--visibility", r.FormValue("visibility")} | |
| 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} | |
| 158 | 169 | var out control.SnippetOut |
| 159 | code, msg := s.dispatchIntoStdin(u, argv, r.FormValue("content"), &out) | |
| 170 | code, msg := s.dispatchIntoStdin(u, argv, content, &out) | |
| 160 | 171 | if code != protocol.ExitOK { |
| 161 | http.Error(w, msg, statusForExit(code)) | |
| 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 | }) | |
| 162 | 176 | return |
| 163 | 177 | } |
| 164 | 178 | http.Redirect(w, r, "/"+u.Username+"/-/snippets/"+out.ID, http.StatusSeeOther) |
internal/web/templates/snippetnew.html +9 −4
| @@ -1,11 +1,16 @@ | ||
| 1 | 1 | {{define "title"}}new snippet · {{.Owner}}{{end}} |
| 2 | 2 | {{define "content"}} |
| 3 | 3 | <h1>New snippet</h1> |
| 4 | {{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}} | |
| 4 | 5 | <form method="post" action="/{{.Owner}}/-/snippets/new" class="commentform"> |
| 5 | <p><input type="text" name="name" aria-label="File name" placeholder="filename" required></p> | |
| 6 | <p><input type="text" name="description" aria-label="Description" placeholder="description"></p> | |
| 7 | <p><select name="visibility" aria-label="Visibility"><option value="unlisted">unlisted</option><option value="public">public</option><option value="private">private</option></select></p> | |
| 8 | <p><textarea name="content" aria-label="Content" rows="16" required></textarea></p> | |
| 6 | <p><input type="text" name="name" aria-label="File name" placeholder="filename" value="{{.Name}}" required></p> | |
| 7 | <p><input type="text" name="description" aria-label="Description" placeholder="description" value="{{.Description}}"></p> | |
| 8 | <p><select name="visibility" aria-label="Visibility"> | |
| 9 | <option value="unlisted"{{if or (eq .Visibility "unlisted") (eq .Visibility "")}} selected{{end}}>unlisted</option> | |
| 10 | <option value="public"{{if eq .Visibility "public"}} selected{{end}}>public</option> | |
| 11 | <option value="private"{{if eq .Visibility "private"}} selected{{end}}>private</option> | |
| 12 | </select></p> | |
| 13 | <p><textarea name="content" aria-label="Content" rows="16" required>{{.Content}}</textarea></p> | |
| 9 | 14 | <p><button type="submit">Create snippet</button></p> |
| 10 | 15 | </form> |
| 11 | 16 | {{end}} |