Skip to content

Commit c87c66f

Browse files
committed
fix
1 parent 5eebe1d commit c87c66f

29 files changed

+467
-321
lines changed

modules/html/html.go

Lines changed: 0 additions & 25 deletions
This file was deleted.

modules/htmlutil/html.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package htmlutil
5+
6+
import (
7+
"fmt"
8+
"html"
9+
"html/template"
10+
"slices"
11+
"strings"
12+
)
13+
14+
// ParseSizeAndClass get size and class from string with default values
15+
// If present, "others" expects the new size first and then the classes to use
16+
func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) {
17+
size := defaultSize
18+
if len(others) >= 1 {
19+
if v, ok := others[0].(int); ok && v != 0 {
20+
size = v
21+
}
22+
}
23+
class := defaultClass
24+
if len(others) >= 2 {
25+
if v, ok := others[1].(string); ok && v != "" {
26+
if class != "" {
27+
class += " "
28+
}
29+
class += v
30+
}
31+
}
32+
return size, class
33+
}
34+
35+
func HTMLFormat(s string, rawArgs ...any) template.HTML {
36+
args := slices.Clone(rawArgs)
37+
for i, v := range args {
38+
switch v := v.(type) {
39+
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
40+
// for most basic types (including template.HTML which is safe), just do nothing and use it
41+
case string:
42+
args[i] = template.HTMLEscapeString(v)
43+
case fmt.Stringer:
44+
args[i] = template.HTMLEscapeString(v.String())
45+
default:
46+
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
47+
}
48+
}
49+
return template.HTML(fmt.Sprintf(s, args...))
50+
}
51+
52+
func CreateElementHTML[T template.HTML | string](tag string, attributes map[string]any, content ...T) template.HTML {
53+
var sb strings.Builder
54+
sb.WriteString("<")
55+
sb.WriteString(tag)
56+
for k, v := range attributes {
57+
if v == nil {
58+
continue
59+
}
60+
sb.WriteString(" ")
61+
sb.WriteString(html.EscapeString(k))
62+
sb.WriteString(`="`)
63+
sb.WriteString(html.EscapeString(fmt.Sprint(v)))
64+
sb.WriteString(`"`)
65+
}
66+
sb.WriteString(">")
67+
for _, c := range content {
68+
switch c := any(c).(type) {
69+
case template.HTML:
70+
sb.WriteString(string(c))
71+
case string:
72+
sb.WriteString(html.EscapeString(c))
73+
default:
74+
panic(fmt.Sprintf("unexpected type %T", c))
75+
}
76+
}
77+
sb.WriteString("</")
78+
sb.WriteString(tag)
79+
sb.WriteString(">")
80+
return template.HTML(sb.String())
81+
}

modules/htmlutil/html_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package htmlutil
5+
6+
import (
7+
"html/template"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestHTMLFormat(t *testing.T) {
14+
assert.Equal(t, template.HTML("<a>&lt; < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1))
15+
}

modules/markup/html.go

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ const (
2525
IssueNameStyleRegexp = "regexp"
2626
)
2727

28-
// CSS class for action keywords (e.g. "closes: #1")
29-
const keywordClass = "issue-keyword"
30-
3128
type globalVarsType struct {
3229
hashCurrentPattern *regexp.Regexp
3330
shortLinkPattern *regexp.Regexp
@@ -39,6 +36,7 @@ type globalVarsType struct {
3936
emojiShortCodeRegex *regexp.Regexp
4037
issueFullPattern *regexp.Regexp
4138
filesChangedFullPattern *regexp.Regexp
39+
codePreviewPattern *regexp.Regexp
4240

4341
tagCleaner *regexp.Regexp
4442
nulCleaner *strings.Replacer
@@ -88,6 +86,9 @@ var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType {
8886
// example: https://domain/org/repo/pulls/27/files#hash
8987
v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`)
9088

89+
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
90+
v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
91+
9192
v.tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`)
9293
v.nulCleaner = strings.NewReplacer("\000", "")
9394
return v
@@ -164,11 +165,7 @@ var defaultProcessors = []processor{
164165
// emails with HTML links, parsing shortlinks in the format of [[Link]], like
165166
// MediaWiki, linking issues in the format #ID, and mentions in the format
166167
// @user, and others.
167-
func PostProcess(
168-
ctx *RenderContext,
169-
input io.Reader,
170-
output io.Writer,
171-
) error {
168+
func PostProcess(ctx *RenderContext, input io.Reader, output io.Writer) error {
172169
return postProcess(ctx, defaultProcessors, input, output)
173170
}
174171

@@ -189,10 +186,7 @@ var commitMessageProcessors = []processor{
189186
// RenderCommitMessage will use the same logic as PostProcess, but will disable
190187
// the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
191188
// set, which changes every text node into a link to the passed default link.
192-
func RenderCommitMessage(
193-
ctx *RenderContext,
194-
content string,
195-
) (string, error) {
189+
func RenderCommitMessage(ctx *RenderContext, content string) (string, error) {
196190
procs := commitMessageProcessors
197191
return renderProcessString(ctx, procs, content)
198192
}
@@ -219,10 +213,7 @@ var emojiProcessors = []processor{
219213
// RenderCommitMessage, but will disable the shortLinkProcessor and
220214
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
221215
// which changes every text node into a link to the passed default link.
222-
func RenderCommitMessageSubject(
223-
ctx *RenderContext,
224-
defaultLink, content string,
225-
) (string, error) {
216+
func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
226217
procs := slices.Clone(commitMessageSubjectProcessors)
227218
procs = append(procs, func(ctx *RenderContext, node *html.Node) {
228219
ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data}
@@ -236,10 +227,7 @@ func RenderCommitMessageSubject(
236227
}
237228

238229
// RenderIssueTitle to process title on individual issue/pull page
239-
func RenderIssueTitle(
240-
ctx *RenderContext,
241-
title string,
242-
) (string, error) {
230+
func RenderIssueTitle(ctx *RenderContext, title string) (string, error) {
243231
// do not render other issue/commit links in an issue's title - which in most cases is already a link.
244232
return renderProcessString(ctx, []processor{
245233
emojiShortCodeProcessor,
@@ -257,10 +245,7 @@ func renderProcessString(ctx *RenderContext, procs []processor, content string)
257245

258246
// RenderDescriptionHTML will use similar logic as PostProcess, but will
259247
// use a single special linkProcessor.
260-
func RenderDescriptionHTML(
261-
ctx *RenderContext,
262-
content string,
263-
) (string, error) {
248+
func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) {
264249
return renderProcessString(ctx, []processor{
265250
descriptionLinkProcessor,
266251
emojiShortCodeProcessor,
@@ -270,10 +255,7 @@ func RenderDescriptionHTML(
270255

271256
// RenderEmoji for when we want to just process emoji and shortcodes
272257
// in various places it isn't already run through the normal markdown processor
273-
func RenderEmoji(
274-
ctx *RenderContext,
275-
content string,
276-
) (string, error) {
258+
func RenderEmoji(ctx *RenderContext, content string) (string, error) {
277259
return renderProcessString(ctx, emojiProcessors, content)
278260
}
279261

@@ -405,13 +387,16 @@ func processTextNodes(ctx *RenderContext, procs []processor, node *html.Node) {
405387
}
406388

407389
// createKeyword() renders a highlighted version of an action keyword
408-
func createKeyword(content string) *html.Node {
390+
func createKeyword(ctx *RenderContext, content string) *html.Node {
391+
// CSS class for action keywords (e.g. "closes: #1")
392+
const keywordClass = "issue-keyword"
393+
409394
span := &html.Node{
410395
Type: html.ElementNode,
411396
Data: atom.Span.String(),
412397
Attr: []html.Attribute{},
413398
}
414-
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: keywordClass})
399+
span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", keywordClass))
415400

416401
text := &html.Node{
417402
Type: html.TextNode,
@@ -422,7 +407,7 @@ func createKeyword(content string) *html.Node {
422407
return span
423408
}
424409

425-
func createLink(href, content, class string) *html.Node {
410+
func createLink(ctx *RenderContext, href, content, class string) *html.Node {
426411
a := &html.Node{
427412
Type: html.ElementNode,
428413
Data: atom.A.String(),
@@ -432,7 +417,7 @@ func createLink(href, content, class string) *html.Node {
432417
a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"})
433418
}
434419
if class != "" {
435-
a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
420+
a.Attr = append(a.Attr, ctx.RenderInternal.NodeSafeAttr("class", class))
436421
}
437422

438423
text := &html.Node{

modules/markup/html_codepreview.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package markup
66
import (
77
"html/template"
88
"net/url"
9-
"regexp"
109
"strconv"
1110
"strings"
1211

@@ -16,9 +15,6 @@ import (
1615
"golang.org/x/net/html"
1716
)
1817

19-
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
20-
var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
21-
2218
type RenderCodePreviewOptions struct {
2319
FullURL string
2420
OwnerName string
@@ -30,7 +26,7 @@ type RenderCodePreviewOptions struct {
3026
}
3127

3228
func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
33-
m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
29+
m := globalVars().codePreviewPattern.FindStringSubmatchIndex(node.Data)
3430
if m == nil {
3531
return 0, 0, "", nil
3632
}
@@ -66,8 +62,8 @@ func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
6662
node = node.NextSibling
6763
continue
6864
}
69-
urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
70-
if err != nil || h == "" {
65+
urlPosStart, urlPosEnd, renderedCodeBlock, err := renderCodeBlock(ctx, node)
66+
if err != nil || renderedCodeBlock == "" {
7167
if err != nil {
7268
log.Error("Unable to render code preview: %v", err)
7369
}
@@ -84,7 +80,8 @@ func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
8480
// then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>",
8581
// so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node.
8682
node.Data = textBefore
87-
node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
83+
renderedCodeNode := &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(renderedCodeBlock))}
84+
node.Parent.InsertBefore(renderedCodeNode, next)
8885
if textAfter != "" {
8986
node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next)
9087
}

modules/markup/html_email.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
1515
}
1616

1717
mail := node.Data[m[2]:m[3]]
18-
replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto"))
18+
replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "mailto"))
1919
node = node.NextSibling.NextSibling
2020
}
2121
}

modules/markup/html_emoji.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ func createEmoji(content, class, name string) *html.Node {
3535
return span
3636
}
3737

38-
func createCustomEmoji(alias string) *html.Node {
38+
func createCustomEmoji(ctx *RenderContext, alias string) *html.Node {
3939
span := &html.Node{
4040
Type: html.ElementNode,
4141
Data: atom.Span.String(),
4242
Attr: []html.Attribute{},
4343
}
44-
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: "emoji"})
44+
span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", "emoji"))
4545
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias})
4646

4747
img := &html.Node{
@@ -77,7 +77,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
7777
if converted == nil {
7878
// check if this is a custom reaction
7979
if _, exist := setting.UI.CustomEmojisMap[alias]; exist {
80-
replaceContent(node, m[0], m[1], createCustomEmoji(alias))
80+
replaceContent(node, m[0], m[1], createCustomEmoji(ctx, alias))
8181
node = node.NextSibling.NextSibling
8282
start = 0
8383
continue

modules/markup/html_issue.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
5757
matchRepo := linkParts[len(linkParts)-3]
5858

5959
if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] {
60-
replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue"))
60+
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
6161
} else {
6262
text = matchOrg + "/" + matchRepo + text
63-
replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue"))
63+
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
6464
}
6565
node = node.NextSibling.NextSibling
6666
}
@@ -129,16 +129,16 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
129129
log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
130130
}
131131

132-
link = createLink(res, reftext, "ref-issue ref-external-issue")
132+
link = createLink(ctx, res, reftext, "ref-issue ref-external-issue")
133133
} else {
134134
// Path determines the type of link that will be rendered. It's unknown at this point whether
135135
// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
136136
// Gitea will redirect on click as appropriate.
137137
issuePath := util.Iif(ref.IsPull, "pulls", "issues")
138138
if ref.Owner == "" {
139-
link = createLink(util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue")
139+
link = createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue")
140140
} else {
141-
link = createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue")
141+
link = createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue")
142142
}
143143
}
144144

@@ -151,7 +151,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
151151
// Decorate action keywords if actionable
152152
var keyword *html.Node
153153
if references.IsXrefActionable(ref, hasExtTrackFormat) {
154-
keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
154+
keyword = createKeyword(ctx, node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
155155
} else {
156156
keyword = &html.Node{
157157
Type: html.TextNode,
@@ -177,7 +177,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
177177
}
178178

179179
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
180-
link := createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
180+
link := createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
181181

182182
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
183183
node = node.NextSibling.NextSibling

0 commit comments

Comments
 (0)