internal/httpd/wiki.go
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}