Skip to content

Commit 8fcbdf0

Browse files
authored
Refactor flash message and remove SanitizeHTML template func (#37179)
1. Fix the "flash message" layout problem for different cases * I am sure most of the users should have ever seen the ugly center-aligned error message with multiple lines. 2. Fix inconsistent "Details" flash message EOL handling, sometimes `\n`, sometimes `<br>` * Now, always use "\n" and use `<pre>` to render 3. Remove SanitizeHTML template func because it is not useful and can be easily abused. * But it is still kept for mail templates, for example: #36049 4. Clarify PostProcessCommitMessage's behavior and add FIXME comment By the way: cleaned up some devtest pages, move embedded style block to CSS file
1 parent ba9258c commit 8fcbdf0

File tree

29 files changed

+159
-113
lines changed

29 files changed

+159
-113
lines changed

models/issues/comment.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"code.gitea.io/gitea/modules/htmlutil"
2525
"code.gitea.io/gitea/modules/json"
2626
"code.gitea.io/gitea/modules/log"
27+
"code.gitea.io/gitea/modules/markup"
2728
"code.gitea.io/gitea/modules/optional"
2829
"code.gitea.io/gitea/modules/references"
2930
"code.gitea.io/gitea/modules/structs"
@@ -543,6 +544,12 @@ func (c *Comment) EventTag() string {
543544
return fmt.Sprintf("event-%d", c.ID)
544545
}
545546

547+
func (c *Comment) GetSanitizedContentHTML() template.HTML {
548+
// mainly for type=4 CommentTypeCommitRef
549+
// the content is a link like <a href="{RepoLink}/commit/{CommitID}">message title</a> (from CreateRefComment)
550+
return markup.Sanitize(c.Content)
551+
}
552+
546553
// LoadLabel if comment.Type is CommentTypeLabel, then load Label
547554
func (c *Comment) LoadLabel(ctx context.Context) error {
548555
var label Label

modules/markup/html.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package markup
66
import (
77
"bytes"
88
"fmt"
9+
"html/template"
910
"io"
1011
"regexp"
1112
"slices"
@@ -149,9 +150,9 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e
149150
return postProcess(ctx, procs, input, output)
150151
}
151152

152-
// PostProcessCommitMessage will use the same logic as PostProcess, but will disable
153-
// the shortLinkProcessor.
154-
func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error) {
153+
// PostProcessCommitMessage will use the same logic as PostProcess, but will disable the shortLinkProcessor.
154+
// FIXME: this function and its family have a very strange design: it takes HTML as input and output, processes the "escaped" content.
155+
func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (template.HTML, error) {
155156
procs := []processor{
156157
fullIssuePatternProcessor,
157158
comparePatternProcessor,
@@ -165,7 +166,8 @@ func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error
165166
emojiProcessor,
166167
emojiShortCodeProcessor,
167168
}
168-
return postProcessString(ctx, procs, content)
169+
s, err := postProcessString(ctx, procs, string(content))
170+
return template.HTML(s), err
169171
}
170172

171173
var emojiProcessors = []processor{

modules/templates/helper.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@ func newFuncMapWebPage() template.FuncMap {
3232

3333
// -----------------------------------------------------------------
3434
// html/template related functions
35-
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
36-
"Iif": iif,
37-
"Eval": evalTokens,
38-
"HTMLFormat": htmlFormat,
39-
"QueryEscape": queryEscape,
40-
"QueryBuild": QueryBuild,
41-
"SanitizeHTML": SanitizeHTML,
35+
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
36+
"Iif": iif,
37+
"Eval": evalTokens,
38+
"HTMLFormat": htmlFormat,
39+
"QueryEscape": queryEscape,
40+
"QueryBuild": QueryBuild,
4241

4342
"PathEscape": url.PathEscape,
4443
"PathEscapeSegments": util.PathEscapeSegments,
@@ -146,9 +145,8 @@ func newFuncMapWebPage() template.FuncMap {
146145
}
147146
}
148147

149-
// SanitizeHTML sanitizes the input by default sanitization rules.
150-
func SanitizeHTML(s string) template.HTML {
151-
return markup.Sanitize(s)
148+
func sanitizeHTML(msg string) template.HTML {
149+
return markup.Sanitize(msg)
152150
}
153151

154152
func htmlFormat(s any, args ...any) template.HTML {

modules/templates/helper_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func TestSubjectBodySeparator(t *testing.T) {
5858
}
5959

6060
func TestSanitizeHTML(t *testing.T) {
61-
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
61+
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), sanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
6262
}
6363

6464
func TestTemplateIif(t *testing.T) {

modules/templates/mail.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,16 @@ func mailBodyFuncMap() template.FuncMap {
6565
"NIL": func() any { return nil },
6666

6767
// html/template related functions
68-
"dict": dict,
69-
"Iif": iif,
70-
"Eval": evalTokens,
71-
"HTMLFormat": htmlFormat,
72-
"QueryEscape": queryEscape,
73-
"QueryBuild": QueryBuild,
74-
"SanitizeHTML": SanitizeHTML,
68+
"dict": dict,
69+
"Iif": iif,
70+
"Eval": evalTokens,
71+
"HTMLFormat": htmlFormat,
72+
"QueryEscape": queryEscape,
73+
"QueryBuild": QueryBuild,
74+
75+
// deprecated, use "HTMLFormat" instead, but some user custom mail templates still use it
76+
// see: https://github.com/go-gitea/gitea/issues/36049
77+
"SanitizeHTML": sanitizeHTML,
7578

7679
"PathEscape": url.PathEscape,
7780
"PathEscapeSegments": util.PathEscapeSegments,

modules/templates/util_render.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils {
4040

4141
// RenderCommitMessage renders commit message with XSS-safe and special links.
4242
func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML {
43-
cleanMsg := template.HTMLEscapeString(msg)
43+
cleanMsg := template.HTML(template.HTMLEscapeString(msg))
4444
// we can safely assume that it will not return any error, since there shouldn't be any special HTML.
4545
// "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed.
4646
fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg)
4747
if err != nil {
4848
log.Error("PostProcessCommitMessage: %v", err)
4949
return ""
5050
}
51-
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
51+
msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n")
5252
if len(msgLines) == 0 {
5353
return ""
5454
}
@@ -91,12 +91,14 @@ func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) templ
9191
return ""
9292
}
9393

94-
renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine))
94+
rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo)
95+
htmlContent := template.HTML(template.HTMLEscapeString(msgLine))
96+
renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent)
9597
if err != nil {
9698
log.Error("PostProcessCommitMessage: %v", err)
9799
return ""
98100
}
99-
return template.HTML(renderedMessage)
101+
return renderedMessage
100102
}
101103

102104
// Match text that is between back ticks.
@@ -279,6 +281,35 @@ func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize in
279281
return htmlutil.HTMLFormat(`<div class="theme-menu-item" data-tooltip-content="%s">%s %s %s</div>`, info.GetDescription(), icon, info.DisplayName, extraIcon)
280282
}
281283

284+
func (ut *RenderUtils) RenderFlashMessage(typ, msg string) template.HTML {
285+
msg = strings.TrimSpace(msg)
286+
if msg == "" {
287+
return ""
288+
}
289+
290+
cls := typ
291+
// legacy logic: "negative" for error, "positive" for success
292+
switch cls {
293+
case "error":
294+
cls = "negative"
295+
case "success":
296+
cls = "positive"
297+
}
298+
299+
var msgContent template.HTML
300+
if strings.Contains(msg, "</pre>") || strings.Contains(msg, "</details>") || strings.Contains(msg, "</ul>") || strings.Contains(msg, "</div>") {
301+
// If the message contains some known "block" elements, no need to do more alignment or line-break processing, just sanitize it directly.
302+
msgContent = sanitizeHTML(msg)
303+
} else if !strings.Contains(msg, "\n") {
304+
// If the message is a single line, center-align it by wrapping it
305+
msgContent = htmlutil.HTMLFormat(`<div class="tw-text-center">%s</div>`, sanitizeHTML(msg))
306+
} else {
307+
// For a multi-line message, preserve line breaks, and left-align it.
308+
msgContent = htmlutil.HTMLFormat(`%s`, sanitizeHTML(strings.ReplaceAll(msg, "\n", "<br>")))
309+
}
310+
return htmlutil.HTMLFormat(`<div class="ui %s message flash-message flash-%s">%s</div>`, cls, typ, msgContent)
311+
}
312+
282313
func (ut *RenderUtils) RenderUnicodeEscapeToggleButton(escapeStatus *charset.EscapeStatus) template.HTML {
283314
if escapeStatus == nil || !escapeStatus.Escaped {
284315
return ""

routers/utils/utils.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ package utils
55

66
import (
77
"html"
8-
"strings"
8+
"html/template"
99
)
1010

11-
// SanitizeFlashErrorString will sanitize a flash error string
12-
func SanitizeFlashErrorString(x string) string {
13-
return strings.ReplaceAll(html.EscapeString(x), "\n", "<br>")
11+
// EscapeFlashErrorString will escape the flash error string
12+
// Maybe do more sanitization in the future, e.g.: hide sensitive information, etc.
13+
func EscapeFlashErrorString(x string) template.HTML {
14+
return template.HTML(html.EscapeString(x))
1415
}

routers/utils/utils_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
package utils
55

66
import (
7+
"html/template"
78
"testing"
89

910
"github.com/stretchr/testify/assert"
1011
)
1112

12-
func TestSanitizeFlashErrorString(t *testing.T) {
13+
func TestEscapeFlashErrorString(t *testing.T) {
1314
tests := []struct {
1415
name string
1516
arg string
16-
want string
17+
want template.HTML
1718
}{
1819
{
1920
name: "no error",
@@ -28,13 +29,13 @@ func TestSanitizeFlashErrorString(t *testing.T) {
2829
{
2930
name: "line break error",
3031
arg: "some error:\n\nawesome!",
31-
want: "some error:<br><br>awesome!",
32+
want: "some error:\n\nawesome!",
3233
},
3334
}
3435

3536
for _, tt := range tests {
3637
t.Run(tt.name, func(t *testing.T) {
37-
got := SanitizeFlashErrorString(tt.arg)
38+
got := EscapeFlashErrorString(tt.arg)
3839
assert.Equal(t, tt.want, got)
3940
})
4041
}

routers/web/auth/oauth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func SignInOAuthCallback(ctx *context.Context) {
7979
}
8080
}
8181
sort.Strings(errorKeyValues)
82-
ctx.Flash.Error(strings.Join(errorKeyValues, "<br>"), true)
82+
ctx.Flash.Error(strings.Join(errorKeyValues, "\n"), true)
8383
}
8484

8585
// first look if the provider is still active

routers/web/devtest/devtest.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ func List(ctx *context.Context) {
4545

4646
func FetchActionTest(ctx *context.Context) {
4747
_ = ctx.Req.ParseForm()
48-
ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "<br>" +
49-
"Form: " + ctx.Req.Form.Encode() + "<br>" +
48+
ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" +
49+
"Form: " + ctx.Req.Form.Encode() + "\n" +
5050
"PostForm: " + ctx.Req.PostForm.Encode(),
5151
)
5252
time.Sleep(2 * time.Second)
@@ -192,11 +192,31 @@ func prepareMockData(ctx *context.Context) {
192192
prepareMockDataBadgeActionsSvg(ctx)
193193
case "/devtest/relative-time":
194194
prepareMockDataRelativeTime(ctx)
195+
case "/devtest/toast-and-message":
196+
prepareMockDataToastAndMessage(ctx)
195197
case "/devtest/unicode-escape":
196198
prepareMockDataUnicodeEscape(ctx)
197199
}
198200
}
199201

202+
func prepareMockDataToastAndMessage(ctx *context.Context) {
203+
msgWithDetails, _ := ctx.RenderToHTML("base/alert_details", map[string]any{
204+
"Message": "message with details <script>escape xss</script>",
205+
"Summary": "summary with details",
206+
"Details": "details line 1\n details line 2\n details line 3",
207+
})
208+
msgWithSummary, _ := ctx.RenderToHTML("base/alert_details", map[string]any{
209+
"Message": "message with summary <script>escape xss</script>",
210+
"Summary": "summary only",
211+
})
212+
213+
ctx.Flash.ErrorMsg = string(msgWithDetails)
214+
ctx.Flash.WarningMsg = string(msgWithSummary)
215+
ctx.Flash.InfoMsg = "a long message with line break\nthe second line <script>removed xss</script>"
216+
ctx.Flash.SuccessMsg = "single line message <script>removed xss</script>"
217+
ctx.Data["Flash"] = ctx.Flash
218+
}
219+
200220
func prepareMockDataUnicodeEscape(ctx *context.Context) {
201221
content := "// demo code\n"
202222
content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n"
@@ -223,8 +243,8 @@ func TmplCommon(ctx *context.Context) {
223243
prepareMockData(ctx)
224244
if ctx.Req.Method == http.MethodPost {
225245
_ = ctx.Req.ParseForm()
226-
ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"<br>"+
227-
"Form: "+ctx.Req.Form.Encode()+"<br>"+
246+
ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+
247+
"Form: "+ctx.Req.Form.Encode()+"\n"+
228248
"PostForm: "+ctx.Req.PostForm.Encode(),
229249
true,
230250
)

0 commit comments

Comments
 (0)