mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-04 17:35:21 +02:00
feat(ui): add links to assigners in issue comments (#8264)
When a user is assigning an issue or PR to another user, an entry is posted in the issue comment list. This PR adds a link to the one assigning the issue. Additionally, tests for preventing XSS vulnerabilities in label rendering were added. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8264 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
commit
8ac572682b
7 changed files with 110 additions and 33 deletions
|
@ -3,6 +3,7 @@
|
|||
repo_id: 1
|
||||
org_id: 0
|
||||
name: label1
|
||||
description: 'First label'
|
||||
color: '#abcdef'
|
||||
exclusive: false
|
||||
num_issues: 2
|
||||
|
@ -107,3 +108,26 @@
|
|||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
archived_unix: 0
|
||||
|
||||
-
|
||||
id: 11
|
||||
repo_id: 3
|
||||
org_id: 0
|
||||
name: " <script>malicious</script> /'?&"
|
||||
description: "Malicious label ' <script>malicious</script>"
|
||||
color: '#000000'
|
||||
exclusive: true
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
archived_unix: 0
|
||||
|
||||
-
|
||||
id: 12
|
||||
repo_id: 3
|
||||
org_id: 0
|
||||
name: 'archived label<>'
|
||||
color: '#000000'
|
||||
exclusive: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
archived_unix: 2991092130
|
||||
|
|
|
@ -188,6 +188,7 @@ func NewFuncMap() template.FuncMap {
|
|||
"RenderMarkdownToHtml": RenderMarkdownToHtml,
|
||||
"RenderLabel": RenderLabel,
|
||||
"RenderLabels": RenderLabels,
|
||||
"RenderUser": RenderUser,
|
||||
"RenderReviewRequest": RenderReviewRequest,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"math"
|
||||
"net/url"
|
||||
|
@ -15,6 +16,7 @@ import (
|
|||
"unicode"
|
||||
|
||||
issues_model "forgejo.org/models/issues"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/emoji"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/markup"
|
||||
|
@ -26,7 +28,7 @@ import (
|
|||
|
||||
// RenderCommitMessage renders commit message with XSS-safe and special links.
|
||||
func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML {
|
||||
cleanMsg := template.HTMLEscapeString(msg)
|
||||
cleanMsg := html.EscapeString(msg)
|
||||
// we can safely assume that it will not return any error, since there
|
||||
// shouldn't be any special HTML.
|
||||
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
|
@ -63,7 +65,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string,
|
|||
Ctx: ctx,
|
||||
DefaultLink: urlDefault,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(msgLine))
|
||||
}, html.EscapeString(msgLine))
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessageSubject: %v", err)
|
||||
return template.HTML("")
|
||||
|
@ -88,7 +90,7 @@ func RenderCommitBody(ctx context.Context, msg string, metas map[string]string)
|
|||
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(msgLine))
|
||||
}, html.EscapeString(msgLine))
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessage: %v", err)
|
||||
return ""
|
||||
|
@ -122,7 +124,7 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
|
|||
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(text))
|
||||
}, html.EscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderIssueTitle: %v", err)
|
||||
return template.HTML("")
|
||||
|
@ -132,7 +134,7 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
|
|||
|
||||
// RenderRefIssueTitle renders referenced issue/pull title with defined post processors
|
||||
func RenderRefIssueTitle(ctx context.Context, text string) template.HTML {
|
||||
renderedText, err := markup.RenderRefIssueTitle(&markup.RenderContext{Ctx: ctx}, template.HTMLEscapeString(text))
|
||||
renderedText, err := markup.RenderRefIssueTitle(&markup.RenderContext{Ctx: ctx}, html.EscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderRefIssueTitle: %v", err)
|
||||
return ""
|
||||
|
@ -150,7 +152,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
|
|||
labelScope = label.ExclusiveScope()
|
||||
)
|
||||
|
||||
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
|
||||
description := emoji.ReplaceAliases(html.EscapeString(label.Description))
|
||||
|
||||
if label.IsArchived() {
|
||||
archivedCSSClass = "archived-label"
|
||||
|
@ -212,7 +214,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
|
|||
// RenderEmoji renders html text with emoji post processors
|
||||
func RenderEmoji(ctx context.Context, text string) template.HTML {
|
||||
renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
|
||||
template.HTMLEscapeString(text))
|
||||
html.EscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderEmoji: %v", err)
|
||||
return template.HTML("")
|
||||
|
@ -263,10 +265,20 @@ func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issu
|
|||
return template.HTML(htmlCode)
|
||||
}
|
||||
|
||||
func RenderUser(ctx context.Context, user user_model.User) template.HTML {
|
||||
if user.ID > 0 {
|
||||
return template.HTML(fmt.Sprintf(
|
||||
"<a href='%s' rel='nofollow'><strong>%s</strong></a>",
|
||||
user.HomeLink(), html.EscapeString(user.GetDisplayName())))
|
||||
}
|
||||
return template.HTML(fmt.Sprintf("<strong>%s</strong>",
|
||||
html.EscapeString(user.GetDisplayName())))
|
||||
}
|
||||
|
||||
func RenderReviewRequest(users []issues_model.RequestReviewTarget) template.HTML {
|
||||
usernames := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
usernames = append(usernames, template.HTMLEscapeString(user.Name()))
|
||||
usernames = append(usernames, html.EscapeString(user.Name()))
|
||||
}
|
||||
|
||||
htmlCode := `<span class="review-request-list">`
|
||||
|
|
|
@ -11,6 +11,9 @@ import (
|
|||
"forgejo.org/models/db"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/modules/translation"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -215,9 +218,51 @@ func TestRenderLabels(t *testing.T) {
|
|||
|
||||
tr := &translation.MockLocale{}
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
|
||||
labelScoped := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})
|
||||
labelMalicious := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 11})
|
||||
labelArchived := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 12})
|
||||
|
||||
assert.Contains(t, RenderLabels(db.DefaultContext, tr, []*issues_model.Label{label}, "user2/repo1", false),
|
||||
"user2/repo1/issues?labels=1")
|
||||
assert.Contains(t, RenderLabels(db.DefaultContext, tr, []*issues_model.Label{label}, "user2/repo1", true),
|
||||
"user2/repo1/pulls?labels=1")
|
||||
rendered := RenderLabels(db.DefaultContext, tr, []*issues_model.Label{label}, "user2/repo1", false)
|
||||
assert.Contains(t, rendered, "user2/repo1/issues?labels=1")
|
||||
assert.Contains(t, rendered, ">label1<")
|
||||
assert.Contains(t, rendered, "title='First label'")
|
||||
rendered = RenderLabels(db.DefaultContext, tr, []*issues_model.Label{label}, "user2/repo1", true)
|
||||
assert.Contains(t, rendered, "user2/repo1/pulls?labels=1")
|
||||
assert.Contains(t, rendered, ">label1<")
|
||||
rendered = RenderLabels(db.DefaultContext, tr, []*issues_model.Label{labelScoped}, "user2/repo1", false)
|
||||
assert.Contains(t, rendered, "user2/repo1/issues?labels=7")
|
||||
assert.Contains(t, rendered, ">scope<")
|
||||
assert.Contains(t, rendered, ">label1<")
|
||||
rendered = RenderLabels(db.DefaultContext, tr, []*issues_model.Label{labelMalicious}, "user2/repo1", false)
|
||||
assert.Contains(t, rendered, "user2/repo1/issues?labels=11")
|
||||
assert.Contains(t, rendered, "> <script>malicious</script> <")
|
||||
assert.Contains(t, rendered, ">'?&<")
|
||||
assert.Contains(t, rendered, "title='Malicious label ' <script>malicious</script>'")
|
||||
rendered = RenderLabels(db.DefaultContext, tr, []*issues_model.Label{labelArchived}, "user2/repo1", false)
|
||||
assert.Contains(t, rendered, "user2/repo1/issues?labels=12")
|
||||
assert.Contains(t, rendered, ">archived label<><")
|
||||
assert.Contains(t, rendered, "title='repo.issues.archived_label_description'")
|
||||
}
|
||||
|
||||
func TestRenderUser(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
ghost := user_model.NewGhostUser()
|
||||
|
||||
assert.Contains(t, RenderUser(db.DefaultContext, *user),
|
||||
"<a href='/user2' rel='nofollow'><strong>user2</strong></a>")
|
||||
assert.Contains(t, RenderUser(db.DefaultContext, *org),
|
||||
"<a href='/org3' rel='nofollow'><strong>org3</strong></a>")
|
||||
assert.Contains(t, RenderUser(db.DefaultContext, *ghost),
|
||||
"<strong>Ghost</strong>")
|
||||
|
||||
defer test.MockVariableValue(&setting.UI.DefaultShowFullName, true)()
|
||||
assert.Contains(t, RenderUser(db.DefaultContext, *user),
|
||||
"<a href='/user2' rel='nofollow'><strong>< U<se>r Tw<o > ><</strong></a>")
|
||||
assert.Contains(t, RenderUser(db.DefaultContext, *org),
|
||||
"<a href='/org3' rel='nofollow'><strong><<<< >> >> > >> > >>> >></strong></a>")
|
||||
assert.Contains(t, RenderUser(db.DefaultContext, *ghost),
|
||||
"<strong>Ghost</strong>")
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ func TestLabel_ToLabel(t *testing.T) {
|
|||
ID: label.ID,
|
||||
Name: label.Name,
|
||||
Color: "abcdef",
|
||||
Description: label.Description,
|
||||
URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID),
|
||||
}, ToLabel(label, repo, nil))
|
||||
}
|
||||
|
|
|
@ -221,27 +221,23 @@
|
|||
{{else if and (eq .Type 9) (gt .AssigneeID 0)}}
|
||||
<div class="timeline-item event" id="{{.HashTag}}">
|
||||
<span class="badge">{{svg "octicon-person"}}</span>
|
||||
{{if .RemovedAssignee}}
|
||||
{{template "shared/user/avatarlink" dict "user" .Assignee}}
|
||||
<span class="text grey muted-links">
|
||||
{{template "shared/user/authorlink" .Assignee}}
|
||||
{{if .RemovedAssignee}}
|
||||
{{if eq .Poster.ID .Assignee.ID}}
|
||||
{{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.issues.remove_assignee_at" .Poster.GetDisplayName $createdStr}}
|
||||
{{ctx.Locale.Tr "repo.issues.remove_assignee_at" (RenderUser $.Context .Poster) $createdStr}}
|
||||
{{end}}
|
||||
</span>
|
||||
{{else}}
|
||||
{{template "shared/user/avatarlink" dict "user" .Assignee}}
|
||||
<span class="text grey muted-links">
|
||||
{{template "shared/user/authorlink" .Assignee}}
|
||||
{{if eq .Poster.ID .AssigneeID}}
|
||||
{{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.issues.add_assignee_at" .Poster.GetDisplayName $createdStr}}
|
||||
{{ctx.Locale.Tr "repo.issues.add_assignee_at" (RenderUser $.Context .Poster) $createdStr}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if eq .Type 10}}
|
||||
<div class="timeline-item event" id="{{.HashTag}}">
|
||||
|
|
|
@ -223,15 +223,13 @@ func TestIssueCommentChangeAssignee(t *testing.T) {
|
|||
testIssueCommentChangeEvent(t, htmlDoc, "2041",
|
||||
"octicon-person", "User One", "/user1",
|
||||
[]string{"user1 was unassigned by user2"},
|
||||
[]string{"/user1"})
|
||||
// []string{"/user1", "/user2"})
|
||||
[]string{"/user1", "/user2"})
|
||||
|
||||
// Add other
|
||||
testIssueCommentChangeEvent(t, htmlDoc, "2042",
|
||||
"octicon-person", "< U<se>r Tw<o > ><", "/user2",
|
||||
[]string{"user2 was assigned by user1"},
|
||||
[]string{"/user2"})
|
||||
// []string{"/user2", "/user1"})
|
||||
[]string{"/user2", "/user1"})
|
||||
|
||||
// Self-remove
|
||||
testIssueCommentChangeEvent(t, htmlDoc, "2043",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue