Skip to content

Commit fec8cf3

Browse files
committed
fix
1 parent 5eebe1d commit fec8cf3

31 files changed

+490
-398
lines changed

modules/html/html.go

-25
This file was deleted.

modules/htmlutil/html.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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/template"
9+
"slices"
10+
)
11+
12+
// ParseSizeAndClass get size and class from string with default values
13+
// If present, "others" expects the new size first and then the classes to use
14+
func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) {
15+
size := defaultSize
16+
if len(others) >= 1 {
17+
if v, ok := others[0].(int); ok && v != 0 {
18+
size = v
19+
}
20+
}
21+
class := defaultClass
22+
if len(others) >= 2 {
23+
if v, ok := others[1].(string); ok && v != "" {
24+
if class != "" {
25+
class += " "
26+
}
27+
class += v
28+
}
29+
}
30+
return size, class
31+
}
32+
33+
func HTMLFormat(s string, rawArgs ...any) template.HTML {
34+
args := slices.Clone(rawArgs)
35+
for i, v := range args {
36+
switch v := v.(type) {
37+
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
38+
// for most basic types (including template.HTML which is safe), just do nothing and use it
39+
case string:
40+
args[i] = template.HTMLEscapeString(v)
41+
case fmt.Stringer:
42+
args[i] = template.HTMLEscapeString(v.String())
43+
default:
44+
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
45+
}
46+
}
47+
return template.HTML(fmt.Sprintf(s, args...))
48+
}

modules/htmlutil/html_test.go

+15
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

+38-70
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

@@ -333,6 +315,17 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
333315
return nil
334316
}
335317

318+
func isEmojiNode(node *html.Node) bool {
319+
if node.Type == html.ElementNode && node.Data == atom.Span.String() {
320+
for _, attr := range node.Attr {
321+
if (attr.Key == "class" || attr.Key == "data-attr-class") && strings.Contains(attr.Val, "emoji") {
322+
return true
323+
}
324+
}
325+
}
326+
return false
327+
}
328+
336329
func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node {
337330
// Add user-content- to IDs and "#" links if they don't already have them
338331
for idx, attr := range node.Attr {
@@ -346,47 +339,27 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
346339
if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix {
347340
node.Attr[idx].Val = "#user-content-" + val
348341
}
349-
350-
if attr.Key == "class" && attr.Val == "emoji" {
351-
procs = nil
352-
}
353342
}
354343

355344
switch node.Type {
356345
case html.TextNode:
357-
processTextNodes(ctx, procs, node)
346+
for _, proc := range procs {
347+
proc(ctx, node) // it might add siblings
348+
}
349+
358350
case html.ElementNode:
359-
if node.Data == "code" || node.Data == "pre" {
360-
// ignore code and pre nodes
351+
if isEmojiNode(node) {
352+
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
353+
// if we don't stop it, it will go into the TextNode again and create an infinite recursion
361354
return node.NextSibling
355+
} else if node.Data == "code" || node.Data == "pre" {
356+
return node.NextSibling // ignore code and pre nodes
362357
} else if node.Data == "img" {
363358
return visitNodeImg(ctx, node)
364359
} else if node.Data == "video" {
365360
return visitNodeVideo(ctx, node)
366361
} else if node.Data == "a" {
367-
// Restrict text in links to emojis
368-
procs = emojiProcessors
369-
} else if node.Data == "i" {
370-
for _, attr := range node.Attr {
371-
if attr.Key != "class" {
372-
continue
373-
}
374-
classes := strings.Split(attr.Val, " ")
375-
for i, class := range classes {
376-
if class == "icon" {
377-
classes[0], classes[i] = classes[i], classes[0]
378-
attr.Val = strings.Join(classes, " ")
379-
380-
// Remove all children of icons
381-
child := node.FirstChild
382-
for child != nil {
383-
node.RemoveChild(child)
384-
child = node.FirstChild
385-
}
386-
break
387-
}
388-
}
389-
}
362+
procs = emojiProcessors // Restrict text in links to emojis
390363
}
391364
for n := node.FirstChild; n != nil; {
392365
n = visitNode(ctx, procs, n)
@@ -396,22 +369,17 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
396369
return node.NextSibling
397370
}
398371

399-
// processTextNodes runs the passed node through various processors, in order to handle
400-
// all kinds of special links handled by the post-processing.
401-
func processTextNodes(ctx *RenderContext, procs []processor, node *html.Node) {
402-
for _, p := range procs {
403-
p(ctx, node)
404-
}
405-
}
406-
407372
// createKeyword() renders a highlighted version of an action keyword
408-
func createKeyword(content string) *html.Node {
373+
func createKeyword(ctx *RenderContext, content string) *html.Node {
374+
// CSS class for action keywords (e.g. "closes: #1")
375+
const keywordClass = "issue-keyword"
376+
409377
span := &html.Node{
410378
Type: html.ElementNode,
411379
Data: atom.Span.String(),
412380
Attr: []html.Attribute{},
413381
}
414-
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: keywordClass})
382+
span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", keywordClass))
415383

416384
text := &html.Node{
417385
Type: html.TextNode,
@@ -422,7 +390,7 @@ func createKeyword(content string) *html.Node {
422390
return span
423391
}
424392

425-
func createLink(href, content, class string) *html.Node {
393+
func createLink(ctx *RenderContext, href, content, class string) *html.Node {
426394
a := &html.Node{
427395
Type: html.ElementNode,
428396
Data: atom.A.String(),
@@ -432,7 +400,7 @@ func createLink(href, content, class string) *html.Node {
432400
a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"})
433401
}
434402
if class != "" {
435-
a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
403+
a.Attr = append(a.Attr, ctx.RenderInternal.NodeSafeAttr("class", class))
436404
}
437405

438406
text := &html.Node{

modules/markup/html_codepreview.go

+5-8
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

+1-1
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
}

0 commit comments

Comments
 (0)