aboutsummaryrefslogtreecommitdiffstats
path: root/services/issue
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2024-03-04 09:16:03 +0100
committerGitHub <noreply@github.com>2024-03-04 08:16:03 +0000
commitc337ff0ec70618ef2ead7850f90ab2a8458db192 (patch)
treecf4618cf7dc258018d5f9ec827b0fda4a9ebd196 /services/issue
parent8e12ba34bab7e728ac93ccfaecbe91e053ef1c89 (diff)
downloadgitea-c337ff0ec70618ef2ead7850f90ab2a8458db192.tar.gz
gitea-c337ff0ec70618ef2ead7850f90ab2a8458db192.zip
Add user blocking (#29028)
Fixes #17453 This PR adds the abbility to block a user from a personal account or organization to restrict how the blocked user can interact with the blocker. The docs explain what's the consequence of blocking a user. Screenshots: ![grafik](https://github.com/go-gitea/gitea/assets/1666336/4ed884f3-e06a-4862-afd3-3b8aa2488dc6) ![grafik](https://github.com/go-gitea/gitea/assets/1666336/ae6d4981-f252-4f50-a429-04f0f9f1cdf1) ![grafik](https://github.com/go-gitea/gitea/assets/1666336/ca153599-5b0f-4b4a-90fe-18bdfd6f0b6b) --------- Co-authored-by: Lauris BH <lauris@nix.lv>
Diffstat (limited to 'services/issue')
-rw-r--r--services/issue/comments.go26
-rw-r--r--services/issue/commit.go4
-rw-r--r--services/issue/content.go13
-rw-r--r--services/issue/issue.go41
-rw-r--r--services/issue/reaction.go50
-rw-r--r--services/issue/reaction_test.go162
6 files changed, 281 insertions, 15 deletions
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 8d8c575c14..d68623aff6 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
@@ -21,6 +22,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
return fmt.Errorf("cannot create reference with empty commit SHA")
}
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
// Check if same reference from same commit has already existed.
has, err := db.GetEngine(ctx).Get(&issues_model.Comment{
Type: issues_model.CommentTypeCommitRef,
@@ -46,6 +53,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
// CreateIssueComment creates a plain issue comment.
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+ return nil, user_model.ErrBlockedUser
+ }
+ }
+
comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeComment,
Doer: doer,
@@ -70,6 +83,19 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
// UpdateComment updates information of comment.
func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
+ if err := c.LoadIssue(ctx); err != nil {
+ return err
+ }
+ if err := c.Issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
if needsContentHistory {
hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
diff --git a/services/issue/commit.go b/services/issue/commit.go
index e493a03211..0a59088d12 100644
--- a/services/issue/commit.go
+++ b/services/issue/commit.go
@@ -5,6 +5,7 @@ package issue
import (
"context"
+ "errors"
"fmt"
"html"
"net/url"
@@ -160,6 +161,9 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ continue
+ }
return err
}
diff --git a/services/issue/content.go b/services/issue/content.go
index 6e56714ddf..2f9bee806a 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -7,12 +7,23 @@ import (
"context"
issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
notify_service "code.gitea.io/gitea/services/notify"
)
// ChangeContent changes issue content, as the given user.
-func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
+func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error {
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
oldContent := issue.Content
if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
diff --git a/services/issue/issue.go b/services/issue/issue.go
index b1f418c32e..27a106009c 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -15,6 +15,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/storage"
notify_service "code.gitea.io/gitea/services/notify"
@@ -22,6 +23,14 @@ import (
// NewIssue creates new issue with labels for repository.
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
+ if err := issue.LoadPoster(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+ return user_model.ErrBlockedUser
+ }
+
if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
return err
}
@@ -57,6 +66,16 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
return nil
}
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
return err
}
@@ -93,31 +112,25 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
- var allNewAssignees []*user_model.User
+ uniqueAssignees := container.SetOf(multipleAssignees...)
// Keep the old assignee thingy for compatibility reasons
if oneAssignee != "" {
- // Prevent double adding assignees
- var isDouble bool
- for _, assignee := range multipleAssignees {
- if assignee == oneAssignee {
- isDouble = true
- break
- }
- }
-
- if !isDouble {
- multipleAssignees = append(multipleAssignees, oneAssignee)
- }
+ uniqueAssignees.Add(oneAssignee)
}
// Loop through all assignees to add them
- for _, assigneeName := range multipleAssignees {
+ allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
+ for _, assigneeName := range uniqueAssignees.Values() {
assignee, err := user_model.GetUserByName(ctx, assigneeName)
if err != nil {
return err
}
+ if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
+ return user_model.ErrBlockedUser
+ }
+
allNewAssignees = append(allNewAssignees, assignee)
}
diff --git a/services/issue/reaction.go b/services/issue/reaction.go
new file mode 100644
index 0000000000..deb99169e1
--- /dev/null
+++ b/services/issue/reaction.go
@@ -0,0 +1,50 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ user_model "code.gitea.io/gitea/models/user"
+)
+
+// CreateIssueReaction creates a reaction on an issue.
+func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
+ if err := issue.LoadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ return nil, user_model.ErrBlockedUser
+ }
+
+ return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+ Type: content,
+ DoerID: doer.ID,
+ IssueID: issue.ID,
+ })
+}
+
+// CreateCommentReaction creates a reaction on a comment.
+func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
+ if err := comment.LoadIssue(ctx); err != nil {
+ return nil, err
+ }
+
+ if err := comment.Issue.LoadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) {
+ return nil, user_model.ErrBlockedUser
+ }
+
+ return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+ Type: content,
+ DoerID: doer.ID,
+ IssueID: comment.Issue.ID,
+ CommentID: comment.ID,
+ })
+}
diff --git a/services/issue/reaction_test.go b/services/issue/reaction_test.go
new file mode 100644
index 0000000000..7734860fc0
--- /dev/null
+++ b/services/issue/reaction_test.go
@@ -0,0 +1,162 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func addReaction(t *testing.T, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) {
+ var reaction *issues_model.Reaction
+ var err error
+ if comment == nil {
+ reaction, err = CreateIssueReaction(db.DefaultContext, doer, issue, content)
+ } else {
+ reaction, err = CreateCommentReaction(db.DefaultContext, doer, comment, content)
+ }
+ assert.NoError(t, err)
+ assert.NotNil(t, reaction)
+}
+
+func TestIssueAddReaction(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+
+ addReaction(t, user1, issue, nil, "heart")
+
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
+}
+
+func TestIssueAddDuplicateReaction(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+
+ addReaction(t, user1, issue, nil, "heart")
+
+ reaction, err := CreateIssueReaction(db.DefaultContext, user1, issue, "heart")
+ assert.Error(t, err)
+ assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
+
+ existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
+ assert.Equal(t, existingR.ID, reaction.ID)
+}
+
+func TestIssueDeleteReaction(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+
+ addReaction(t, user1, issue, nil, "heart")
+
+ err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue.ID, "heart")
+ assert.NoError(t, err)
+
+ unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
+}
+
+func TestIssueReactionCount(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ setting.UI.ReactionMaxUserNum = 2
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ ghost := user_model.NewGhostUser()
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ addReaction(t, user1, issue, nil, "heart")
+ addReaction(t, user2, issue, nil, "heart")
+ addReaction(t, org3, issue, nil, "heart")
+ addReaction(t, org3, issue, nil, "+1")
+ addReaction(t, user4, issue, nil, "+1")
+ addReaction(t, user4, issue, nil, "heart")
+ addReaction(t, ghost, issue, nil, "-1")
+
+ reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
+ IssueID: issue.ID,
+ })
+ assert.NoError(t, err)
+ assert.Len(t, reactionsList, 7)
+ _, err = reactionsList.LoadUsers(db.DefaultContext, repo)
+ assert.NoError(t, err)
+
+ reactions := reactionsList.GroupByType()
+ assert.Len(t, reactions["heart"], 4)
+ assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
+ assert.Equal(t, user1.Name+", "+user2.Name, reactions["heart"].GetFirstUsers())
+ assert.True(t, reactions["heart"].HasUser(1))
+ assert.False(t, reactions["heart"].HasUser(5))
+ assert.False(t, reactions["heart"].HasUser(0))
+ assert.Len(t, reactions["+1"], 2)
+ assert.Equal(t, 0, reactions["+1"].GetMoreUserCount())
+ assert.Len(t, reactions["-1"], 1)
+}
+
+func TestIssueCommentAddReaction(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
+
+ addReaction(t, user1, nil, comment, "heart")
+
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
+}
+
+func TestIssueCommentDeleteReaction(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
+
+ addReaction(t, user1, nil, comment, "heart")
+ addReaction(t, user2, nil, comment, "heart")
+ addReaction(t, org3, nil, comment, "heart")
+ addReaction(t, user4, nil, comment, "+1")
+
+ reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
+ IssueID: comment.IssueID,
+ CommentID: comment.ID,
+ })
+ assert.NoError(t, err)
+ assert.Len(t, reactionsList, 4)
+
+ reactions := reactionsList.GroupByType()
+ assert.Len(t, reactions["heart"], 3)
+ assert.Len(t, reactions["+1"], 1)
+}
+
+func TestIssueCommentReactionCount(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
+
+ addReaction(t, user1, nil, comment, "heart")
+ assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, comment.IssueID, comment.ID, "heart"))
+
+ unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
+}