package httpd
import (
"bytes"
"html/template"
"net/http"
"strings"
"gitbay.org/gitbay/internal/control"
"gitbay.org/gitbay/internal/policy"
"gitbay.org/gitbay/internal/protocol"
"gitbay.org/gitbay/internal/store"
)
// snippetScope resolves the owner and id in the URL for the viewer. A
// missing owner, an id under another owner, and a private snippet the
// viewer may not read are all the same 404.
func (s *Server) snippetScope(w http.ResponseWriter, r *http.Request) (store.Snippet, store.User, bool) {
viewer := s.viewer(r)
sn, err := s.st.SnippetByPublicID(r.PathValue("id"))
if err != nil || sn.OwnerName != r.PathValue("owner") || !policy.CanReadSnippet(viewer, sn) {
s.notFound(w, r)
return sn, viewer, false
}
return sn, viewer, true
}
type snippetRow struct {
store.Snippet
Names string
}
// ownerSnippets lists an owner's snippets for the profile's Snippets
// tab. The list is a section of the profile like the repositories are,
// not a page of its own (a snippet itself still is). A private snippet
// is in the list only for its owner and the admins.
func (s *Server) ownerSnippets(w http.ResponseWriter, r *http.Request, viewer store.User, name string) ([]snippetRow, bool) {
owner, err := s.st.UserByUsername(name)
if err != nil {
s.notFound(w, r)
return nil, false
}
all := viewer.IsAdmin || (viewer.ID != 0 && viewer.ID == owner.ID)
list, err := s.st.ListSnippets(owner.ID, all, 0, 0)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return nil, false
}
rows := make([]snippetRow, 0, len(list))
for _, sn := range list {
var names bytes.Buffer
for i, f := range sn.Files {
if i > 0 {
names.WriteString(", ")
}
names.WriteString(f.Name)
}
rows = append(rows, snippetRow{sn, names.String()})
}
return rows, true
}
type snippetFileView struct {
Name string
Size int64
Lines int
Content string
HTML template.HTML
TooLarge bool
}
// snippetPage highlights files up to a shared budget across the page: a
// snippet with many or large files does not make one request highlight
// megabytes of markup. Content is filled only for the owner, whose edit
// textarea needs the raw text regardless of the budget.
func (s *Server) snippetPage(w http.ResponseWriter, r *http.Request) {
sn, viewer, ok := s.snippetScope(w, r)
if !ok {
return
}
files, err := s.st.SnippetFiles(sn.ID)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
canWrite := policy.CanWriteSnippet(viewer, sn)
budget := int64(maxRenderBytes)
views := make([]snippetFileView, 0, len(files))
for _, f := range files {
lines := bytes.Count(f.Content, []byte("\n"))
if len(f.Content) > 0 && f.Content[len(f.Content)-1] != '\n' {
lines++
}
view := snippetFileView{Name: f.Name, Size: f.Size, Lines: lines}
if canWrite {
view.Content = string(f.Content)
}
if f.Size <= budget {
view.HTML = highlightPlain(f.Name, f.Content)
budget -= f.Size
} else {
view.TooLarge = true
}
views = append(views, view)
}
s.render(w, "snippet.html", struct {
basePage
Owner string
Snippet store.Snippet
Files []snippetFileView
CanWrite bool
Notice string
}{s.baseFor(viewer), sn.OwnerName, sn, views, canWrite, s.takeFlash(w, r)})
}
// snippetRaw serves one file as text, inert on the forge's origin.
func (s *Server) snippetRaw(w http.ResponseWriter, r *http.Request) {
sn, _, ok := s.snippetScope(w, r)
if !ok {
return
}
f, err := s.st.SnippetFile(sn.ID, r.PathValue("name"))
if err != nil {
s.notFound(w, r)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Write(f.Content)
}
type snippetNewPage struct {
basePage
Owner string
Name string
Description string
Visibility string
Content string
Error string
}
// snippetNewForm is the owner's own page only: the URL names the owner
// and a snippet cannot be created for someone else.
func (s *Server) snippetNewForm(w http.ResponseWriter, r *http.Request, u store.User) {
if r.PathValue("owner") != u.Username {
s.notFound(w, r)
return
}
s.render(w, "snippetnew.html", snippetNewPage{basePage: s.baseFor(u), Owner: u.Username})
}
// snippetNewSubmit re-renders the form with the submitted values on a
// refusal, so a typo in the name does not throw away a pasted body.
func (s *Server) snippetNewSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
if r.PathValue("owner") != u.Username {
s.notFound(w, r)
return
}
name := strings.TrimSpace(r.FormValue("name"))
description := strings.TrimSpace(r.FormValue("description"))
visibility := r.FormValue("visibility")
content := r.FormValue("content")
argv := []string{"snippet", "create", name, "--description", description, "--visibility", visibility}
var out control.SnippetOut
code, msg := s.dispatchIntoStdin(u, argv, content, &out)
if code != protocol.ExitOK {
s.render(w, "snippetnew.html", snippetNewPage{
basePage: s.baseFor(u), Owner: u.Username,
Name: name, Description: description, Visibility: visibility, Content: content, Error: msg,
})
return
}
http.Redirect(w, r, "/"+u.Username+"/-/snippets/"+out.ID, http.StatusSeeOther)
}
// snippetAction runs a write on an already-resolved snippet and returns to
// its page with the message, or to dest (the list, for a delete) on
// success. Callers resolve the snippet with snippetScope first, so a
// snippet the viewer may not read is the 404 page before any confirmation
// or write is considered.
func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.User, sn store.Snippet, argv []string, stdin string, dest string) {
page := "/" + sn.OwnerName + "/-/snippets/" + sn.PublicID
if dest == "" {
dest = page
}
back := func(w http.ResponseWriter, r *http.Request, msg string) {
s.setFlash(w, msg)
to := dest
if msg != "" {
to = page
}
http.Redirect(w, r, to, http.StatusSeeOther)
}
msg, code := s.runControlStdinCode(u, argv, stdin)
if code == protocol.ExitDenied {
http.Error(w, msg, http.StatusForbidden)
return
}
s.done(w, r, code, msg, back)
}
func (s *Server) snippetEditSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
sn, _, ok := s.snippetScope(w, r)
if !ok {
return
}
s.snippetAction(w, r, u, sn, []string{"snippet", "edit", r.PathValue("id"),
"--description", strings.TrimSpace(r.FormValue("description")),
"--visibility", r.FormValue("visibility")}, "", "")
}
func (s *Server) snippetDeleteSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
sn, _, ok := s.snippetScope(w, r)
if !ok {
return
}
if ok, msg := confirmed(r, sn.PublicID); !ok {
s.setFlash(w, msg)
http.Redirect(w, r, "/"+sn.OwnerName+"/-/snippets/"+sn.PublicID, http.StatusSeeOther)
return
}
s.snippetAction(w, r, u, sn, []string{"snippet", "delete", sn.PublicID}, "",
"/"+sn.OwnerName+"/-/snippets")
}
// An empty textarea reaches the command as empty stdin, which it refuses;
// the message lands on the page like any other.
func (s *Server) snippetFileSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
sn, _, ok := s.snippetScope(w, r)
if !ok {
return
}
s.snippetAction(w, r, u, sn, []string{"snippet", "file", "set", r.PathValue("id"), strings.TrimSpace(r.FormValue("name"))},
r.FormValue("content"), "")
}
func (s *Server) snippetFileRemoveSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
sn, _, ok := s.snippetScope(w, r)
if !ok {
return
}
name := strings.TrimSpace(r.FormValue("name"))
if ok, msg := confirmed(r, name); !ok {
s.setFlash(w, msg)
http.Redirect(w, r, "/"+sn.OwnerName+"/-/snippets/"+sn.PublicID, http.StatusSeeOther)
return
}
s.snippetAction(w, r, u, sn, []string{"snippet", "file", "remove", r.PathValue("id"), name}, "", "")
}