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:
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
This commit is contained in:
wxiaoguang
2026-04-12 10:17:25 +08:00
committed by GitHub
parent ba9258c478
commit 8fcbdf05b0
29 changed files with 159 additions and 113 deletions

View File

@@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
@@ -543,6 +544,12 @@ func (c *Comment) EventTag() string {
return fmt.Sprintf("event-%d", c.ID) return fmt.Sprintf("event-%d", c.ID)
} }
func (c *Comment) GetSanitizedContentHTML() template.HTML {
// mainly for type=4 CommentTypeCommitRef
// the content is a link like <a href="{RepoLink}/commit/{CommitID}">message title</a> (from CreateRefComment)
return markup.Sanitize(c.Content)
}
// LoadLabel if comment.Type is CommentTypeLabel, then load Label // LoadLabel if comment.Type is CommentTypeLabel, then load Label
func (c *Comment) LoadLabel(ctx context.Context) error { func (c *Comment) LoadLabel(ctx context.Context) error {
var label Label var label Label

View File

@@ -6,6 +6,7 @@ package markup
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"html/template"
"io" "io"
"regexp" "regexp"
"slices" "slices"
@@ -149,9 +150,9 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e
return postProcess(ctx, procs, input, output) return postProcess(ctx, procs, input, output)
} }
// PostProcessCommitMessage will use the same logic as PostProcess, but will disable // PostProcessCommitMessage will use the same logic as PostProcess, but will disable the shortLinkProcessor.
// 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 string) (string, error) { func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (template.HTML, error) {
procs := []processor{ procs := []processor{
fullIssuePatternProcessor, fullIssuePatternProcessor,
comparePatternProcessor, comparePatternProcessor,
@@ -165,7 +166,8 @@ func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error
emojiProcessor, emojiProcessor,
emojiShortCodeProcessor, emojiShortCodeProcessor,
} }
return postProcessString(ctx, procs, content) s, err := postProcessString(ctx, procs, string(content))
return template.HTML(s), err
} }
var emojiProcessors = []processor{ var emojiProcessors = []processor{

View File

@@ -32,13 +32,12 @@ func newFuncMapWebPage() template.FuncMap {
// ----------------------------------------------------------------- // -----------------------------------------------------------------
// html/template related functions // html/template related functions
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
"Iif": iif, "Iif": iif,
"Eval": evalTokens, "Eval": evalTokens,
"HTMLFormat": htmlFormat, "HTMLFormat": htmlFormat,
"QueryEscape": queryEscape, "QueryEscape": queryEscape,
"QueryBuild": QueryBuild, "QueryBuild": QueryBuild,
"SanitizeHTML": SanitizeHTML,
"PathEscape": url.PathEscape, "PathEscape": url.PathEscape,
"PathEscapeSegments": util.PathEscapeSegments, "PathEscapeSegments": util.PathEscapeSegments,
@@ -146,9 +145,8 @@ func newFuncMapWebPage() template.FuncMap {
} }
} }
// SanitizeHTML sanitizes the input by default sanitization rules. func sanitizeHTML(msg string) template.HTML {
func SanitizeHTML(s string) template.HTML { return markup.Sanitize(msg)
return markup.Sanitize(s)
} }
func htmlFormat(s any, args ...any) template.HTML { func htmlFormat(s any, args ...any) template.HTML {

View File

@@ -58,7 +58,7 @@ func TestSubjectBodySeparator(t *testing.T) {
} }
func TestSanitizeHTML(t *testing.T) { func TestSanitizeHTML(t *testing.T) {
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>`)) 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>`))
} }
func TestTemplateIif(t *testing.T) { func TestTemplateIif(t *testing.T) {

View File

@@ -65,13 +65,16 @@ func mailBodyFuncMap() template.FuncMap {
"NIL": func() any { return nil }, "NIL": func() any { return nil },
// html/template related functions // html/template related functions
"dict": dict, "dict": dict,
"Iif": iif, "Iif": iif,
"Eval": evalTokens, "Eval": evalTokens,
"HTMLFormat": htmlFormat, "HTMLFormat": htmlFormat,
"QueryEscape": queryEscape, "QueryEscape": queryEscape,
"QueryBuild": QueryBuild, "QueryBuild": QueryBuild,
"SanitizeHTML": SanitizeHTML,
// 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, "PathEscape": url.PathEscape,
"PathEscapeSegments": util.PathEscapeSegments, "PathEscapeSegments": util.PathEscapeSegments,

View File

@@ -40,7 +40,7 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils {
// RenderCommitMessage renders commit message with XSS-safe and special links. // RenderCommitMessage renders commit message with XSS-safe and special links.
func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { 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. // 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. // "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) 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) log.Error("PostProcessCommitMessage: %v", err)
return "" return ""
} }
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n")
if len(msgLines) == 0 { if len(msgLines) == 0 {
return "" return ""
} }
@@ -91,12 +91,14 @@ func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) templ
return "" 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 { if err != nil {
log.Error("PostProcessCommitMessage: %v", err) log.Error("PostProcessCommitMessage: %v", err)
return "" return ""
} }
return template.HTML(renderedMessage) return renderedMessage
} }
// Match text that is between back ticks. // Match text that is between back ticks.
@@ -279,6 +281,35 @@ func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize in
return htmlutil.HTMLFormat(`<div class="theme-menu-item" data-tooltip-content="%s">%s %s %s</div>`, info.GetDescription(), icon, info.DisplayName, extraIcon) return htmlutil.HTMLFormat(`<div class="theme-menu-item" data-tooltip-content="%s">%s %s %s</div>`, 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, "</pre>") || strings.Contains(msg, "</details>") || strings.Contains(msg, "</ul>") || strings.Contains(msg, "</div>") {
// 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(`<div class="tw-text-center">%s</div>`, sanitizeHTML(msg))
} else {
// For a multi-line message, preserve line breaks, and left-align it.
msgContent = htmlutil.HTMLFormat(`%s`, sanitizeHTML(strings.ReplaceAll(msg, "\n", "<br>")))
}
return htmlutil.HTMLFormat(`<div class="ui %s message flash-message flash-%s">%s</div>`, cls, typ, msgContent)
}
func (ut *RenderUtils) RenderUnicodeEscapeToggleButton(escapeStatus *charset.EscapeStatus) template.HTML { func (ut *RenderUtils) RenderUnicodeEscapeToggleButton(escapeStatus *charset.EscapeStatus) template.HTML {
if escapeStatus == nil || !escapeStatus.Escaped { if escapeStatus == nil || !escapeStatus.Escaped {
return "" return ""

View File

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

View File

@@ -4,16 +4,17 @@
package utils package utils
import ( import (
"html/template"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestSanitizeFlashErrorString(t *testing.T) { func TestEscapeFlashErrorString(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
arg string arg string
want string want template.HTML
}{ }{
{ {
name: "no error", name: "no error",
@@ -28,13 +29,13 @@ func TestSanitizeFlashErrorString(t *testing.T) {
{ {
name: "line break error", name: "line break error",
arg: "some error:\n\nawesome!", arg: "some error:\n\nawesome!",
want: "some error:<br><br>awesome!", want: "some error:\n\nawesome!",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := SanitizeFlashErrorString(tt.arg) got := EscapeFlashErrorString(tt.arg)
assert.Equal(t, tt.want, got) assert.Equal(t, tt.want, got)
}) })
} }

View File

@@ -79,7 +79,7 @@ func SignInOAuthCallback(ctx *context.Context) {
} }
} }
sort.Strings(errorKeyValues) sort.Strings(errorKeyValues)
ctx.Flash.Error(strings.Join(errorKeyValues, "<br>"), true) ctx.Flash.Error(strings.Join(errorKeyValues, "\n"), true)
} }
// first look if the provider is still active // first look if the provider is still active

View File

@@ -45,8 +45,8 @@ func List(ctx *context.Context) {
func FetchActionTest(ctx *context.Context) { func FetchActionTest(ctx *context.Context) {
_ = ctx.Req.ParseForm() _ = ctx.Req.ParseForm()
ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "<br>" + ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" +
"Form: " + ctx.Req.Form.Encode() + "<br>" + "Form: " + ctx.Req.Form.Encode() + "\n" +
"PostForm: " + ctx.Req.PostForm.Encode(), "PostForm: " + ctx.Req.PostForm.Encode(),
) )
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
@@ -192,11 +192,31 @@ func prepareMockData(ctx *context.Context) {
prepareMockDataBadgeActionsSvg(ctx) prepareMockDataBadgeActionsSvg(ctx)
case "/devtest/relative-time": case "/devtest/relative-time":
prepareMockDataRelativeTime(ctx) prepareMockDataRelativeTime(ctx)
case "/devtest/toast-and-message":
prepareMockDataToastAndMessage(ctx)
case "/devtest/unicode-escape": case "/devtest/unicode-escape":
prepareMockDataUnicodeEscape(ctx) prepareMockDataUnicodeEscape(ctx)
} }
} }
func prepareMockDataToastAndMessage(ctx *context.Context) {
msgWithDetails, _ := ctx.RenderToHTML("base/alert_details", map[string]any{
"Message": "message with details <script>escape xss</script>",
"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 <script>escape xss</script>",
"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 <script>removed xss</script>"
ctx.Flash.SuccessMsg = "single line message <script>removed xss</script>"
ctx.Data["Flash"] = ctx.Flash
}
func prepareMockDataUnicodeEscape(ctx *context.Context) { func prepareMockDataUnicodeEscape(ctx *context.Context) {
content := "// demo code\n" content := "// demo code\n"
content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\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) prepareMockData(ctx)
if ctx.Req.Method == http.MethodPost { if ctx.Req.Method == http.MethodPost {
_ = ctx.Req.ParseForm() _ = ctx.Req.ParseForm()
ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"<br>"+ ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+
"Form: "+ctx.Req.Form.Encode()+"<br>"+ "Form: "+ctx.Req.Form.Encode()+"\n"+
"PostForm: "+ctx.Req.PostForm.Encode(), "PostForm: "+ctx.Req.PostForm.Encode(),
true, true,
) )

View File

@@ -15,6 +15,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/renderhelper" "code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo" 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/markup/markdown"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
@@ -237,7 +238,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
} }
} }
if len(content) == 0 { if len(content) == 0 {
content = templates.SanitizeHTML(desc) content = markup.Sanitize(desc)
} }
items = append(items, &feeds.Item{ items = append(items, &feeds.Item{

View File

@@ -231,7 +231,7 @@ func CreateBranch(ctx *context.Context) {
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"), "Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(e.Message), "Details": utils.EscapeFlashErrorString(e.Message),
}) })
if err != nil { if err != nil {
ctx.ServerError("UpdatePullRequest.HTMLString", err) ctx.ServerError("UpdatePullRequest.HTMLString", err)

View File

@@ -410,7 +410,8 @@ func Diff(ctx *context.Context) {
ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, 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))}) 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 { if err != nil {
ctx.ServerError("PostProcessCommitMessage", err) ctx.ServerError("PostProcessCommitMessage", err)
return return

View File

@@ -27,13 +27,13 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message,
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": message, "Message": message,
"Summary": summary, "Summary": summary,
"Details": utils.SanitizeFlashErrorString(details), "Details": utils.EscapeFlashErrorString(details),
}) })
if err == nil { if err == nil {
ctx.JSONError(flashError) ctx.JSONError(flashError)
} else { } else {
log.Error("RenderToHTML: %v", err) log.Error("RenderToHTML(%q, %q, %q), error: %v", message, summary, details, err)
ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) ctx.JSONError("Unable to render error details, see server logs") // it should never happen
} }
} }

View File

@@ -170,7 +170,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) templat
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"), "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
"Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)), "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 { if err != nil {
log.Debug("render flash error: %v", err) log.Debug("render flash error: %v", err)

View File

@@ -29,7 +29,6 @@ import (
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/templates/vars"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@@ -781,14 +780,14 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
} else if comment.Type == issues_model.CommentTypeAddTimeManual || } else if comment.Type == issues_model.CommentTypeAddTimeManual ||
comment.Type == issues_model.CommentTypeStopTracking || comment.Type == issues_model.CommentTypeStopTracking ||
comment.Type == issues_model.CommentTypeDeleteTimeManual { 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) _ = comment.LoadTime(ctx)
if comment.Content != "" { if comment.Content != "" {
// Content before v1.21 did store the formatted string instead of seconds, // Content before v1.21 did store the formatted string instead of seconds,
// so "|" is used as delimiter to mark the new format // so "|" is used as delimiter to mark the new format
if comment.Content[0] != '|' { if comment.Content[0] != '|' {
// handle old time comments that have formatted text stored // handle old time comments that have formatted text stored
comment.RenderedContent = templates.SanitizeHTML(comment.Content) comment.RenderedContent = markup.Sanitize(comment.Content)
comment.Content = "" comment.Content = ""
} else { } else {
// else it's just a duration in seconds to pass on to the frontend // else it's just a duration in seconds to pass on to the frontend

View File

@@ -1042,7 +1042,7 @@ func UpdatePullRequest(ctx *context.Context) {
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.merge_conflict"), "Message": ctx.Tr("repo.pulls.merge_conflict"),
"Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut),
}) })
if err != nil { if err != nil {
ctx.ServerError("UpdatePullRequest.HTMLString", err) ctx.ServerError("UpdatePullRequest.HTMLString", err)
@@ -1054,9 +1054,9 @@ func UpdatePullRequest(ctx *context.Context) {
} else if pull_service.IsErrRebaseConflicts(err) { } else if pull_service.IsErrRebaseConflicts(err) {
conflictError := err.(pull_service.ErrRebaseConflicts) conflictError := err.(pull_service.ErrRebaseConflicts)
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ 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"), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut),
}) })
if err != nil { if err != nil {
ctx.ServerError("UpdatePullRequest.HTMLString", err) ctx.ServerError("UpdatePullRequest.HTMLString", err)
@@ -1191,7 +1191,7 @@ func MergePullRequest(ctx *context.Context) {
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.merge_conflict"), "Message": ctx.Tr("repo.editor.merge_conflict"),
"Summary": ctx.Tr("repo.editor.merge_conflict_summary"), "Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut),
}) })
if err != nil { if err != nil {
ctx.ServerError("MergePullRequest.HTMLString", err) ctx.ServerError("MergePullRequest.HTMLString", err)
@@ -1202,9 +1202,9 @@ func MergePullRequest(ctx *context.Context) {
} else if pull_service.IsErrRebaseConflicts(err) { } else if pull_service.IsErrRebaseConflicts(err) {
conflictError := err.(pull_service.ErrRebaseConflicts) conflictError := err.(pull_service.ErrRebaseConflicts)
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ 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"), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut),
}) })
if err != nil { if err != nil {
ctx.ServerError("MergePullRequest.HTMLString", err) ctx.ServerError("MergePullRequest.HTMLString", err)
@@ -1234,7 +1234,7 @@ func MergePullRequest(ctx *context.Context) {
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"), "Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.push_rejected_summary"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(pushrejErr.Message), "Details": utils.EscapeFlashErrorString(pushrejErr.Message),
}) })
if err != nil { if err != nil {
ctx.ServerError("MergePullRequest.HTMLString", err) ctx.ServerError("MergePullRequest.HTMLString", err)
@@ -1454,7 +1454,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"), "Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.push_rejected_summary"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(pushrejErr.Message), "Details": utils.EscapeFlashErrorString(pushrejErr.Message),
}) })
if err != nil { if err != nil {
ctx.ServerError("CompareAndPullRequest.HTMLString", err) ctx.ServerError("CompareAndPullRequest.HTMLString", err)

View File

@@ -1,25 +1,9 @@
{{- if .Flash.ErrorMsg -}} {{- if .Flash.ErrorMsg}}{{ctx.RenderUtils.RenderFlashMessage "error" .Flash.ErrorMsg}}{{end -}}
<div class="ui negative message flash-message flash-error"> {{- if .Flash.WarningMsg}}{{ctx.RenderUtils.RenderFlashMessage "warning" .Flash.WarningMsg}}{{end -}}
<p>{{.Flash.ErrorMsg | SanitizeHTML}}</p> {{- if .Flash.InfoMsg}}{{ctx.RenderUtils.RenderFlashMessage "info" .Flash.InfoMsg}}{{end -}}
</div> {{- if .Flash.SuccessMsg}}{{ctx.RenderUtils.RenderFlashMessage "success" .Flash.SuccessMsg}}{{end -}}
{{- end -}}
{{- if .Flash.SuccessMsg -}}
<div class="ui positive message flash-message flash-success">
<p>{{.Flash.SuccessMsg | SanitizeHTML}}</p>
</div>
{{- end -}}
{{- if .Flash.InfoMsg -}}
<div class="ui info message flash-message flash-info">
<p>{{.Flash.InfoMsg | SanitizeHTML}}</p>
</div>
{{- end -}}
{{- if .Flash.WarningMsg -}}
<div class="ui warning message flash-message flash-warning">
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
</div>
{{- end -}}
{{- if .ShowTwoFactorRequiredMessage -}} {{- if .ShowTwoFactorRequiredMessage -}}
<div class="ui negative message flash-message flash-error"> <div class="ui error message flash-message flash-error">
<p><a href="{{AppSubUrl}}/user/settings/security/two_factor/enroll">{{ctx.Locale.Tr "auth.twofa_required"}}</a></p> <a href="{{AppSubUrl}}/user/settings/security/two_factor/enroll">{{ctx.Locale.Tr "auth.twofa_required"}}</a>
</div> </div>
{{- end -}} {{- end -}}

View File

@@ -2,10 +2,8 @@
{{if .Details}} {{if .Details}}
<details> <details>
<summary>{{.Summary}}</summary> <summary>{{.Summary}}</summary>
{{.Details | SanitizeHTML}} <pre>{{.Details}}</pre>
</details> </details>
{{else}} {{else}}
<div> <div>{{.Summary}}</div>
{{.Summary}}
</div>
{{end}} {{end}}

View File

@@ -1,4 +1,4 @@
{{template "base/head" ctx.RootData}} {{template "base/head" ctx.RootData}}
<link rel="stylesheet" href="{{AssetURI "css/devtest.css"}}"> <link rel="stylesheet" href="{{AssetURI "css/devtest.css"}}">
<div class="tw-hidden" data-global-init="initDevtestPage"></div> <div class="tw-hidden" data-global-init="initDevtestPage"></div>
{{template "base/alert" .}} <div class="ui container tw-mt-4">{{template "base/alert" ctx.RootData}}</div>

View File

@@ -1,6 +1,5 @@
{{template "devtest/devtest-header"}} {{template "devtest/devtest-header"}}
<div class="page-content devtest ui container"> <div class="page-content devtest ui container">
{{template "base/alert" .}}
<div> <div>
<h1>link-action</h1> <h1>link-action</h1>
<div> <div>
@@ -17,29 +16,20 @@
<div> <div>
<h1>form-fetch-action</h1> <h1>form-fetch-action</h1>
<div>Use "window.fetch" to send a form request to backend</div> <div>Use "window.fetch" to send a form request to backend</div>
<div> <div class="flex-relaxed-list fetch-action-demo-forms">
<form method="get" action="fetch-action-test?k=1" class="form-fetch-action"> <form method="get" action="./fetch-action-test?k=1" class="form-fetch-action">
<button name="btn">submit get</button> <button name="btn">submit get</button>
</form> </form>
<form method="post" action="fetch-action-test?k=1" class="form-fetch-action"> <form method="post" action="./fetch-action-test?k=1" class="form-fetch-action">
<div><textarea name="text" rows="3"></textarea></div> <div><textarea name="text" rows="3"></textarea></div>
<div><label><input name="check" type="checkbox"> check</label></div> <div><label><input name="check" type="checkbox"> check</label></div>
<div><button name="btn">submit post</button></div> <div><button name="btn">submit post</button></div>
</form> </form>
<form method="post" action="no-such-uri" class="form-fetch-action"> <form method="post" action="./no-such-uri" class="form-fetch-action">
<div class="tw-py-8">bad action url</div> <div class="tw-py-8">bad action url</div>
<div><button name="btn">submit test</button></div> <div><button name="btn">submit test</button></div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<style>
.ui.message.flash-message {
text-align: left;
}
.form-fetch-action {
margin-bottom: 1em;
border: 1px red dashed; /* show the border for demo purpose */
}
</style>
{{template "devtest/devtest-footer"}} {{template "devtest/devtest-footer"}}

View File

@@ -1,5 +1,5 @@
{{template "devtest/devtest-header"}} {{template "devtest/devtest-header"}}
<div> <div class="ui container">
<h1>Toast</h1> <h1>Toast</h1>
<div> <div>
<button class="ui button toast-test-button" data-toast-level="info" data-toast-message="test info">Show Info Toast</button> <button class="ui button toast-test-button" data-toast-level="info" data-toast-message="test info">Show Info Toast</button>

View File

@@ -195,7 +195,7 @@
<span class="tw-text-text-light">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span> <span class="tw-text-text-light">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
</div> </div>
<div class="ui bottom attached info segment git-notes"> <div class="ui bottom attached info segment git-notes">
<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre> <pre class="commit-body">{{.NoteRendered}}</pre>
</div> </div>
{{end}} {{end}}

View File

@@ -164,7 +164,7 @@
<div class="detail flex-text-block"> <div class="detail flex-text-block">
{{svg "octicon-git-commit"}} {{svg "octicon-git-commit"}}
{{/* the content is a link like <a href="{RepoLink}/commit/{CommitID}">message title</a> (from CreateRefComment) */}} {{/* the content is a link like <a href="{RepoLink}/commit/{CommitID}">message title</a> (from CreateRefComment) */}}
<span class="comment-text-line">{{.Content | SanitizeHTML}}</span> <span class="comment-text-line">{{.GetSanitizedContentHTML}}</span>
</div> </div>
</div> </div>
{{else if eq .Type 7}} {{else if eq .Type 7}}

View File

@@ -317,9 +317,8 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) 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, `Branch "user1/repo1:feature/test" has been deleted.`, resultMsg)
assert.Equal(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg)
}) })
} }

View File

@@ -36,8 +36,7 @@ func testLoginFailed(t *testing.T, username, password, message string) {
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) 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) assert.Equal(t, message, resultMsg)
} }

View File

@@ -5,6 +5,7 @@ package integration
import ( import (
"net/http" "net/http"
"strings"
"testing" "testing"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
@@ -309,8 +310,7 @@ func TestUserSettingsApplications(t *testing.T) {
}) })
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body) doc := NewHTMLParser(t, resp.Body)
msg := strings.TrimSpace(doc.Find(".ui.message.flash-message").Text())
msg := doc.Find(".flash-error p").Text()
assert.Equal(t, `form.RedirectURIs"ftp://127.0.0.1" is not a valid URL.`, msg) assert.Equal(t, `form.RedirectURIs"ftp://127.0.0.1" is not a valid URL.`, msg)
}) })

View File

@@ -431,8 +431,9 @@ img.ui.avatar,
margin-top: calc(var(--page-spacing) - 1rem); margin-top: calc(var(--page-spacing) - 1rem);
} }
.ui .message.flash-message { .ui.message.flash-message pre {
text-align: center; white-space: pre-line;
margin: 0;
} }
.ui .header > i + .content { .ui .header > i + .content {
@@ -865,6 +866,13 @@ table th[data-sortt-desc] .svg {
align-items: stretch; 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.list.flex-items-block > .item,
.ui.vertical.menu.flex-items-block > .item, .ui.vertical.menu.flex-items-block > .item,
.ui.form .field > label.flex-text-block, /* override fomantic "block" style */ .ui.form .field > label.flex-text-block, /* override fomantic "block" style */

View File

@@ -1,3 +1,8 @@
h1, h2 {
margin: 0;
padding: 10px 0;
}
.button-sample-groups { .button-sample-groups {
margin: 0; padding: 0; margin: 0; padding: 0;
} }
@@ -10,7 +15,6 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
h1, h2 { .fetch-action-demo-forms .form-fetch-action {
margin: 0; border: 1px red dashed; /* show the border for demo purpose */
padding: 10px 0;
} }