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