feat: vendor gitea 1.16.2
This commit is contained in:
@@ -30,22 +30,16 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
|
||||
seed = u.Name
|
||||
}
|
||||
|
||||
img, err := avatar.RandomImage([]byte(seed))
|
||||
if err != nil {
|
||||
return fmt.Errorf("RandomImage: %w", err)
|
||||
}
|
||||
img := avatar.RandomImageDefaultSize([]byte(seed))
|
||||
|
||||
u.Avatar = avatars.HashEmail(seed)
|
||||
|
||||
_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
|
||||
_, err := storage.Avatars.Stat(u.CustomAvatarRelativePath())
|
||||
if err != nil {
|
||||
// If unable to Stat the avatar file (usually it means non-existing), then try to save a new one
|
||||
// Don't share the images so that we can delete them easily
|
||||
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
if err := png.Encode(w, img); err != nil {
|
||||
log.Error("Encode: %v", err)
|
||||
}
|
||||
return nil
|
||||
return png.Encode(w, img)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
@@ -74,7 +68,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||
switch {
|
||||
case u.UseCustomAvatar:
|
||||
useLocalAvatar = true
|
||||
case disableGravatar, setting.OfflineMode:
|
||||
case disableGravatar:
|
||||
useLocalAvatar = true
|
||||
autoGenerateAvatar = true
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// Badge represents a user badge
|
||||
@@ -22,7 +25,16 @@ type Badge struct {
|
||||
type UserBadge struct { //nolint:revive // export stutter
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BadgeID int64
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// TableIndices implements xorm's TableIndices interface
|
||||
func (n *UserBadge) TableIndices() []*schemas.Index {
|
||||
indices := make([]*schemas.Index, 0, 1)
|
||||
ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType)
|
||||
ubUnique.AddColumn("user_id", "badge_id")
|
||||
indices = append(indices, ubUnique)
|
||||
return indices
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -42,32 +54,85 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
|
||||
return badges, count, err
|
||||
}
|
||||
|
||||
// CreateBadge creates a new badge.
|
||||
func CreateBadge(ctx context.Context, badge *Badge) error {
|
||||
_, err := db.GetEngine(ctx).Insert(badge)
|
||||
return err
|
||||
// GetBadgeUsersOptions contains options for getting users with a specific badge
|
||||
type GetBadgeUsersOptions struct {
|
||||
db.ListOptions
|
||||
BadgeSlug string
|
||||
}
|
||||
|
||||
// GetBadge returns a badge
|
||||
// GetBadgeUsers returns the users that have a specific badge with pagination support.
|
||||
func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
Select("`user`.*").
|
||||
Join("INNER", "user_badge", "`user_badge`.user_id=`user`.id").
|
||||
Join("INNER", "badge", "`user_badge`.badge_id=badge.id").
|
||||
Where("badge.slug=?", opts.BadgeSlug)
|
||||
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
}
|
||||
|
||||
users := make([]*User, 0, opts.PageSize)
|
||||
count, err := sess.FindAndCount(&users)
|
||||
return users, count, err
|
||||
}
|
||||
|
||||
// CreateBadge creates a new badge.
|
||||
func CreateBadge(ctx context.Context, badge *Badge) error {
|
||||
exists, err := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug)
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Insert(badge); err != nil {
|
||||
// Handle race between existence check and insert.
|
||||
exists, existErr := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge))
|
||||
if existErr == nil && exists {
|
||||
return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBadge returns a specific badge
|
||||
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
|
||||
badge := new(Badge)
|
||||
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
|
||||
if !has {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return badge, err
|
||||
if !has {
|
||||
return nil, util.NewNotExistErrorf("badge does not exist [slug: %s]", slug)
|
||||
}
|
||||
return badge, nil
|
||||
}
|
||||
|
||||
// UpdateBadge updates a badge based on its slug.
|
||||
func UpdateBadge(ctx context.Context, badge *Badge) error {
|
||||
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
|
||||
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteBadge deletes a badge.
|
||||
// DeleteBadge deletes a badge and all associated user_badge entries.
|
||||
func DeleteBadge(ctx context.Context, badge *Badge) error {
|
||||
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
|
||||
return err
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// First delete all user_badge entries for this badge
|
||||
if _, err := db.GetEngine(ctx).
|
||||
Where("badge_id = ?", badge.ID).
|
||||
Delete(&UserBadge{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then delete the badge itself
|
||||
if _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// AddUserBadge adds a badge to a user.
|
||||
@@ -84,12 +149,25 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
|
||||
return util.NewNotExistErrorf("badge does not exist [slug: %s]", badge.Slug)
|
||||
}
|
||||
|
||||
exists, err := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID)
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, &UserBadge{
|
||||
BadgeID: badge.ID,
|
||||
UserID: u.ID,
|
||||
}); err != nil {
|
||||
exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge))
|
||||
if existErr == nil && exists {
|
||||
return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -102,16 +180,33 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
|
||||
return RemoveUserBadges(ctx, u, []*Badge{badge})
|
||||
}
|
||||
|
||||
// RemoveUserBadges removes badges from a user.
|
||||
// RemoveUserBadges removes specific badges from a user.
|
||||
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if len(badges) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
badgeSlugs := make([]string, 0, len(badges))
|
||||
for _, badge := range badges {
|
||||
if _, err := db.GetEngine(ctx).
|
||||
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
|
||||
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
|
||||
Delete(&UserBadge{}); err != nil {
|
||||
return err
|
||||
}
|
||||
badgeSlugs = append(badgeSlugs, badge.Slug)
|
||||
}
|
||||
var userBadges []UserBadge
|
||||
if err := db.GetEngine(ctx).Table("user_badge").
|
||||
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
|
||||
Where("`user_badge`.user_id = ?", u.ID).In("`badge`.slug", badgeSlugs).
|
||||
Find(&userBadges); err != nil {
|
||||
return err
|
||||
}
|
||||
userBadgeIDs := make([]int64, 0, len(userBadges))
|
||||
for _, ub := range userBadges {
|
||||
userBadgeIDs = append(userBadgeIDs, ub.ID)
|
||||
}
|
||||
if len(userBadgeIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Table("user_badge").In("id", userBadgeIDs).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -122,3 +217,57 @@ func RemoveAllUserBadges(ctx context.Context, u *User) error {
|
||||
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
|
||||
return err
|
||||
}
|
||||
|
||||
// SearchBadgeOptions represents the options when finding badges
|
||||
type SearchBadgeOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
Keyword string
|
||||
Slug string
|
||||
ID int64
|
||||
OrderBy db.SearchOrderBy
|
||||
}
|
||||
|
||||
func (opts *SearchBadgeOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if opts.Keyword != "" {
|
||||
keywordCond := builder.Or(
|
||||
db.BuildCaseInsensitiveLike("badge.slug", opts.Keyword),
|
||||
db.BuildCaseInsensitiveLike("badge.description", opts.Keyword),
|
||||
)
|
||||
cond = cond.And(keywordCond)
|
||||
}
|
||||
|
||||
if opts.ID > 0 {
|
||||
cond = cond.And(builder.Eq{"badge.id": opts.ID})
|
||||
}
|
||||
|
||||
if len(opts.Slug) > 0 {
|
||||
cond = cond.And(builder.Eq{"badge.slug": opts.Slug})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts *SearchBadgeOptions) ToOrders() string {
|
||||
return opts.OrderBy.String()
|
||||
}
|
||||
|
||||
// SearchBadges returns badges based on the provided SearchBadgeOptions options
|
||||
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) {
|
||||
return db.FindAndCount[Badge](ctx, opts)
|
||||
}
|
||||
|
||||
// GetBadgeByID returns a specific badge by ID
|
||||
func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) {
|
||||
badge := new(Badge)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(badge)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, util.NewNotExistErrorf("badge does not exist [id: %d]", id)
|
||||
}
|
||||
return badge, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBadge(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("GetBadgeNotExist", testGetBadgeNotExist)
|
||||
t.Run("CreateBadgeAlreadyExists", testCreateBadgeAlreadyExists)
|
||||
t.Run("GetBadgeUsers", testGetBadgeUsers)
|
||||
t.Run("AddAndRemoveUserBadges", testAddAndRemoveUserBadges)
|
||||
t.Run("SearchBadgesOrderingAndKeyword", testSearchBadgesOrderingAndKeyword)
|
||||
}
|
||||
|
||||
func testGetBadgeNotExist(t *testing.T) {
|
||||
badge, err := user_model.GetBadge(t.Context(), "does-not-exist")
|
||||
assert.Nil(t, badge)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
func testCreateBadgeAlreadyExists(t *testing.T) {
|
||||
badge := &user_model.Badge{
|
||||
Slug: "duplicate-badge-slug",
|
||||
Description: "First",
|
||||
}
|
||||
assert.NoError(t, user_model.CreateBadge(t.Context(), badge))
|
||||
|
||||
err := user_model.CreateBadge(t.Context(), &user_model.Badge{
|
||||
Slug: "duplicate-badge-slug",
|
||||
Description: "Second",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrAlreadyExist)
|
||||
}
|
||||
|
||||
func testGetBadgeUsers(t *testing.T) {
|
||||
// Create a test badge
|
||||
badge := &user_model.Badge{
|
||||
Slug: "test-badge-users",
|
||||
Description: "Test Badge",
|
||||
ImageURL: "test.png",
|
||||
}
|
||||
assert.NoError(t, user_model.CreateBadge(t.Context(), badge))
|
||||
|
||||
// Create test users and assign badges
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge))
|
||||
assert.NoError(t, user_model.AddUserBadge(t.Context(), user2, badge))
|
||||
defer func() {
|
||||
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge))
|
||||
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user2, badge))
|
||||
}()
|
||||
|
||||
// Test getting users with pagination
|
||||
opts := &user_model.GetBadgeUsersOptions{
|
||||
BadgeSlug: badge.Slug,
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 1,
|
||||
},
|
||||
}
|
||||
|
||||
users, count, err := user_model.GetBadgeUsers(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, count)
|
||||
assert.Len(t, users, 1)
|
||||
|
||||
// Test second page
|
||||
opts.Page = 2
|
||||
users, count, err = user_model.GetBadgeUsers(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, count)
|
||||
assert.Len(t, users, 1)
|
||||
|
||||
// Test with non-existent badge
|
||||
opts.BadgeSlug = "non-existent"
|
||||
users, count, err = user_model.GetBadgeUsers(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
assert.Empty(t, users)
|
||||
}
|
||||
|
||||
func testAddAndRemoveUserBadges(t *testing.T) {
|
||||
badge1 := unittest.AssertExistsAndLoadBean(t, &user_model.Badge{ID: 1})
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
// Add a badge to user and verify that it is returned in the list
|
||||
assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge1))
|
||||
badges, count, err := user_model.GetUserBadges(t.Context(), user1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
assert.Equal(t, badge1.Slug, badges[0].Slug)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Confirm that it is impossible to duplicate the same badge
|
||||
err = user_model.AddUserBadge(t.Context(), user1, badge1)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrAlreadyExist)
|
||||
|
||||
// Nothing happened to the existing badge
|
||||
badges, count, err = user_model.GetUserBadges(t.Context(), user1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
assert.Equal(t, badge1.Slug, badges[0].Slug)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Remove a badge from user and verify that it is no longer in the list
|
||||
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge1))
|
||||
_, count, err = user_model.GetUserBadges(t.Context(), user1)
|
||||
assert.Equal(t, int64(0), count)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Removing empty or missing badge selections should be a no-op.
|
||||
assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, nil))
|
||||
assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, []*user_model.Badge{{Slug: "does-not-exist"}}))
|
||||
}
|
||||
|
||||
func testSearchBadgesOrderingAndKeyword(t *testing.T) {
|
||||
createdBadges := []*user_model.Badge{
|
||||
{Slug: "badge-sort-b", Description: "Badge Sort B"},
|
||||
{Slug: "badge-sort-c", Description: "Badge Sort C"},
|
||||
{Slug: "badge-sort-a", Description: "Badge Sort A"},
|
||||
{Slug: "badge-sort-case", Description: "MiXeDCaSeKeyword"},
|
||||
}
|
||||
for _, badge := range createdBadges {
|
||||
assert.NoError(t, user_model.CreateBadge(t.Context(), badge))
|
||||
}
|
||||
|
||||
opts := &user_model.SearchBadgeOptions{
|
||||
ListOptions: db.ListOptions{ListAll: true},
|
||||
Keyword: "badge-sort-",
|
||||
OrderBy: db.SearchOrderBy("`badge`.id ASC"),
|
||||
}
|
||||
|
||||
oldestFirst, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
assert.Equal(t, []string{"badge-sort-b", "badge-sort-c", "badge-sort-a", "badge-sort-case"}, collectBadgeSlugs(oldestFirst))
|
||||
|
||||
opts.OrderBy = db.SearchOrderBy("`badge`.id DESC")
|
||||
newestFirst, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
assert.Equal(t, []string{"badge-sort-case", "badge-sort-a", "badge-sort-c", "badge-sort-b"}, collectBadgeSlugs(newestFirst))
|
||||
|
||||
opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC")
|
||||
alpha, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
assert.Equal(t, []string{"badge-sort-a", "badge-sort-b", "badge-sort-c", "badge-sort-case"}, collectBadgeSlugs(alpha))
|
||||
|
||||
opts.OrderBy = db.SearchOrderBy("`badge`.slug DESC")
|
||||
reverseAlpha, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
assert.Equal(t, []string{"badge-sort-case", "badge-sort-c", "badge-sort-b", "badge-sort-a"}, collectBadgeSlugs(reverseAlpha))
|
||||
|
||||
opts.Keyword = "mixedcasekeyword"
|
||||
opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC")
|
||||
caseInsensitive, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, count)
|
||||
assert.Equal(t, []string{"badge-sort-case"}, collectBadgeSlugs(caseInsensitive))
|
||||
}
|
||||
|
||||
func collectBadgeSlugs(badges []*user_model.Badge) []string {
|
||||
slugs := make([]string, 0, len(badges))
|
||||
for _, badge := range badges {
|
||||
slugs = append(slugs, badge.Slug)
|
||||
}
|
||||
return slugs
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er
|
||||
return nil, err
|
||||
}
|
||||
if len(blocks) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return blocks[0], nil
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, err
|
||||
if has, err := db.GetEngine(ctx).ID(id).Get(email); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
@@ -276,17 +276,22 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
|
||||
return UpdateUserCols(ctx, user, "rands")
|
||||
}
|
||||
|
||||
func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error {
|
||||
return makeEmailPrimaryInternal(ctx, emailID, true)
|
||||
func MakeActiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error {
|
||||
return makeEmailPrimaryInternal(ctx, ownerID, emailID, true)
|
||||
}
|
||||
|
||||
func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error {
|
||||
return makeEmailPrimaryInternal(ctx, emailID, false)
|
||||
func MakeInactiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error {
|
||||
return makeEmailPrimaryInternal(ctx, ownerID, emailID, false)
|
||||
}
|
||||
|
||||
func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error {
|
||||
func makeEmailPrimaryInternal(ctx context.Context, ownerID, emailID int64, isActive bool) error {
|
||||
email := &EmailAddress{}
|
||||
if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil {
|
||||
if has, err := db.GetEngine(ctx).ID(emailID).
|
||||
Where(builder.Eq{
|
||||
"uid": ownerID,
|
||||
"is_activated": isActive,
|
||||
}).
|
||||
Get(email); err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return ErrEmailAddressNotExist{}
|
||||
@@ -336,7 +341,7 @@ func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, ne
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MakeInactiveEmailPrimary(ctx, newEmail.ID)
|
||||
return MakeInactiveEmailPrimary(ctx, uid, newEmail.ID)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -46,22 +46,22 @@ func TestIsEmailUsed(t *testing.T) {
|
||||
func TestMakeEmailPrimary(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
err := user_model.MakeActiveEmailPrimary(t.Context(), 9999999)
|
||||
err := user_model.MakeActiveEmailPrimary(t.Context(), 1, 9999999)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{})
|
||||
|
||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"})
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary"
|
||||
|
||||
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"})
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user_model.IsErrUserNotExist(err))
|
||||
|
||||
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"})
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, _ := user_model.GetUserByID(t.Context(), int64(10))
|
||||
|
||||
@@ -24,7 +24,7 @@ func (users UserList) GetUserIDs() []int64 {
|
||||
return userIDs
|
||||
}
|
||||
|
||||
// GetTwoFaStatus return state of 2FA enrollement
|
||||
// GetTwoFaStatus return state of 2FA enrollment
|
||||
func (users UserList) GetTwoFaStatus(ctx context.Context) map[int64]bool {
|
||||
results := make(map[int64]bool, len(users))
|
||||
for _, user := range users {
|
||||
@@ -48,7 +48,7 @@ func (users UserList) GetTwoFaStatus(ctx context.Context) map[int64]bool {
|
||||
|
||||
func (users UserList) loadTwoFactorStatus(ctx context.Context) (map[int64]*auth.TwoFactor, error) {
|
||||
if len(users) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // returns nil when there are no users
|
||||
}
|
||||
|
||||
userIDs := users.GetUserIDs()
|
||||
|
||||
@@ -6,6 +6,7 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -17,12 +18,29 @@ import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AdminUserOrderByMap represents all possible admin user search orders
|
||||
// This should only be used for admin API endpoints as we should not expose "updated" ordering which could expose recent user activity including logins.
|
||||
var AdminUserOrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||
"asc": {
|
||||
"name": db.SearchOrderByAlphabetically,
|
||||
"created": db.SearchOrderByOldest,
|
||||
"updated": db.SearchOrderByLeastUpdated,
|
||||
"id": db.SearchOrderByID,
|
||||
},
|
||||
"desc": {
|
||||
"name": db.SearchOrderByAlphabeticallyReverse,
|
||||
"created": db.SearchOrderByNewest,
|
||||
"updated": db.SearchOrderByRecentUpdated,
|
||||
"id": db.SearchOrderByIDReverse,
|
||||
},
|
||||
}
|
||||
|
||||
// SearchUserOptions contains the options for searching
|
||||
type SearchUserOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
Keyword string
|
||||
Type UserType
|
||||
Types []UserType
|
||||
UID int64
|
||||
LoginName string // this option should be used only for admin user
|
||||
SourceID int64 // this option should be used only for admin user
|
||||
@@ -41,18 +59,24 @@ type SearchUserOptions struct {
|
||||
IncludeReserved bool
|
||||
}
|
||||
|
||||
func (opts *SearchUserOptions) ApplyPublicOnly(publicOnly bool) {
|
||||
if publicOnly {
|
||||
opts.Visible = []structs.VisibleType{structs.VisibleTypePublic}
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
|
||||
var cond builder.Cond
|
||||
cond = builder.Eq{"type": opts.Type}
|
||||
cond = builder.In("type", opts.Types)
|
||||
if opts.IncludeReserved {
|
||||
switch opts.Type {
|
||||
case UserTypeIndividual:
|
||||
switch {
|
||||
case slices.Contains(opts.Types, UserTypeIndividual):
|
||||
cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or(
|
||||
builder.Eq{"type": UserTypeBot},
|
||||
).Or(
|
||||
builder.Eq{"type": UserTypeRemoteUser},
|
||||
)
|
||||
case UserTypeOrganization:
|
||||
case slices.Contains(opts.Types, UserTypeOrganization):
|
||||
cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
setting_module "code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm/convert"
|
||||
)
|
||||
|
||||
// Setting is a key value store of user settings
|
||||
@@ -211,3 +213,44 @@ func upsertUserSettingValue(ctx context.Context, userID int64, key, value string
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func GetUserSettingJSON[T any](ctx context.Context, userID int64, key string, def T) (ret T, _ error) {
|
||||
ret = def
|
||||
str, err := GetUserSetting(ctx, userID, key)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
conv, ok := any(&ret).(convert.ConversionFrom)
|
||||
if !ok {
|
||||
conv, ok = any(ret).(convert.ConversionFrom)
|
||||
}
|
||||
if ok {
|
||||
if err := conv.FromDB(util.UnsafeStringToBytes(str)); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
} else {
|
||||
if str == "" {
|
||||
return ret, nil
|
||||
}
|
||||
err = json.Unmarshal(util.UnsafeStringToBytes(str), &ret)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func SetUserSettingJSON[T any](ctx context.Context, userID int64, key string, val T) (err error) {
|
||||
conv, ok := any(&val).(convert.ConversionTo)
|
||||
if !ok {
|
||||
conv, ok = any(val).(convert.ConversionTo)
|
||||
}
|
||||
var bs []byte
|
||||
if ok {
|
||||
bs, err = conv.ToDB()
|
||||
} else {
|
||||
bs, err = json.Marshal(val)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return SetUserSetting(ctx, userID, key, util.UnsafeBytesToString(bs))
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ const (
|
||||
// SettingsKeyShowOutdatedComments is the setting key whether or not to show outdated comments in PRs
|
||||
SettingsKeyShowOutdatedComments = "comment_code.show_outdated"
|
||||
|
||||
// UserActivityPubPrivPem is user's private key
|
||||
UserActivityPubPrivPem = "activitypub.priv_pem"
|
||||
// UserActivityPubPubPem is user's public key
|
||||
UserActivityPubPubPem = "activitypub.pub_pem"
|
||||
// SignupIP is the IP address that the user signed up with
|
||||
SignupIP = "signup.ip"
|
||||
// SignupUserAgent is the user agent that the user signed up with
|
||||
@@ -26,4 +22,6 @@ const (
|
||||
SettingEmailNotificationGiteaActionsAll = "all"
|
||||
SettingEmailNotificationGiteaActionsFailureOnly = "failure-only" // Default for actions email preference
|
||||
SettingEmailNotificationGiteaActionsDisabled = "disabled"
|
||||
|
||||
SettingsKeyActionsConfig = "actions.config"
|
||||
)
|
||||
|
||||
@@ -7,12 +7,15 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"mime"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -27,6 +30,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@@ -184,7 +188,7 @@ func (u *User) BeforeUpdate() {
|
||||
}
|
||||
|
||||
// FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field
|
||||
// This trick could be removed in new releases to display the user inputed email as-is.
|
||||
// This trick could be removed in new releases to display the user inputted email as-is.
|
||||
u.Email = strings.ToLower(u.Email)
|
||||
if !u.IsOrganization() {
|
||||
if len(u.AvatarEmail) == 0 {
|
||||
@@ -212,7 +216,7 @@ func (u *User) SetLastLogin() {
|
||||
|
||||
// GetPlaceholderEmail returns an noreply email
|
||||
func (u *User) GetPlaceholderEmail() string {
|
||||
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
|
||||
return fmt.Sprintf("%d+%s@%s", u.ID, u.LowerName, setting.Service.NoReplyAddress)
|
||||
}
|
||||
|
||||
// GetEmail returns a noreply email, if the user has set to keep his
|
||||
@@ -249,8 +253,13 @@ func (u *User) MaxCreationLimit() int {
|
||||
}
|
||||
|
||||
// CanCreateRepoIn checks whether the doer(u) can create a repository in the owner
|
||||
// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised
|
||||
// NOTE: functions calling this assume a failure due to repository count limit, or the owner is not a real user.
|
||||
// It ONLY checks the repo number LIMIT or whether owner user is real. If new checks are added, those functions should be revised.
|
||||
// TODO: the callers can only return ErrReachLimitOfRepo, need to fine tune to support other error types in the future.
|
||||
func (u *User) CanCreateRepoIn(owner *User) bool {
|
||||
if u.ID <= 0 || owner.ID <= 0 {
|
||||
return false // fake user like Ghost or Actions user
|
||||
}
|
||||
if u.IsAdmin {
|
||||
return true
|
||||
}
|
||||
@@ -298,6 +307,13 @@ func (u *User) DashboardLink() string {
|
||||
return setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
func (u *User) SettingsLink() string {
|
||||
if u.IsOrganization() {
|
||||
return u.OrganisationLink() + "/settings"
|
||||
}
|
||||
return setting.AppSubURL + "/user/settings"
|
||||
}
|
||||
|
||||
// HomeLink returns the user or organization home page link.
|
||||
func (u *User) HomeLink() string {
|
||||
return setting.AppSubURL + "/" + url.PathEscape(u.Name)
|
||||
@@ -411,16 +427,6 @@ func (u *User) IsTokenAccessAllowed() bool {
|
||||
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
|
||||
}
|
||||
|
||||
// DisplayName returns full name if it's not empty,
|
||||
// returns username otherwise.
|
||||
func (u *User) DisplayName() string {
|
||||
trimmed := strings.TrimSpace(u.FullName)
|
||||
if len(trimmed) > 0 {
|
||||
return trimmed
|
||||
}
|
||||
return u.Name
|
||||
}
|
||||
|
||||
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
|
||||
func (u *User) EmailTo() string {
|
||||
sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
|
||||
@@ -439,27 +445,45 @@ func (u *User) EmailTo() string {
|
||||
return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address)
|
||||
}
|
||||
|
||||
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
|
||||
// returns username otherwise.
|
||||
// TODO: DefaultShowFullName causes messy logic, there are already too many methods to display a user's "display name", need to refactor them
|
||||
// * user.Name / user.FullName: directly used in templates
|
||||
// * user.DisplayName(): always show FullName if it's not empty, otherwise show Name
|
||||
// * user.GetDisplayName(): show FullName if it's not empty and DefaultShowFullName is set, otherwise show Name
|
||||
// * user.ShortName(): used a lot in templates, but it should be removed and let frontend use "ellipsis" styles
|
||||
// * activity action.ShortActUserName/GetActDisplayName/GetActDisplayNameTitle, etc: duplicate and messy
|
||||
|
||||
// DisplayName returns full name if it's not empty, returns username otherwise.
|
||||
func (u *User) DisplayName() string {
|
||||
fullName := strings.TrimSpace(u.FullName)
|
||||
if fullName != "" {
|
||||
return fullName
|
||||
}
|
||||
return u.Name
|
||||
}
|
||||
|
||||
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, otherwise, username.
|
||||
func (u *User) GetDisplayName() string {
|
||||
if setting.UI.DefaultShowFullName {
|
||||
trimmed := strings.TrimSpace(u.FullName)
|
||||
if len(trimmed) > 0 {
|
||||
return trimmed
|
||||
fullName := strings.TrimSpace(u.FullName)
|
||||
if fullName != "" {
|
||||
return fullName
|
||||
}
|
||||
}
|
||||
return u.Name
|
||||
}
|
||||
|
||||
// GetCompleteName returns the full name and username in the form of
|
||||
// "Full Name (username)" if full name is not empty, otherwise it returns
|
||||
// "username".
|
||||
func (u *User) GetCompleteName() string {
|
||||
trimmedFullName := strings.TrimSpace(u.FullName)
|
||||
if len(trimmedFullName) > 0 {
|
||||
return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name)
|
||||
// ShortName ellipses username to length (still used by many templates), it calls GetDisplayName and respects DEFAULT_SHOW_FULL_NAME
|
||||
func (u *User) ShortName(length int) string {
|
||||
return util.EllipsisDisplayString(u.GetDisplayName(), length)
|
||||
}
|
||||
|
||||
func (u *User) GetShortDisplayNameLinkHTML() template.HTML {
|
||||
fullName := strings.TrimSpace(u.FullName)
|
||||
displayName, displayTooltip := u.Name, fullName
|
||||
if setting.UI.DefaultShowFullName && fullName != "" {
|
||||
displayName, displayTooltip = fullName, u.Name
|
||||
}
|
||||
return u.Name
|
||||
return htmlutil.HTMLFormat(`<a class="muted" href="%s" data-tooltip-content="%s">%s</a>`, u.HomeLink(), displayTooltip, displayName)
|
||||
}
|
||||
|
||||
func gitSafeName(name string) string {
|
||||
@@ -482,18 +506,10 @@ func (u *User) GitName() string {
|
||||
return fmt.Sprintf("user-%d", u.ID)
|
||||
}
|
||||
|
||||
// ShortName ellipses username to length
|
||||
func (u *User) ShortName(length int) string {
|
||||
if setting.UI.DefaultShowFullName && len(u.FullName) > 0 {
|
||||
return util.EllipsisDisplayString(u.FullName, length)
|
||||
}
|
||||
return util.EllipsisDisplayString(u.Name, length)
|
||||
}
|
||||
|
||||
// IsMailable checks if a user is eligible
|
||||
// to receive emails.
|
||||
// IsMailable checks if a user is eligible to receive emails.
|
||||
// System users like Ghost and Gitea Actions are excluded.
|
||||
func (u *User) IsMailable() bool {
|
||||
return u.IsActive
|
||||
return u.IsActive && !u.IsGiteaActions() && !u.IsGhost()
|
||||
}
|
||||
|
||||
// IsUserExist checks if given username exist,
|
||||
@@ -980,7 +996,7 @@ func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, er
|
||||
|
||||
// UserPath returns the path absolute path of user repositories.
|
||||
func UserPath(userName string) string { //revive:disable-line:exported
|
||||
return filepath.Join(setting.RepoRootPath, strings.ToLower(userName))
|
||||
return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName)))
|
||||
}
|
||||
|
||||
// GetUserByID returns the user object by given ID if exists.
|
||||
@@ -1008,17 +1024,22 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
return users, err
|
||||
}
|
||||
|
||||
// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0
|
||||
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
|
||||
// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
|
||||
func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
|
||||
if id < 0 {
|
||||
if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
|
||||
return newFunc(), nil
|
||||
u = newFunc()
|
||||
}
|
||||
return nil, ErrUserNotExist{UID: id}
|
||||
} else if id == 0 {
|
||||
return nil, ErrUserNotExist{}
|
||||
}
|
||||
return GetUserByID(ctx, id)
|
||||
if u == nil {
|
||||
u, err = GetUserByID(ctx, id)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
u = NewGhostUser()
|
||||
} else if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
}
|
||||
return u.ID, u, nil
|
||||
}
|
||||
|
||||
// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
|
||||
@@ -1039,13 +1060,13 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetUserByName returns user by given name.
|
||||
func GetUserByName(ctx context.Context, name string) (*User, error) {
|
||||
if len(name) == 0 {
|
||||
return nil, ErrUserNotExist{Name: name}
|
||||
func getUserByNameWithTypes(ctx context.Context, name string, types ...UserType) (*User, error) {
|
||||
u := &User{}
|
||||
sess := db.GetEngine(ctx).Where(builder.Eq{"lower_name": strings.ToLower(name)})
|
||||
if len(types) > 0 {
|
||||
sess.In("`type`", types)
|
||||
}
|
||||
u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual}
|
||||
has, err := db.GetEngine(ctx).Get(u)
|
||||
has, err := sess.Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -1054,6 +1075,15 @@ func GetUserByName(ctx context.Context, name string) (*User, error) {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// GetUserByName returns the user object by given name, any user type.
|
||||
func GetUserByName(ctx context.Context, name string) (*User, error) {
|
||||
return getUserByNameWithTypes(ctx, name)
|
||||
}
|
||||
|
||||
func GetIndividualUserByName(ctx context.Context, name string) (*User, error) {
|
||||
return getUserByNameWithTypes(ctx, name, UserTypeIndividual)
|
||||
}
|
||||
|
||||
// GetUserEmailsByNames returns a list of e-mails corresponds to names of users
|
||||
// that have their email notifications set to enabled or onmention.
|
||||
func GetUserEmailsByNames(ctx context.Context, names []string) []string {
|
||||
@@ -1096,19 +1126,6 @@ func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]
|
||||
Find(&ous)
|
||||
}
|
||||
|
||||
// GetUserNameByID returns username for the id
|
||||
func GetUserNameByID(ctx context.Context, id int64) (string, error) {
|
||||
var name string
|
||||
has, err := db.GetEngine(ctx).Table("user").Where("id = ?", id).Cols("name").Get(&name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if has {
|
||||
return name, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// GetUserIDsByNames returns a slice of ids corresponds to names.
|
||||
func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bool) ([]int64, error) {
|
||||
ids := make([]int64, 0, len(names))
|
||||
@@ -1187,19 +1204,23 @@ func (eum *EmailUserMap) GetByEmail(email string) *User {
|
||||
|
||||
func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) {
|
||||
if len(emails) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when there are no emails to look up
|
||||
}
|
||||
|
||||
needCheckEmails := make(container.Set[string])
|
||||
needCheckUserNames := make(container.Set[string])
|
||||
needCheckUserIDs := make(container.Set[int64])
|
||||
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
|
||||
for _, email := range emails {
|
||||
emailLower := strings.ToLower(email)
|
||||
if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
|
||||
needCheckUserNames.Add(noReplyUserNameLower)
|
||||
needCheckEmails.Add(emailLower)
|
||||
} else {
|
||||
needCheckEmails.Add(emailLower)
|
||||
needCheckEmails.Add(emailLower)
|
||||
if localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
|
||||
name, id := parseLocalPartToNameID(localPart)
|
||||
if id != 0 {
|
||||
needCheckUserIDs.Add(id)
|
||||
} else if name != "" {
|
||||
needCheckUserNames.Add(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1229,16 +1250,59 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
|
||||
}
|
||||
}
|
||||
|
||||
users := make(map[int64]*User, len(needCheckUserNames))
|
||||
if err := db.GetEngine(ctx).In("lower_name", needCheckUserNames.Values()).Find(&users); err != nil {
|
||||
return nil, err
|
||||
usersByIDs := make(map[int64]*User)
|
||||
if len(needCheckUserIDs) > 0 || len(needCheckUserNames) > 0 {
|
||||
cond := builder.NewCond()
|
||||
if len(needCheckUserIDs) > 0 {
|
||||
cond = cond.Or(builder.In("id", needCheckUserIDs.Values()))
|
||||
}
|
||||
if len(needCheckUserNames) > 0 {
|
||||
cond = cond.Or(builder.In("lower_name", needCheckUserNames.Values()))
|
||||
}
|
||||
if err := db.GetEngine(ctx).Where(cond).Find(&usersByIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, user := range users {
|
||||
results[strings.ToLower(user.GetPlaceholderEmail())] = user
|
||||
|
||||
usersByName := make(map[string]*User)
|
||||
for _, user := range usersByIDs {
|
||||
usersByName[user.LowerName] = user
|
||||
}
|
||||
|
||||
for _, email := range emails {
|
||||
emailLower := strings.ToLower(email)
|
||||
if _, ok := results[emailLower]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, id := parseLocalPartToNameID(localPart)
|
||||
if user, ok := usersByIDs[id]; ok {
|
||||
results[emailLower] = user
|
||||
} else if user, ok := usersByName[name]; ok {
|
||||
results[emailLower] = user
|
||||
}
|
||||
}
|
||||
|
||||
return &EmailUserMap{results}, nil
|
||||
}
|
||||
|
||||
// parseLocalPartToNameID attempts to unparse local-part of email that's in format id+user
|
||||
// returns user and id if possible
|
||||
func parseLocalPartToNameID(localPart string) (string, int64) {
|
||||
var id int64
|
||||
idstr, name, hasPlus := strings.Cut(localPart, "+")
|
||||
if hasPlus {
|
||||
id, _ = strconv.ParseInt(idstr, 10, 64)
|
||||
} else {
|
||||
name = idstr
|
||||
}
|
||||
return name, id
|
||||
}
|
||||
|
||||
// GetUserByEmail returns the user object by given e-mail if exists.
|
||||
func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
if len(email) == 0 {
|
||||
@@ -1257,24 +1321,24 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
}
|
||||
|
||||
// Finally, if email address is the protected email address:
|
||||
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
|
||||
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
|
||||
user := &User{}
|
||||
has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return user, nil
|
||||
if localPart, ok := strings.CutSuffix(email, strings.ToLower("@"+setting.Service.NoReplyAddress)); ok {
|
||||
name, id := parseLocalPartToNameID(localPart)
|
||||
if id != 0 {
|
||||
return GetUserByID(ctx, id)
|
||||
}
|
||||
return GetIndividualUserByName(ctx, name)
|
||||
}
|
||||
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
|
||||
// GetUser checks if a user already exists
|
||||
func GetUser(ctx context.Context, user *User) (bool, error) {
|
||||
return db.GetEngine(ctx).Get(user)
|
||||
func GetIndividualUser(ctx context.Context, user *User) (bool, error) {
|
||||
// FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future
|
||||
has, err := db.GetEngine(ctx).Get(user)
|
||||
if has && user.Type != UserTypeIndividual {
|
||||
has = false
|
||||
}
|
||||
return has, err
|
||||
}
|
||||
|
||||
// GetUserByOpenID returns the user object by given OpenID if exists.
|
||||
@@ -1444,15 +1508,3 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
|
||||
}
|
||||
return &setting.Admin.UserDisabledFeatures
|
||||
}
|
||||
|
||||
// GetUserOrOrgByName returns the user or org by name
|
||||
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
|
||||
var u User
|
||||
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrUserNotExist{Name: name}
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
@@ -23,10 +24,6 @@ func NewGhostUser() *User {
|
||||
}
|
||||
}
|
||||
|
||||
func IsGhostUserName(name string) bool {
|
||||
return strings.EqualFold(name, GhostUserName)
|
||||
}
|
||||
|
||||
// IsGhost check if user is fake user for a deleted account
|
||||
func (u *User) IsGhost() bool {
|
||||
if u == nil {
|
||||
@@ -41,36 +38,52 @@ const (
|
||||
ActionsUserEmail = "teabot@gitea.io"
|
||||
)
|
||||
|
||||
func IsGiteaActionsUserName(name string) bool {
|
||||
return strings.EqualFold(name, ActionsUserName)
|
||||
}
|
||||
|
||||
// NewActionsUser creates and returns a fake user for running the actions.
|
||||
func NewActionsUser() *User {
|
||||
return &User{
|
||||
ID: ActionsUserID,
|
||||
Name: ActionsUserName,
|
||||
LowerName: ActionsUserName,
|
||||
IsActive: true,
|
||||
FullName: "Gitea Actions",
|
||||
Email: ActionsUserEmail,
|
||||
KeepEmailPrivate: true,
|
||||
LoginName: ActionsUserName,
|
||||
Type: UserTypeBot,
|
||||
AllowCreateOrganization: true,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
ID: ActionsUserID,
|
||||
Name: ActionsUserName,
|
||||
LowerName: ActionsUserName,
|
||||
IsActive: true,
|
||||
FullName: "Gitea Actions",
|
||||
Email: ActionsUserEmail,
|
||||
KeepEmailPrivate: true,
|
||||
LoginName: ActionsUserName,
|
||||
Type: UserTypeBot,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
func NewActionsUserWithTaskID(id int64) *User {
|
||||
u := NewActionsUser()
|
||||
// LoginName is for only internal usage in this case, so it can be moved to other fields in the future
|
||||
u.LoginSource = -1
|
||||
u.LoginName = "@" + ActionsUserName + "/" + strconv.FormatInt(id, 10)
|
||||
return u
|
||||
}
|
||||
|
||||
func GetActionsUserTaskID(u *User) (int64, bool) {
|
||||
if u == nil || u.ID != ActionsUserID {
|
||||
return 0, false
|
||||
}
|
||||
prefix, payload, _ := strings.Cut(u.LoginName, "/")
|
||||
if prefix != "@"+ActionsUserName {
|
||||
return 0, false
|
||||
} else if taskID, err := strconv.ParseInt(payload, 10, 64); err == nil {
|
||||
return taskID, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (u *User) IsGiteaActions() bool {
|
||||
return u != nil && u.ID == ActionsUserID
|
||||
}
|
||||
|
||||
func GetSystemUserByName(name string) *User {
|
||||
if IsGhostUserName(name) {
|
||||
if strings.EqualFold(name, GhostUserName) {
|
||||
return NewGhostUser()
|
||||
}
|
||||
if IsGiteaActionsUserName(name) {
|
||||
if strings.EqualFold(name, ActionsUserName) {
|
||||
return NewActionsUser()
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -11,20 +11,30 @@ import (
|
||||
)
|
||||
|
||||
func TestSystemUser(t *testing.T) {
|
||||
u, err := GetPossibleUserByID(t.Context(), -1)
|
||||
uid, u, err := GetPossibleUserByID(t.Context(), -1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(-1), uid)
|
||||
assert.Equal(t, "Ghost", u.Name)
|
||||
assert.Equal(t, "ghost", u.LowerName)
|
||||
assert.True(t, u.IsGhost())
|
||||
assert.True(t, IsGhostUserName("gHost"))
|
||||
|
||||
u, err = GetPossibleUserByID(t.Context(), -2)
|
||||
u = GetSystemUserByName("gHost")
|
||||
require.NotNil(t, u)
|
||||
assert.Equal(t, "Ghost", u.Name)
|
||||
|
||||
uid, u, err = GetPossibleUserByID(t.Context(), -2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(-2), uid)
|
||||
assert.Equal(t, "gitea-actions", u.Name)
|
||||
assert.Equal(t, "gitea-actions", u.LowerName)
|
||||
assert.True(t, u.IsGiteaActions())
|
||||
assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
|
||||
|
||||
_, err = GetPossibleUserByID(t.Context(), -3)
|
||||
require.Error(t, err)
|
||||
u = GetSystemUserByName("Gitea-actionS")
|
||||
require.NotNil(t, u)
|
||||
assert.Equal(t, "Gitea Actions", u.FullName)
|
||||
|
||||
uid, u, err = GetPossibleUserByID(t.Context(), 999999)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(-1), uid)
|
||||
assert.Equal(t, "Ghost", u.Name)
|
||||
}
|
||||
|
||||
@@ -51,12 +51,27 @@ func TestOAuth2Application_LoadUser(t *testing.T) {
|
||||
|
||||
func TestUserEmails(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")()
|
||||
t.Run("GetUserEmailsByNames", func(t *testing.T) {
|
||||
// ignore none active user email
|
||||
// ignore not active user email
|
||||
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user9"}))
|
||||
assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user5"}))
|
||||
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "org7"}))
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
Email string
|
||||
UID int64
|
||||
}{
|
||||
{"UseR1@example.com", 1},
|
||||
{"user1-2@example.COM", 1},
|
||||
{"USER2@" + setting.Service.NoReplyAddress, 2},
|
||||
{"2+user2@" + setting.Service.NoReplyAddress, 2},
|
||||
{"2+oldUser2UsernameWhichDoesNotMatterForQuery@" + setting.Service.NoReplyAddress, 2},
|
||||
{"99999+badUser@" + setting.Service.NoReplyAddress, 0},
|
||||
{"user4@example.com", 4},
|
||||
{"no-such", 0},
|
||||
}
|
||||
t.Run("GetUsersByEmails", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")()
|
||||
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
|
||||
@@ -70,15 +85,27 @@ func TestUserEmails(t *testing.T) {
|
||||
require.NotNil(t, user)
|
||||
assert.Equal(t, uid, user.ID)
|
||||
}
|
||||
cases := []struct {
|
||||
Email string
|
||||
UID int64
|
||||
}{
|
||||
{"UseR1@example.com", 1},
|
||||
{"user1-2@example.COM", 1},
|
||||
{"USER2@" + setting.Service.NoReplyAddress, 2},
|
||||
{"user4@example.com", 4},
|
||||
{"no-such", 0},
|
||||
for _, c := range cases {
|
||||
t.Run(c.Email, func(t *testing.T) {
|
||||
testGetUserByEmail(t, c.Email, c.UID)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("NoReplyConflict", func(t *testing.T) {
|
||||
setting.Service.NoReplyAddress = "example.com"
|
||||
testGetUserByEmail(t, "user1-2@example.COM", 1)
|
||||
})
|
||||
})
|
||||
t.Run("GetUserByEmail", func(t *testing.T) {
|
||||
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
|
||||
user, err := user_model.GetUserByEmail(t.Context(), email)
|
||||
if uid == 0 {
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, user)
|
||||
} else {
|
||||
require.NotNil(t, user)
|
||||
assert.Equal(t, uid, user.ID)
|
||||
}
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.Email, func(t *testing.T) {
|
||||
@@ -126,7 +153,7 @@ func TestSearchUsers(t *testing.T) {
|
||||
|
||||
// test orgs
|
||||
testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) {
|
||||
opts.Type = user_model.UserTypeOrganization
|
||||
opts.Types = []user_model.UserType{user_model.UserTypeOrganization}
|
||||
testSuccess(opts, expectedOrgIDs)
|
||||
}
|
||||
|
||||
@@ -150,7 +177,7 @@ func TestSearchUsers(t *testing.T) {
|
||||
|
||||
// test users
|
||||
testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) {
|
||||
opts.Type = user_model.UserTypeIndividual
|
||||
opts.Types = []user_model.UserType{user_model.UserTypeIndividual}
|
||||
testSuccess(opts, expectedUserIDs)
|
||||
}
|
||||
|
||||
@@ -648,33 +675,36 @@ func TestGetInactiveUsers(t *testing.T) {
|
||||
func TestCanCreateRepo(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
|
||||
const noLimit = -1
|
||||
doerNormal := &user_model.User{}
|
||||
doerAdmin := &user_model.User{IsAdmin: true}
|
||||
doerActions := user_model.NewActionsUser()
|
||||
doerNormal := &user_model.User{ID: 2}
|
||||
doerAdmin := &user_model.User{ID: 1, IsAdmin: true}
|
||||
t.Run("NoGlobalLimit", func(t *testing.T) {
|
||||
setting.Repository.MaxCreationLimit = noLimit
|
||||
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerActions.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerAdmin.CanCreateRepoIn(doerActions))
|
||||
})
|
||||
|
||||
t.Run("GlobalLimit50", func(t *testing.T) {
|
||||
setting.Repository.MaxCreationLimit = 50
|
||||
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
|
||||
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user