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() }