package web
import (
"bytes"
"html/template"
"net/url"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
// renderCard turns a model card into HTML for the mirror page.
//
// The card is a stranger's text, so what it may do is narrow:
// - raw HTML is dropped, which goldmark does unless told otherwise, and
// links to javascript: and the like are emptied, which it also does;
// - images are not loaded but shown as links, because an admin page that
// fetches whatever a README points at tells a stranger who is looking;
// - relative links, to the repository's own LICENSE and files, are made to
// point at the repository rather than at this page.
func renderCard(card, repoBase string) template.HTML {
return safeMarkdown(card, repoBase)
}
// safeMarkdown renders a stranger's markdown under the rules above. base, when
// set, is what relative links resolve against.
func safeMarkdown(src, base string) template.HTML {
if strings.TrimSpace(src) == "" {
return ""
}
md := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(parser.WithASTTransformers(
util.Prioritized(cardLinks{base: base}, 100))),
goldmark.WithRendererOptions(
renderer.WithNodeRenderers(util.Prioritized(imageAsLink{}, 100))),
)
var out bytes.Buffer
if err := md.Convert([]byte(src), &out); err != nil {
return ""
}
// Safe by construction: goldmark in its default, safe mode, with the two
// changes above. Not something a template may escape again.
return template.HTML(out.String()) //nolint:gosec
}
// cardLinks resolves a card's relative links against its repository.
type cardLinks struct{ base string }
func (c cardLinks) Transform(doc *ast.Document, _ text.Reader, _ parser.Context) {
base, err := url.Parse(c.base)
if err != nil || c.base == "" {
return
}
_ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
var dest *[]byte
switch l := n.(type) {
case *ast.Link:
dest = &l.Destination
case *ast.Image:
dest = &l.Destination
default:
return ast.WalkContinue, nil
}
u, err := url.Parse(string(*dest))
if err != nil || u.IsAbs() || strings.HasPrefix(string(*dest), "#") {
return ast.WalkContinue, nil
}
*dest = []byte(base.ResolveReference(u).String())
return ast.WalkContinue, nil
})
}
// imageAsLink renders an image as a link to it, named by its alt text.
type imageAsLink struct{}
func (imageAsLink) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindImage, func(w util.BufWriter, src []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
img := n.(*ast.Image)
alt := strings.TrimSpace(string(nodeText(img, src)))
if alt == "" {
alt = "image"
}
dest := img.Destination
if html.IsDangerousURL(dest) {
_, _ = w.WriteString(`[` + template.HTMLEscapeString(alt) + `]`)
return ast.WalkSkipChildren, nil
}
_, _ = w.WriteString(`[` +
template.HTMLEscapeString(alt) + `]`)
return ast.WalkSkipChildren, nil
})
}
// nodeText is the plain text beneath a node.
func nodeText(n ast.Node, src []byte) []byte {
var b bytes.Buffer
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
if t, ok := c.(*ast.Text); ok {
b.Write(t.Segment.Value(src))
continue
}
b.Write(nodeText(c, src))
}
return b.Bytes()
}