internal/httpd/wiki.go

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

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