From 8fcbdf05b0a94f2997dffbb40aa587e2b5c56e87 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Apr 2026 10:17:25 +0800 Subject: [PATCH] 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 `
` * Now, always use "\n" and use `
` 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:
https://github.com/go-gitea/gitea/issues/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
---
 models/issues/comment.go                      |  7 ++++
 modules/markup/html.go                        | 10 +++--
 modules/templates/helper.go                   | 18 ++++-----
 modules/templates/helper_test.go              |  2 +-
 modules/templates/mail.go                     | 17 ++++----
 modules/templates/util_render.go              | 39 +++++++++++++++++--
 routers/utils/utils.go                        |  9 +++--
 routers/utils/utils_test.go                   |  9 +++--
 routers/web/auth/oauth.go                     |  2 +-
 routers/web/devtest/devtest.go                | 28 +++++++++++--
 routers/web/feed/convert.go                   |  3 +-
 routers/web/repo/branch.go                    |  2 +-
 routers/web/repo/commit.go                    |  3 +-
 routers/web/repo/editor_error.go              |  6 +--
 routers/web/repo/issue_new.go                 |  2 +-
 routers/web/repo/issue_view.go                |  5 +--
 routers/web/repo/pull.go                      | 16 ++++----
 templates/base/alert.tmpl                     | 28 +++----------
 templates/base/alert_details.tmpl             |  6 +--
 templates/devtest/devtest-header.tmpl         |  2 +-
 templates/devtest/fetch-action.tmpl           | 18 ++-------
 .../{toast.tmpl => toast-and-message.tmpl}    |  2 +-
 templates/repo/commit_page.tmpl               |  2 +-
 .../repo/issue/view_content/comments.tmpl     |  2 +-
 tests/integration/pull_merge_test.go          |  5 +--
 tests/integration/signin_test.go              |  3 +-
 tests/integration/user_settings_test.go       |  4 +-
 web_src/css/base.css                          | 12 +++++-
 web_src/css/devtest.css                       | 10 +++--
 29 files changed, 159 insertions(+), 113 deletions(-)
 rename templates/devtest/{toast.tmpl => toast-and-message.tmpl} (96%)

diff --git a/models/issues/comment.go b/models/issues/comment.go
index 34ce7f3500..84a7150b9f 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -24,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/htmlutil"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/references"
 	"code.gitea.io/gitea/modules/structs"
@@ -543,6 +544,12 @@ func (c *Comment) EventTag() string {
 	return fmt.Sprintf("event-%d", c.ID)
 }
 
+func (c *Comment) GetSanitizedContentHTML() template.HTML {
+	// mainly for type=4 CommentTypeCommitRef
+	// the content is a link like message title (from CreateRefComment)
+	return markup.Sanitize(c.Content)
+}
+
 // LoadLabel if comment.Type is CommentTypeLabel, then load Label
 func (c *Comment) LoadLabel(ctx context.Context) error {
 	var label Label
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 1c2ae6918d..0fe37ae305 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -6,6 +6,7 @@ package markup
 import (
 	"bytes"
 	"fmt"
+	"html/template"
 	"io"
 	"regexp"
 	"slices"
@@ -149,9 +150,9 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e
 	return postProcess(ctx, procs, input, output)
 }
 
-// PostProcessCommitMessage will use the same logic as PostProcess, but will disable
-// the shortLinkProcessor.
-func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error) {
+// PostProcessCommitMessage will use the same logic as PostProcess, but will disable the shortLinkProcessor.
+// FIXME: this function and its family have a very strange design: it takes HTML as input and output, processes the "escaped" content.
+func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (template.HTML, error) {
 	procs := []processor{
 		fullIssuePatternProcessor,
 		comparePatternProcessor,
@@ -165,7 +166,8 @@ func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error
 		emojiProcessor,
 		emojiShortCodeProcessor,
 	}
-	return postProcessString(ctx, procs, content)
+	s, err := postProcessString(ctx, procs, string(content))
+	return template.HTML(s), err
 }
 
 var emojiProcessors = []processor{
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index f81be1255a..3e4289c8ad 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -32,13 +32,12 @@ func newFuncMapWebPage() template.FuncMap {
 
 		// -----------------------------------------------------------------
 		// html/template related functions
-		"dict":         dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
-		"Iif":          iif,
-		"Eval":         evalTokens,
-		"HTMLFormat":   htmlFormat,
-		"QueryEscape":  queryEscape,
-		"QueryBuild":   QueryBuild,
-		"SanitizeHTML": SanitizeHTML,
+		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
+		"Iif":         iif,
+		"Eval":        evalTokens,
+		"HTMLFormat":  htmlFormat,
+		"QueryEscape": queryEscape,
+		"QueryBuild":  QueryBuild,
 
 		"PathEscape":         url.PathEscape,
 		"PathEscapeSegments": util.PathEscapeSegments,
@@ -146,9 +145,8 @@ func newFuncMapWebPage() template.FuncMap {
 	}
 }
 
-// SanitizeHTML sanitizes the input by default sanitization rules.
-func SanitizeHTML(s string) template.HTML {
-	return markup.Sanitize(s)
+func sanitizeHTML(msg string) template.HTML {
+	return markup.Sanitize(msg)
 }
 
 func htmlFormat(s any, args ...any) template.HTML {
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index f90818c0ad..cf1db32476 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -58,7 +58,7 @@ func TestSubjectBodySeparator(t *testing.T) {
 }
 
 func TestSanitizeHTML(t *testing.T) {
-	assert.Equal(t, template.HTML(`link xss 
inline
`), SanitizeHTML(`link xss
inline
`)) + assert.Equal(t, template.HTML(`link xss
inline
`), sanitizeHTML(`link xss
inline
`)) } func TestTemplateIif(t *testing.T) { diff --git a/modules/templates/mail.go b/modules/templates/mail.go index 181c6312b0..f81073902f 100644 --- a/modules/templates/mail.go +++ b/modules/templates/mail.go @@ -65,13 +65,16 @@ func mailBodyFuncMap() template.FuncMap { "NIL": func() any { return nil }, // html/template related functions - "dict": dict, - "Iif": iif, - "Eval": evalTokens, - "HTMLFormat": htmlFormat, - "QueryEscape": queryEscape, - "QueryBuild": QueryBuild, - "SanitizeHTML": SanitizeHTML, + "dict": dict, + "Iif": iif, + "Eval": evalTokens, + "HTMLFormat": htmlFormat, + "QueryEscape": queryEscape, + "QueryBuild": QueryBuild, + + // deprecated, use "HTMLFormat" instead, but some user custom mail templates still use it + // see: https://github.com/go-gitea/gitea/issues/36049 + "SanitizeHTML": sanitizeHTML, "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 081a13fb9e..6641ee7959 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -40,7 +40,7 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { // RenderCommitMessage renders commit message with XSS-safe and special links. func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { - cleanMsg := template.HTMLEscapeString(msg) + cleanMsg := template.HTML(template.HTMLEscapeString(msg)) // we can safely assume that it will not return any error, since there shouldn't be any special HTML. // "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed. fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) @@ -48,7 +48,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) te log.Error("PostProcessCommitMessage: %v", err) return "" } - msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") + msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n") if len(msgLines) == 0 { return "" } @@ -91,12 +91,14 @@ func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) templ return "" } - renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine)) + rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) + htmlContent := template.HTML(template.HTMLEscapeString(msgLine)) + renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent) if err != nil { log.Error("PostProcessCommitMessage: %v", err) return "" } - return template.HTML(renderedMessage) + return renderedMessage } // Match text that is between back ticks. @@ -279,6 +281,35 @@ func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize in return htmlutil.HTMLFormat(`
%s %s %s
`, info.GetDescription(), icon, info.DisplayName, extraIcon) } +func (ut *RenderUtils) RenderFlashMessage(typ, msg string) template.HTML { + msg = strings.TrimSpace(msg) + if msg == "" { + return "" + } + + cls := typ + // legacy logic: "negative" for error, "positive" for success + switch cls { + case "error": + cls = "negative" + case "success": + cls = "positive" + } + + var msgContent template.HTML + if strings.Contains(msg, "
") || strings.Contains(msg, "") || strings.Contains(msg, "") || strings.Contains(msg, "") { + // If the message contains some known "block" elements, no need to do more alignment or line-break processing, just sanitize it directly. + msgContent = sanitizeHTML(msg) + } else if !strings.Contains(msg, "\n") { + // If the message is a single line, center-align it by wrapping it + msgContent = htmlutil.HTMLFormat(`
%s
`, sanitizeHTML(msg)) + } else { + // For a multi-line message, preserve line breaks, and left-align it. + msgContent = htmlutil.HTMLFormat(`%s`, sanitizeHTML(strings.ReplaceAll(msg, "\n", "
"))) + } + return htmlutil.HTMLFormat(`
%s
`, cls, typ, msgContent) +} + func (ut *RenderUtils) RenderUnicodeEscapeToggleButton(escapeStatus *charset.EscapeStatus) template.HTML { if escapeStatus == nil || !escapeStatus.Escaped { return "" diff --git a/routers/utils/utils.go b/routers/utils/utils.go index 3035073d5c..47ca13b2aa 100644 --- a/routers/utils/utils.go +++ b/routers/utils/utils.go @@ -5,10 +5,11 @@ package utils import ( "html" - "strings" + "html/template" ) -// SanitizeFlashErrorString will sanitize a flash error string -func SanitizeFlashErrorString(x string) string { - return strings.ReplaceAll(html.EscapeString(x), "\n", "
") +// EscapeFlashErrorString will escape the flash error string +// Maybe do more sanitization in the future, e.g.: hide sensitive information, etc. +func EscapeFlashErrorString(x string) template.HTML { + return template.HTML(html.EscapeString(x)) } diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go index cc7c888a75..5dfc544777 100644 --- a/routers/utils/utils_test.go +++ b/routers/utils/utils_test.go @@ -4,16 +4,17 @@ package utils import ( + "html/template" "testing" "github.com/stretchr/testify/assert" ) -func TestSanitizeFlashErrorString(t *testing.T) { +func TestEscapeFlashErrorString(t *testing.T) { tests := []struct { name string arg string - want string + want template.HTML }{ { name: "no error", @@ -28,13 +29,13 @@ func TestSanitizeFlashErrorString(t *testing.T) { { name: "line break error", arg: "some error:\n\nawesome!", - want: "some error:

awesome!", + want: "some error:\n\nawesome!", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := SanitizeFlashErrorString(tt.arg) + got := EscapeFlashErrorString(tt.arg) assert.Equal(t, tt.want, got) }) } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index c103d20064..8645aedbde 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -79,7 +79,7 @@ func SignInOAuthCallback(ctx *context.Context) { } } sort.Strings(errorKeyValues) - ctx.Flash.Error(strings.Join(errorKeyValues, "
"), true) + ctx.Flash.Error(strings.Join(errorKeyValues, "\n"), true) } // first look if the provider is still active diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 5cfe08c7fe..8bc5947df8 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -45,8 +45,8 @@ func List(ctx *context.Context) { func FetchActionTest(ctx *context.Context) { _ = ctx.Req.ParseForm() - ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "
" + - "Form: " + ctx.Req.Form.Encode() + "
" + + ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" + + "Form: " + ctx.Req.Form.Encode() + "\n" + "PostForm: " + ctx.Req.PostForm.Encode(), ) time.Sleep(2 * time.Second) @@ -192,11 +192,31 @@ func prepareMockData(ctx *context.Context) { prepareMockDataBadgeActionsSvg(ctx) case "/devtest/relative-time": prepareMockDataRelativeTime(ctx) + case "/devtest/toast-and-message": + prepareMockDataToastAndMessage(ctx) case "/devtest/unicode-escape": prepareMockDataUnicodeEscape(ctx) } } +func prepareMockDataToastAndMessage(ctx *context.Context) { + msgWithDetails, _ := ctx.RenderToHTML("base/alert_details", map[string]any{ + "Message": "message with details ", + "Summary": "summary with details", + "Details": "details line 1\n details line 2\n details line 3", + }) + msgWithSummary, _ := ctx.RenderToHTML("base/alert_details", map[string]any{ + "Message": "message with summary ", + "Summary": "summary only", + }) + + ctx.Flash.ErrorMsg = string(msgWithDetails) + ctx.Flash.WarningMsg = string(msgWithSummary) + ctx.Flash.InfoMsg = "a long message with line break\nthe second line " + ctx.Flash.SuccessMsg = "single line message " + ctx.Data["Flash"] = ctx.Flash +} + func prepareMockDataUnicodeEscape(ctx *context.Context) { content := "// demo code\n" content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n" @@ -223,8 +243,8 @@ func TmplCommon(ctx *context.Context) { prepareMockData(ctx) if ctx.Req.Method == http.MethodPost { _ = ctx.Req.ParseForm() - ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"
"+ - "Form: "+ctx.Req.Form.Encode()+"
"+ + ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+ + "Form: "+ctx.Req.Form.Encode()+"\n"+ "PostForm: "+ctx.Req.PostForm.Encode(), true, ) diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index a5c379e01a..5d208bb286 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -15,6 +15,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" @@ -237,7 +238,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio } } if len(content) == 0 { - content = templates.SanitizeHTML(desc) + content = markup.Sanitize(desc) } items = append(items, &feeds.Item{ diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 5e5cfec5c2..c566e465e9 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -231,7 +231,7 @@ func CreateBranch(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.push_rejected"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(e.Message), + "Details": utils.EscapeFlashErrorString(e.Message), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 168d959494..34e588b141 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -410,7 +410,8 @@ func Diff(ctx *context.Context) { ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))}) - ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, htmlMessage) if err != nil { ctx.ServerError("PostProcessCommitMessage", err) return diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go index e1473a34b3..f23b2738e5 100644 --- a/routers/web/repo/editor_error.go +++ b/routers/web/repo/editor_error.go @@ -27,13 +27,13 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": message, "Summary": summary, - "Details": utils.SanitizeFlashErrorString(details), + "Details": utils.EscapeFlashErrorString(details), }) if err == nil { ctx.JSONError(flashError) } else { - log.Error("RenderToHTML: %v", err) - ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) + log.Error("RenderToHTML(%q, %q, %q), error: %v", message, summary, details, err) + ctx.JSONError("Unable to render error details, see server logs") // it should never happen } } diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 98fb842ddf..592d902ba8 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -170,7 +170,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) templat flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"), "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)), - "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")), + "Details": utils.EscapeFlashErrorString(strings.Join(lines, "\n")), }) if err != nil { log.Debug("render flash error: %v", err) diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 250a54fc24..f678f83878 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -29,7 +29,6 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" @@ -781,14 +780,14 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue } else if comment.Type == issues_model.CommentTypeAddTimeManual || comment.Type == issues_model.CommentTypeStopTracking || comment.Type == issues_model.CommentTypeDeleteTimeManual { - // drop error since times could be pruned from DB.. + // drop error since times could be pruned from DB _ = comment.LoadTime(ctx) if comment.Content != "" { // Content before v1.21 did store the formatted string instead of seconds, // so "|" is used as delimiter to mark the new format if comment.Content[0] != '|' { // handle old time comments that have formatted text stored - comment.RenderedContent = templates.SanitizeHTML(comment.Content) + comment.RenderedContent = markup.Sanitize(comment.Content) comment.Content = "" } else { // else it's just a duration in seconds to pass on to the frontend diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index e312fc9d2a..efcdaac674 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1042,7 +1042,7 @@ func UpdatePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.merge_conflict"), "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) @@ -1054,9 +1054,9 @@ func UpdatePullRequest(ctx *context.Context) { } else if pull_service.IsErrRebaseConflicts(err) { conflictError := err.(pull_service.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), + "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.EscapeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) @@ -1191,7 +1191,7 @@ func MergePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.merge_conflict"), "Summary": ctx.Tr("repo.editor.merge_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1202,9 +1202,9 @@ func MergePullRequest(ctx *context.Context) { } else if pull_service.IsErrRebaseConflicts(err) { conflictError := err.(pull_service.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), + "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.EscapeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1234,7 +1234,7 @@ func MergePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + "Details": utils.EscapeFlashErrorString(pushrejErr.Message), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1454,7 +1454,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + "Details": utils.EscapeFlashErrorString(pushrejErr.Message), }) if err != nil { ctx.ServerError("CompareAndPullRequest.HTMLString", err) diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl index 5ebe191771..242f6278ea 100644 --- a/templates/base/alert.tmpl +++ b/templates/base/alert.tmpl @@ -1,25 +1,9 @@ -{{- if .Flash.ErrorMsg -}} -
-

{{.Flash.ErrorMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.SuccessMsg -}} -
-

{{.Flash.SuccessMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.InfoMsg -}} -
-

{{.Flash.InfoMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.WarningMsg -}} -
-

{{.Flash.WarningMsg | SanitizeHTML}}

-
-{{- end -}} +{{- if .Flash.ErrorMsg}}{{ctx.RenderUtils.RenderFlashMessage "error" .Flash.ErrorMsg}}{{end -}} +{{- if .Flash.WarningMsg}}{{ctx.RenderUtils.RenderFlashMessage "warning" .Flash.WarningMsg}}{{end -}} +{{- if .Flash.InfoMsg}}{{ctx.RenderUtils.RenderFlashMessage "info" .Flash.InfoMsg}}{{end -}} +{{- if .Flash.SuccessMsg}}{{ctx.RenderUtils.RenderFlashMessage "success" .Flash.SuccessMsg}}{{end -}} {{- if .ShowTwoFactorRequiredMessage -}} -
-

{{ctx.Locale.Tr "auth.twofa_required"}}

+ {{- end -}} diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 6380a72498..da8aba452a 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -2,10 +2,8 @@ {{if .Details}}
{{.Summary}} - {{.Details | SanitizeHTML}} +
{{.Details}}
{{else}} -
- {{.Summary}} -
+
{{.Summary}}
{{end}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 628e4388a0..c9d7b3047f 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,4 +1,4 @@ {{template "base/head" ctx.RootData}}
-{{template "base/alert" .}} +
{{template "base/alert" ctx.RootData}}
diff --git a/templates/devtest/fetch-action.tmpl b/templates/devtest/fetch-action.tmpl index 4ee824f04b..cd4da52aac 100644 --- a/templates/devtest/fetch-action.tmpl +++ b/templates/devtest/fetch-action.tmpl @@ -1,6 +1,5 @@ {{template "devtest/devtest-header"}}
- {{template "base/alert" .}}

link-action

@@ -17,29 +16,20 @@

form-fetch-action

Use "window.fetch" to send a form request to backend
-
-
+
+ -
+
-
+
bad action url
- {{template "devtest/devtest-footer"}} diff --git a/templates/devtest/toast.tmpl b/templates/devtest/toast-and-message.tmpl similarity index 96% rename from templates/devtest/toast.tmpl rename to templates/devtest/toast-and-message.tmpl index 597b415469..c4056b6fc6 100644 --- a/templates/devtest/toast.tmpl +++ b/templates/devtest/toast-and-message.tmpl @@ -1,5 +1,5 @@ {{template "devtest/devtest-header"}} -
+

Toast

diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 179f6018d3..8451a6f3cf 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -195,7 +195,7 @@ {{DateUtils.TimeSince .NoteCommit.Author.When}}
-
{{.NoteRendered | SanitizeHTML}}
+
{{.NoteRendered}}
{{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 8c08f1c1f1..6a67ef754d 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -164,7 +164,7 @@
{{svg "octicon-git-commit"}} {{/* the content is a link like message title (from CreateRefComment) */}} - {{.Content | SanitizeHTML}} + {{.GetSanitizedContentHTML}}
{{else if eq .Type 7}} diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 53709e6ff4..d0213962ee 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -317,9 +317,8 @@ func TestPullCleanUpAfterMerge(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - resultMsg := htmlDoc.doc.Find(".ui.message>p").Text() - - assert.Equal(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg) + resultMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.message.flash-message").Text()) + assert.Equal(t, `Branch "user1/repo1:feature/test" has been deleted.`, resultMsg) }) } diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index ff35baae9d..4000c7ebe1 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -36,8 +36,7 @@ func testLoginFailed(t *testing.T, username, password, message string) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - resultMsg := htmlDoc.doc.Find(".ui.message>p").Text() - + resultMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.message.flash-message").Text()) assert.Equal(t, message, resultMsg) } diff --git a/tests/integration/user_settings_test.go b/tests/integration/user_settings_test.go index 20c758dc85..b3527dd467 100644 --- a/tests/integration/user_settings_test.go +++ b/tests/integration/user_settings_test.go @@ -5,6 +5,7 @@ package integration import ( "net/http" + "strings" "testing" "code.gitea.io/gitea/modules/container" @@ -309,8 +310,7 @@ func TestUserSettingsApplications(t *testing.T) { }) resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - - msg := doc.Find(".flash-error p").Text() + msg := strings.TrimSpace(doc.Find(".ui.message.flash-message").Text()) assert.Equal(t, `form.RedirectURIs"ftp://127.0.0.1" is not a valid URL.`, msg) }) diff --git a/web_src/css/base.css b/web_src/css/base.css index c278132a00..4448af68e0 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -431,8 +431,9 @@ img.ui.avatar, margin-top: calc(var(--page-spacing) - 1rem); } -.ui .message.flash-message { - text-align: center; +.ui.message.flash-message pre { + white-space: pre-line; + margin: 0; } .ui .header > i + .content { @@ -865,6 +866,13 @@ table th[data-sortt-desc] .svg { align-items: stretch; } +/* can be used to replace "ui relaxed list" or "tw-flex tw-flex-col tw-gap-xxx" when we need more flexible layout */ +.flex-relaxed-list { + display: flex; + flex-direction: column; + gap: var(--gap-block); +} + .ui.list.flex-items-block > .item, .ui.vertical.menu.flex-items-block > .item, .ui.form .field > label.flex-text-block, /* override fomantic "block" style */ diff --git a/web_src/css/devtest.css b/web_src/css/devtest.css index a7b00e1e56..c344d99058 100644 --- a/web_src/css/devtest.css +++ b/web_src/css/devtest.css @@ -1,3 +1,8 @@ +h1, h2 { + margin: 0; + padding: 10px 0; +} + .button-sample-groups { margin: 0; padding: 0; } @@ -10,7 +15,6 @@ margin-bottom: 5px; } -h1, h2 { - margin: 0; - padding: 10px 0; +.fetch-action-demo-forms .form-fetch-action { + border: 1px red dashed; /* show the border for demo purpose */ }