package httpd
import (
"html/template"
"net/http"
"os"
"path"
"strings"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
"gitbay.org/gitbay/internal/control"
"gitbay.org/gitbay/internal/gitutil"
)
// wikiDir returns the companion repo path, or "" when the repo has none.
func (s *Server) wikiDir(owner, name string) string {
dir := control.RepoDir(s.cfg.Server.Root, owner, name+".wiki")
if _, err := os.Stat(dir); err != nil {
return ""
}
return dir
}
// wiki renders a page from the repo's wiki companion. The home page is
// Home. (or README.); /wiki/ resolves with .md and
// .org fallbacks. Rendering reuses the same sanitized pipeline as READMEs.
func (s *Server) wiki(w http.ResponseWriter, r *http.Request) {
p, ok := s.repoFor(w, r, "")
if !ok {
return
}
p.Tab = "wiki"
dir := s.wikiDir(p.Repo.OwnerName, p.Repo.Name)
if dir == "" {
s.render(w, "wiki.html", struct {
repoPage
Page string
PageHTML template.HTML
Pages []string
Missing bool
}{repoPage: p, Missing: true})
return
}
entries, err := gitutil.ListTree(dir, "main", "")
if err != nil { // wiki repo exists but has no commits yet
s.render(w, "wiki.html", struct {
repoPage
Page string
PageHTML template.HTML
Pages []string
Missing bool
}{repoPage: p, Missing: true})
return
}
var pages []string
for _, e := range entries {
if e.Type != "blob" {
continue
}
ext := strings.ToLower(path.Ext(e.Name))
if ext == ".md" || ext == ".org" || ext == ".markdown" {
pages = append(pages, strings.TrimSuffix(e.Name, path.Ext(e.Name)))
}
}
page := strings.Trim(r.PathValue("page"), "/")
if page == "" {
for _, home := range []string{"Home", "home", "README", "index"} {
for _, pg := range pages {
if pg == home {
page = home
}
}
if page != "" {
break
}
}
if page == "" && len(pages) > 0 {
page = pages[0]
}
}
var pageHTML template.HTML
if page != "" {
fileName, raw := "", []byte(nil)
for _, ext := range []string{".md", ".org", ".markdown"} {
if b, err := gitutil.ReadBlob(dir, "main", page+ext, maxRenderBytes); err == nil {
fileName, raw = page+ext, b
break
}
}
if fileName == "" {
s.notFound(w, r)
return
}
pageHTML = rewriteWikiLinks(renderReadme(fileName, raw), p)
}
s.render(w, "wiki.html", struct {
repoPage
Page string
PageHTML template.HTML
Pages []string
Missing bool
}{p, page, pageHTML, pages, false})
}
// wikiRaw serves non-page files from the wiki (images referenced by pages).
func (s *Server) wikiRaw(w http.ResponseWriter, r *http.Request) {
p, ok := s.repoFor(w, r, "")
if !ok {
return
}
dir := s.wikiDir(p.Repo.OwnerName, p.Repo.Name)
if dir == "" {
s.notFound(w, r)
return
}
data, err := gitutil.ReadBlob(dir, "main", strings.Trim(r.PathValue("path"), "/"), s.cfg.Limits.MaxBlobBytes)
if err != nil {
s.notFound(w, r)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Write(data)
}
// rewriteWikiLinks makes relative links resolve inside the wiki: page
// links (with or without .md/.org/.html extensions) go to /wiki/,
// other relative targets (images) to the wiki raw route.
func rewriteWikiLinks(rendered template.HTML, p repoPage) template.HTML {
ctx := &html.Node{Type: html.ElementNode, Data: "div", DataAtom: atom.Div}
nodes, err := html.ParseFragment(strings.NewReader(string(rendered)), ctx)
if err != nil {
return rendered
}
base := "/" + p.Repo.Path() + "/wiki"
var walk func(*html.Node)
walk = func(n *html.Node) {
if n.Type == html.ElementNode {
for i, a := range n.Attr {
isHref := a.Key == "href" && n.Data == "a"
isSrc := a.Key == "src" && (n.Data == "img" || n.Data == "video" || n.Data == "source")
if !isHref && !isSrc {
continue
}
v := a.Val
if v == "" || strings.Contains(v, "://") || strings.HasPrefix(v, "/") ||
strings.HasPrefix(v, "#") || strings.HasPrefix(v, "mailto:") ||
strings.HasPrefix(v, "data:") {
continue
}
target := path.Clean(v)
if strings.HasPrefix(target, "..") {
continue
}
if isSrc {
n.Attr[i].Val = base + "/_raw/" + target
continue
}
ext := strings.ToLower(path.Ext(target))
switch ext {
case ".md", ".org", ".markdown", ".html":
target = strings.TrimSuffix(target, path.Ext(target))
}
n.Attr[i].Val = base + "/" + target
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
walk(c)
}
}
var out strings.Builder
for _, n := range nodes {
walk(n)
if err := html.Render(&out, n); err != nil {
return rendered
}
}
return template.HTML(out.String())
}