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