internal/httpd/wiki.go

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

186 lines · 5140 bytes

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