mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-14 07:02:17 +09:00
Backport #39708 Co-authored-by: Basavaraj S m <basavaraj@digiscrypt.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
co-authored by
Basavaraj S m
wxiaoguang
parent
f5fd936cbb
commit
63669003eb
@@ -9,6 +9,7 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
_ "gitea.dev/modules/markup/orgmode"
|
||||
|
||||
@@ -80,6 +81,23 @@ func TestRepoFile(t *testing.T) {
|
||||
<video src="/user2/repo1/media/commit/1234/my%20dir/LINK">
|
||||
</video>`, rendered)
|
||||
})
|
||||
|
||||
t.Run("PictureSource", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefSubURL: "branch/main"}).
|
||||
WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := testRenderString(rctx, `<picture><source media="(prefers-color-scheme: dark)" srcset="dark.svg"><img src="light.svg"></picture>`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.NormalizeHTMLSpaces(`
|
||||
<p>
|
||||
<a href="/user2/repo1/src/branch/main/light.svg" target="_blank" rel="nofollow noopener">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="/user2/repo1/media/branch/main/dark.svg"/>
|
||||
<img src="/user2/repo1/media/branch/main/light.svg"/>
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
`), test.NormalizeHTMLSpaces(rendered))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepoFileOrgMode(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package htmlutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SrcSetCandidate struct {
|
||||
SrcLink string
|
||||
SrcDesc []string
|
||||
}
|
||||
|
||||
const (
|
||||
asciiSpaces = " \t\n\r\f"
|
||||
asciiSpacesAndComma = " \t\n\r\f,"
|
||||
)
|
||||
|
||||
func ParseSrcset(srcset string) (candidates []SrcSetCandidate) {
|
||||
// ref: https://html.spec.whatwg.org/multipage/images.html#parse-a-srcset-attribute
|
||||
// It can't simply use "split" because comma can appear in URLs and descriptors can be parenthesized and contain commas.
|
||||
extractLink := func() (string, bool) {
|
||||
srcset = strings.TrimLeft(srcset, asciiSpacesAndComma)
|
||||
if srcset == "" {
|
||||
return "", false
|
||||
}
|
||||
end := strings.IndexAny(srcset, asciiSpaces)
|
||||
if end == -1 {
|
||||
end = len(srcset)
|
||||
}
|
||||
link := srcset[:end]
|
||||
srcset = srcset[end:]
|
||||
if strings.HasSuffix(link, ",") {
|
||||
return strings.TrimRight(link, ","), false
|
||||
}
|
||||
return link, true
|
||||
}
|
||||
extractDesc := func() (string, bool) {
|
||||
srcset = strings.TrimLeft(srcset, asciiSpaces)
|
||||
if srcset == "" {
|
||||
return "", false
|
||||
}
|
||||
inParentheses := false
|
||||
for i := range len(srcset) {
|
||||
c := srcset[i]
|
||||
if inParentheses {
|
||||
inParentheses = c != ')'
|
||||
continue
|
||||
}
|
||||
if c == '(' {
|
||||
inParentheses = true
|
||||
continue
|
||||
}
|
||||
if strings.IndexByte(asciiSpacesAndComma, c) != -1 {
|
||||
desc := srcset[:i]
|
||||
srcset = srcset[i+1:]
|
||||
return desc, c != ','
|
||||
}
|
||||
}
|
||||
desc := srcset
|
||||
srcset = ""
|
||||
return desc, false
|
||||
}
|
||||
|
||||
for {
|
||||
link, hasDesc := extractLink()
|
||||
if link == "" {
|
||||
break
|
||||
}
|
||||
var descriptors []string
|
||||
if hasDesc {
|
||||
for {
|
||||
desc, hasMoreDesc := extractDesc()
|
||||
if desc != "" {
|
||||
descriptors = append(descriptors, desc)
|
||||
}
|
||||
if !hasMoreDesc {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates = append(candidates, SrcSetCandidate{SrcLink: link, SrcDesc: descriptors})
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func EncodeSrcset(candidates []SrcSetCandidate) string {
|
||||
var sb strings.Builder
|
||||
for i, c := range candidates {
|
||||
if i != 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
link := c.SrcLink
|
||||
if prefix, ok := strings.CutSuffix(link, ","); ok {
|
||||
link = prefix + "%2C"
|
||||
}
|
||||
sb.WriteString(link)
|
||||
for _, d := range c.SrcDesc {
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(d)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package htmlutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSrcset(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
input string
|
||||
candidates []SrcSetCandidate
|
||||
encoded string
|
||||
}{
|
||||
{
|
||||
name: "bare URL",
|
||||
input: "image.png",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "image.png"}},
|
||||
},
|
||||
{
|
||||
name: "bare URLs",
|
||||
input: "a.png, b.png , c.png,",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png"}, {SrcLink: "b.png"}, {SrcLink: "c.png"}},
|
||||
encoded: "a.png, b.png, c.png",
|
||||
},
|
||||
{
|
||||
name: "empty candidates",
|
||||
input: ", ,\ta.png 1x,, ,b.png 2x, ,",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png", SrcDesc: []string{"1x"}}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
encoded: "a.png 1x, b.png 2x",
|
||||
},
|
||||
{
|
||||
name: "trailing commas in URL",
|
||||
input: "a.png,, b.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png"}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
encoded: "a.png, b.png 2x",
|
||||
},
|
||||
{
|
||||
name: "comma in URL",
|
||||
input: "comma,in,url.png",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "comma,in,url.png"}},
|
||||
},
|
||||
{
|
||||
name: "embedded data URL",
|
||||
input: "data:image/png;base64,abcd 1x, image.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "data:image/png;base64,abcd", SrcDesc: []string{"1x"}}, {SrcLink: "image.png", SrcDesc: []string{"2x"}}},
|
||||
},
|
||||
{
|
||||
name: "parenthesized descriptor",
|
||||
input: "a.png (x,y) 1x, b.png 2x",
|
||||
candidates: []SrcSetCandidate{{SrcLink: "a.png", SrcDesc: []string{"(x,y)", "1x"}}, {SrcLink: "b.png", SrcDesc: []string{"2x"}}},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
candidates := ParseSrcset(test.input)
|
||||
assert.Equal(t, test.candidates, candidates)
|
||||
encoded := EncodeSrcset(candidates)
|
||||
assert.Equal(t, util.IfZero(test.encoded, test.input), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("encode trailing commas", func(t *testing.T) {
|
||||
encoded := EncodeSrcset([]SrcSetCandidate{
|
||||
{SrcLink: "image,"},
|
||||
{SrcLink: "image,,", SrcDesc: []string{"2x"}},
|
||||
{SrcLink: "image.svg?variant=light,", SrcDesc: []string{"3x"}},
|
||||
})
|
||||
assert.Equal(t, "image%2C, image,%2C 2x, image.svg?variant=light%2C 3x", encoded)
|
||||
assert.Equal(t, []SrcSetCandidate{
|
||||
{SrcLink: "image%2C"},
|
||||
{SrcLink: "image,%2C", SrcDesc: []string{"2x"}},
|
||||
{SrcLink: "image.svg?variant=light%2C", SrcDesc: []string{"3x"}},
|
||||
}, ParseSrcset(encoded))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user