fix(markup): resolve and proxy srcset links of <source> elements (#39708)

1. use correct links for srcset attribute of picture source
2. wrap the picture element with a link correctly

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Basavaraj S mandwxiaoguang authored and GitHub committed 2026-10-13 01:33:19 +00:00
1 parent bd28f254fa
commit a1cc651267
9 files changed
+247 -14

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ func b64encode(data []byte) string {
func camoHandleLink(link string) string {
if setting.Camo.Enabled {
lnkURL, err := url.Parse(link)
if err == nil && lnkURL.IsAbs() && !strings.HasPrefix(link, setting.AppURL) &&
if err == nil && (lnkURL.Scheme == "http" || lnkURL.Scheme == "https") && !strings.HasPrefix(link, setting.AppURL) &&
(setting.Camo.Always || lnkURL.Scheme != "https") {
return CamoEncode(link)
}
+4
View File
@@ -15,8 +15,11 @@ func TestCamoHandleLink(t *testing.T) {
setting.AppURL = "https://gitea.com"
// Test media proxy
setting.Camo.Enabled = true
setting.Camo.Always = false
setting.Camo.ServerURL = "https://image.proxy"
setting.Camo.HMACKey = "geheim"
dataURL := "data:image/png;base64,i//V"
assert.Equal(t, dataURL, camoHandleLink(dataURL))
assert.Equal(t,
"https://gitea.com/img.jpg",
@@ -29,6 +32,7 @@ func TestCamoHandleLink(t *testing.T) {
camoHandleLink("http://testimages.org/img.jpg"))
setting.Camo.Always = true
assert.Equal(t, dataURL, camoHandleLink(dataURL))
assert.Equal(t,
"https://gitea.com/img.jpg",
camoHandleLink("https://gitea.com/img.jpg"))
+4 -1
View File
@@ -355,6 +355,8 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
return visitNodeImg(ctx, node)
} else if node.Data == "video" {
return visitNodeVideo(ctx, node)
} else if node.Data == "source" {
return visitNodePictureSource(ctx, node)
}
if node.Data == "a" {
@@ -363,10 +365,11 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
// because the content there is not processable, for example: the content is a commit id or a full URL.
procs = emojiProcessors
}
next := node.NextSibling
for n := node.FirstChild; n != nil; {
n = visitNode(ctx, procs, n)
}
return node.NextSibling
return next
}
// createKeyword() renders a highlighted version of an action keyword
+32 -10
View File
@@ -6,6 +6,7 @@ package markup
import (
"strings"
"gitea.dev/modules/htmlutil"
"gitea.dev/modules/markup/common"
"golang.org/x/net/html"
@@ -127,6 +128,10 @@ func processNodeA(ctx *RenderContext, node *html.Node) {
}
}
func resolveMediaLinkWithCamo(ctx *RenderContext, link string) string {
return camoHandleLink(ctx.RenderHelper.ResolveLink(link, LinkTypeMedia))
}
func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
next = img.NextSibling
attrSrc, hasLazy := "", false
@@ -152,19 +157,21 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
cnt++
}
if isLinkable {
linkNode := img
if img.Parent.Data == "picture" {
linkNode = img.Parent
}
wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)},
{Key: "target", Val: "_blank"},
}}
parent := img.Parent
imgNext := img.NextSibling
parent.RemoveChild(img)
parent.InsertBefore(wrapper, imgNext)
wrapper.AppendChild(img)
parent := linkNode.Parent
linkNext := linkNode.NextSibling
parent.RemoveChild(linkNode)
parent.InsertBefore(wrapper, linkNext)
wrapper.AppendChild(linkNode)
}
imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia)
imgAttr.Val = camoHandleLink(imgAttr.Val)
imgAttr.Val = resolveMediaLinkWithCamo(ctx, imgSrcOrigin)
img.Attr[i] = imgAttr
}
if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") {
@@ -179,9 +186,24 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
if attr.Key != "src" {
continue
}
attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
attr.Val = camoHandleLink(attr.Val)
attr.Val = resolveMediaLinkWithCamo(ctx, attr.Val)
node.Attr[i] = attr
}
return next
}
func visitNodePictureSource(ctx *RenderContext, node *html.Node) (next *html.Node) {
next = node.NextSibling
for attrIdx, attr := range node.Attr {
if attr.Key != "srcset" {
continue
}
srcsetCandidates := htmlutil.ParseSrcset(attr.Val)
for i, candidate := range srcsetCandidates {
srcsetCandidates[i].SrcLink = resolveMediaLinkWithCamo(ctx, candidate.SrcLink)
}
attr.Val = htmlutil.EncodeSrcset(srcsetCandidates)
node.Attr[attrIdx] = attr
}
return next
}
+1 -1
View File
@@ -42,7 +42,7 @@ func resolveLinkRelative(ctx context.Context, base, cur, link string, absolute b
finalLink += "?" + linkURL.RawQuery
}
if linkURL.Fragment != "" {
finalLink += "#" + linkURL.Fragment
finalLink += "#" + linkURL.EscapedFragment()
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ func TestResolveLinkRelative(t *testing.T) {
assert.Equal(t, "/a/c", resolveLinkRelative(ctx, "/a", "b", "/c", false))
assert.Equal(t, "/a/c#id", resolveLinkRelative(ctx, "/a", "b", "/c#id", false))
assert.Equal(t, "/a/%2f?k=/", resolveLinkRelative(ctx, "/a", "b", "/%2f/?k=/", false))
assert.Equal(t, "/a/b/c?k=v#id", resolveLinkRelative(ctx, "/a", "b", "c/?k=v#id", false))
assert.Equal(t, "/a/b/c?k=v#with%20space", resolveLinkRelative(ctx, "/a", "b", "c/?k=v#with%20space", false))
assert.Equal(t, "%invalid", resolveLinkRelative(ctx, "/a", "b", "%invalid", false))
assert.Equal(t, "http://localhost:3000/a", resolveLinkRelative(ctx, "/a", "", "", true))