mirror of
https://github.com/go-gitea/gitea
synced 2024-12-24 13:15:55 +01:00
8a20fba8eb
Remove unmaintainable sanitizer rules. No need to add special "class" regexp rules anymore, use RenderInternal.SafeAttr instead, more details (and examples) are in the tests
238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package markup
|
||
|
||
import (
|
||
"net/url"
|
||
"path"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"code.gitea.io/gitea/modules/markup/common"
|
||
"code.gitea.io/gitea/modules/util"
|
||
|
||
"golang.org/x/net/html"
|
||
"golang.org/x/net/html/atom"
|
||
)
|
||
|
||
func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
|
||
isAnchorFragment := link != "" && link[0] == '#'
|
||
if !isAnchorFragment && !IsFullURLString(link) {
|
||
linkBase := ctx.Links.Base
|
||
if ctx.IsMarkupContentWiki() {
|
||
// no need to check if the link should be resolved as a wiki link or a wiki raw link
|
||
// just use wiki link here, and it will be redirected to a wiki raw link if necessary
|
||
linkBase = ctx.Links.WikiLink()
|
||
} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" {
|
||
// if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}"
|
||
// and then this link will be handled by the "legacy-ref" code and be redirected to the default branch like "/owner/repo/src/branch/main/{the-file-path}"
|
||
linkBase = ctx.Links.SrcLink()
|
||
}
|
||
link, resolved = util.URLJoin(linkBase, link), true
|
||
}
|
||
if isAnchorFragment && userContentAnchorPrefix != "" {
|
||
link, resolved = userContentAnchorPrefix+link[1:], true
|
||
}
|
||
return link, resolved
|
||
}
|
||
|
||
func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||
next := node.NextSibling
|
||
for node != nil && node != next {
|
||
m := globalVars().shortLinkPattern.FindStringSubmatchIndex(node.Data)
|
||
if m == nil {
|
||
return
|
||
}
|
||
|
||
content := node.Data[m[2]:m[3]]
|
||
tail := node.Data[m[4]:m[5]]
|
||
props := make(map[string]string)
|
||
|
||
// MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
|
||
// It makes page handling terrible, but we prefer GitHub syntax
|
||
// And fall back to MediaWiki only when it is obvious from the look
|
||
// Of text and link contents
|
||
sl := strings.Split(content, "|")
|
||
for _, v := range sl {
|
||
if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
|
||
// There is no equal in this argument; this is a mandatory arg
|
||
if props["name"] == "" {
|
||
if IsFullURLString(v) {
|
||
// If we clearly see it is a link, we save it so
|
||
|
||
// But first we need to ensure, that if both mandatory args provided
|
||
// look like links, we stick to GitHub syntax
|
||
if props["link"] != "" {
|
||
props["name"] = props["link"]
|
||
}
|
||
|
||
props["link"] = strings.TrimSpace(v)
|
||
} else {
|
||
props["name"] = v
|
||
}
|
||
} else {
|
||
props["link"] = strings.TrimSpace(v)
|
||
}
|
||
} else {
|
||
// There is an equal; optional argument.
|
||
|
||
sep := strings.IndexByte(v, '=')
|
||
key, val := v[:sep], html.UnescapeString(v[sep+1:])
|
||
|
||
// When parsing HTML, x/net/html will change all quotes which are
|
||
// not used for syntax into UTF-8 quotes. So checking val[0] won't
|
||
// be enough, since that only checks a single byte.
|
||
if len(val) > 1 {
|
||
if (strings.HasPrefix(val, "“") && strings.HasSuffix(val, "”")) ||
|
||
(strings.HasPrefix(val, "‘") && strings.HasSuffix(val, "’")) {
|
||
const lenQuote = len("‘")
|
||
val = val[lenQuote : len(val)-lenQuote]
|
||
} else if (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) ||
|
||
(strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) {
|
||
val = val[1 : len(val)-1]
|
||
} else if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "’") {
|
||
const lenQuote = len("‘")
|
||
val = val[1 : len(val)-lenQuote]
|
||
}
|
||
}
|
||
props[key] = val
|
||
}
|
||
}
|
||
|
||
var name, link string
|
||
if props["link"] != "" {
|
||
link = props["link"]
|
||
} else if props["name"] != "" {
|
||
link = props["name"]
|
||
}
|
||
if props["title"] != "" {
|
||
name = props["title"]
|
||
} else if props["name"] != "" {
|
||
name = props["name"]
|
||
} else {
|
||
name = link
|
||
}
|
||
|
||
name += tail
|
||
image := false
|
||
ext := filepath.Ext(link)
|
||
switch ext {
|
||
// fast path: empty string, ignore
|
||
case "":
|
||
// leave image as false
|
||
case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg":
|
||
image = true
|
||
}
|
||
|
||
childNode := &html.Node{}
|
||
linkNode := &html.Node{
|
||
FirstChild: childNode,
|
||
LastChild: childNode,
|
||
Type: html.ElementNode,
|
||
Data: "a",
|
||
DataAtom: atom.A,
|
||
}
|
||
childNode.Parent = linkNode
|
||
absoluteLink := IsFullURLString(link)
|
||
if !absoluteLink {
|
||
if image {
|
||
link = strings.ReplaceAll(link, " ", "+")
|
||
} else {
|
||
link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-"
|
||
}
|
||
if !strings.Contains(link, "/") {
|
||
link = url.PathEscape(link) // FIXME: it doesn't seem right and it might cause double-escaping
|
||
}
|
||
}
|
||
if image {
|
||
if !absoluteLink {
|
||
link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link)
|
||
}
|
||
title := props["title"]
|
||
if title == "" {
|
||
title = props["alt"]
|
||
}
|
||
if title == "" {
|
||
title = path.Base(name)
|
||
}
|
||
alt := props["alt"]
|
||
if alt == "" {
|
||
alt = name
|
||
}
|
||
|
||
// make the childNode an image - if we can, we also place the alt
|
||
childNode.Type = html.ElementNode
|
||
childNode.Data = "img"
|
||
childNode.DataAtom = atom.Img
|
||
childNode.Attr = []html.Attribute{
|
||
{Key: "src", Val: link},
|
||
{Key: "title", Val: title},
|
||
{Key: "alt", Val: alt},
|
||
}
|
||
if alt == "" {
|
||
childNode.Attr = childNode.Attr[:2]
|
||
}
|
||
} else {
|
||
link, _ = ResolveLink(ctx, link, "")
|
||
childNode.Type = html.TextNode
|
||
childNode.Data = name
|
||
}
|
||
linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
|
||
replaceContent(node, m[0], m[1], linkNode)
|
||
node = node.NextSibling.NextSibling
|
||
}
|
||
}
|
||
|
||
// linkProcessor creates links for any HTTP or HTTPS URL not captured by
|
||
// markdown.
|
||
func linkProcessor(ctx *RenderContext, node *html.Node) {
|
||
next := node.NextSibling
|
||
for node != nil && node != next {
|
||
m := common.GlobalVars().LinkRegex.FindStringIndex(node.Data)
|
||
if m == nil {
|
||
return
|
||
}
|
||
|
||
uri := node.Data[m[0]:m[1]]
|
||
replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/))
|
||
node = node.NextSibling.NextSibling
|
||
}
|
||
}
|
||
|
||
// descriptionLinkProcessor creates links for DescriptionHTML
|
||
func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||
next := node.NextSibling
|
||
for node != nil && node != next {
|
||
m := common.GlobalVars().LinkRegex.FindStringIndex(node.Data)
|
||
if m == nil {
|
||
return
|
||
}
|
||
|
||
uri := node.Data[m[0]:m[1]]
|
||
replaceContent(node, m[0], m[1], createDescriptionLink(uri, uri))
|
||
node = node.NextSibling.NextSibling
|
||
}
|
||
}
|
||
|
||
func createDescriptionLink(href, content string) *html.Node {
|
||
textNode := &html.Node{
|
||
Type: html.TextNode,
|
||
Data: content,
|
||
}
|
||
linkNode := &html.Node{
|
||
FirstChild: textNode,
|
||
LastChild: textNode,
|
||
Type: html.ElementNode,
|
||
Data: "a",
|
||
DataAtom: atom.A,
|
||
Attr: []html.Attribute{
|
||
{Key: "href", Val: href},
|
||
{Key: "target", Val: "_blank"},
|
||
{Key: "rel", Val: "noopener noreferrer"},
|
||
},
|
||
}
|
||
textNode.Parent = linkNode
|
||
return linkNode
|
||
}
|