internal/httpd/wiki.go

dbec077010394f2955c9b20cfd81700ef6b2bdb9
gitbay/internal/httpd/wiki.go history · blame · raw

168 lines · 4507 bytes

  1package httpd
  2
  3import (
  4	"html/template"
  5	"net/http"
  6	"os"
  7	"path"
  8	"strings"
  9
 10	"golang.org/x/net/html"
 11	"golang.org/x/net/html/atom"
 12
 13	"gitbay.org/gitbay/internal/control"
 14	"gitbay.org/gitbay/internal/gitutil"
 15	"gitbay.org/gitbay/internal/store"
 16)
 17
 18// wikiDir returns the companion repo path, or "" when the repo has none.
 19func (s *Server) wikiDir(owner, name string) string {
 20	dir := control.RepoDir(s.cfg.Server.Root, owner, name+".wiki")
 21	if _, err := os.Stat(dir); err != nil {
 22		return ""
 23	}
 24	return dir
 25}
 26
 27// wiki renders a page from the repo's wiki companion. The home page is
 28// Home.<ext> (or README.<ext>); /wiki/<name> resolves <name> with .md and
 29// .org fallbacks. Rendering reuses the same sanitized pipeline as READMEs.
 30func (s *Server) wiki(w http.ResponseWriter, r *http.Request) {
 31	p, ok := s.repoFor(w, r, "")
 32	if !ok {
 33		return
 34	}
 35	p.Tab = "wiki"
 36	dir := s.wikiDir(p.Repo.OwnerName, p.Repo.Name)
 37	if dir == "" {
 38		s.render(w, "wiki.html", struct {
 39			repoPage
 40			Page     string
 41			PageHTML template.HTML
 42			Pages    []string
 43			Missing  bool
 44		}{repoPage: p, Missing: true})
 45		return
 46	}
 47	var listing struct {
 48		Home  string   `json:"home"`
 49		Pages []string `json:"pages"`
 50	}
 51	var viewer store.User
 52	if s.cfg.Web.Mode == "accounts" {
 53		viewer = s.viewer(r)
 54	}
 55	_, listed := s.runControlInto(viewer, []string{"wiki", "list", p.Repo.Path()}, &listing)
 56	if !listed || len(listing.Pages) == 0 { // no wiki, or no commits yet
 57		s.render(w, "wiki.html", struct {
 58			repoPage
 59			Page     string
 60			PageHTML template.HTML
 61			Pages    []string
 62			Missing  bool
 63		}{repoPage: p, Missing: true})
 64		return
 65	}
 66	pages := listing.Pages
 67
 68	page := strings.Trim(r.PathValue("page"), "/")
 69	if page == "" {
 70		page = listing.Home
 71	}
 72	var pageHTML template.HTML
 73	if page != "" {
 74		var shown struct {
 75			File    string `json:"file"`
 76			Content string `json:"content"`
 77		}
 78		if _, ok := s.runControlInto(viewer,
 79			[]string{"wiki", "show", p.Repo.Path(), page}, &shown); !ok {
 80			s.notFound(w, r)
 81			return
 82		}
 83		pageHTML = rewriteWikiLinks(renderReadme(shown.File, []byte(shown.Content)), p)
 84	}
 85	s.render(w, "wiki.html", struct {
 86		repoPage
 87		Page     string
 88		PageHTML template.HTML
 89		Pages    []string
 90		Missing  bool
 91	}{p, page, pageHTML, pages, false})
 92}
 93
 94// wikiRaw serves non-page files from the wiki (images referenced by pages).
 95func (s *Server) wikiRaw(w http.ResponseWriter, r *http.Request) {
 96	p, ok := s.repoFor(w, r, "")
 97	if !ok {
 98		return
 99	}
100	dir := s.wikiDir(p.Repo.OwnerName, p.Repo.Name)
101	if dir == "" {
102		s.notFound(w, r)
103		return
104	}
105	data, err := gitutil.ReadBlob(dir, "main", strings.Trim(r.PathValue("path"), "/"), s.cfg.Limits.MaxBlobBytes)
106	if err != nil {
107		s.notFound(w, r)
108		return
109	}
110	w.Header().Set("Content-Type", "application/octet-stream")
111	w.Header().Set("X-Content-Type-Options", "nosniff")
112	w.Write(data)
113}
114
115// rewriteWikiLinks makes relative links resolve inside the wiki: page
116// links (with or without .md/.org/.html extensions) go to /wiki/<page>,
117// other relative targets (images) to the wiki raw route.
118func rewriteWikiLinks(rendered template.HTML, p repoPage) template.HTML {
119	ctx := &html.Node{Type: html.ElementNode, Data: "div", DataAtom: atom.Div}
120	nodes, err := html.ParseFragment(strings.NewReader(string(rendered)), ctx)
121	if err != nil {
122		return rendered
123	}
124	base := "/" + p.Repo.Path() + "/wiki"
125	var walk func(*html.Node)
126	walk = func(n *html.Node) {
127		if n.Type == html.ElementNode {
128			for i, a := range n.Attr {
129				isHref := a.Key == "href" && n.Data == "a"
130				isSrc := a.Key == "src" && (n.Data == "img" || n.Data == "video" || n.Data == "source")
131				if !isHref && !isSrc {
132					continue
133				}
134				v := a.Val
135				if v == "" || strings.Contains(v, "://") || strings.HasPrefix(v, "/") ||
136					strings.HasPrefix(v, "#") || strings.HasPrefix(v, "mailto:") ||
137					strings.HasPrefix(v, "data:") {
138					continue
139				}
140				target := path.Clean(v)
141				if strings.HasPrefix(target, "..") {
142					continue
143				}
144				if isSrc {
145					n.Attr[i].Val = base + "/_raw/" + target
146					continue
147				}
148				ext := strings.ToLower(path.Ext(target))
149				switch ext {
150				case ".md", ".org", ".markdown", ".html":
151					target = strings.TrimSuffix(target, path.Ext(target))
152				}
153				n.Attr[i].Val = base + "/" + target
154			}
155		}
156		for c := n.FirstChild; c != nil; c = c.NextSibling {
157			walk(c)
158		}
159	}
160	var out strings.Builder
161	for _, n := range nodes {
162		walk(n)
163		if err := html.Render(&out, n); err != nil {
164			return rendered
165		}
166	}
167	return template.HTML(out.String())
168}