Skip to content

Refactor user package #33423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion models/issues/reaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"context"
"fmt"
"strings"

"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
Expand Down Expand Up @@ -321,6 +322,11 @@ func valuesUser(m map[int64]*user_model.User) []*user_model.User {
return values
}

// newMigrationOriginalUser creates and returns a fake user for external user
func newMigrationOriginalUser(name string) *user_model.User {
return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)}
}

// LoadUsers loads reactions' all users
func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) {
if len(list) == 0 {
Expand All @@ -338,7 +344,7 @@ func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Reposit

for _, reaction := range list {
if reaction.OriginalAuthor != "" {
reaction.User = user_model.NewReplaceUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
reaction.User = newMigrationOriginalUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
} else if user, ok := userMaps[reaction.UserID]; ok {
reaction.User = user
} else {
Expand Down
5 changes: 1 addition & 4 deletions models/user/email_address.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"context"
"fmt"
"net/mail"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -153,8 +152,6 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
return err
}

var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

// ValidateEmail check if email is a valid & allowed address
func ValidateEmail(email string) error {
if err := validateEmailBasic(email); err != nil {
Expand Down Expand Up @@ -514,7 +511,7 @@ func validateEmailBasic(email string) error {
return ErrEmailInvalid{email}
}

if !emailRegexp.MatchString(email) {
if !globalVars().emailRegexp.MatchString(email) {
return ErrEmailCharIsNotSupported{email}
}

Expand Down
5 changes: 1 addition & 4 deletions models/user/openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ import (
"code.gitea.io/gitea/modules/util"
)

// ErrOpenIDNotExist openid is not known
var ErrOpenIDNotExist = util.NewNotExistErrorf("OpenID is unknown")

// UserOpenID is the list of all OpenID identities of a user.
// Since this is a middle table, name it OpenID is not suitable, so we ignore the lint here
type UserOpenID struct { //revive:disable-line:exported
Expand Down Expand Up @@ -99,7 +96,7 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
if err != nil {
return err
} else if deleted != 1 {
return ErrOpenIDNotExist
return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
}
Expand Down
59 changes: 37 additions & 22 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"unicode"

Expand Down Expand Up @@ -417,19 +418,9 @@ func (u *User) DisplayName() string {
return u.Name
}

var emailToReplacer = strings.NewReplacer(
"\n", "",
"\r", "",
"<", "",
">", "",
",", "",
":", "",
";", "",
)

// EmailTo returns a string suitable to be put into a e-mail `To:` header.
func (u *User) EmailTo() string {
sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName())
sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())

// should be an edge case but nice to have
if sanitizedDisplayName == u.Email {
Expand Down Expand Up @@ -526,28 +517,52 @@ func GetUserSalt() (string, error) {
if err != nil {
return "", err
}
// Returns a 32 bytes long string.
// Returns a 32-byte long string.
return hex.EncodeToString(rBytes), nil
}

// Note: The set of characters here can safely expand without a breaking change,
// but characters removed from this set can cause user account linking to break
var (
customCharsReplacement = strings.NewReplacer("Æ", "AE")
removeCharsRE = regexp.MustCompile("['`´]")
transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
)
type globalVarsStruct struct {
customCharsReplacement *strings.Replacer
removeCharsRE *regexp.Regexp
transformDiacritics transform.Transformer
replaceCharsHyphenRE *regexp.Regexp
emailToReplacer *strings.Replacer
emailRegexp *regexp.Regexp
}

var globalVars = sync.OnceValue(func() *globalVarsStruct {
return &globalVarsStruct{
// Note: The set of characters here can safely expand without a breaking change,
// but characters removed from this set can cause user account linking to break
customCharsReplacement: strings.NewReplacer("Æ", "AE"),

removeCharsRE: regexp.MustCompile("['`´]"),
transformDiacritics: transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC),
replaceCharsHyphenRE: regexp.MustCompile(`[\s~+]`),

emailToReplacer: strings.NewReplacer(
"\n", "",
"\r", "",
"<", "",
">", "",
",", "",
":", "",
";", "",
),
emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"),
}
})

// NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
// It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
func NormalizeUserName(s string) (string, error) {
vars := globalVars()
s, _, _ = strings.Cut(s, "@")
strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
strDiacriticsRemoved, n, err := transform.String(vars.transformDiacritics, vars.customCharsReplacement.Replace(s))
if err != nil {
return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n)
}
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
return vars.replaceCharsHyphenRE.ReplaceAllLiteralString(vars.removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
}

var (
Expand Down
31 changes: 12 additions & 19 deletions models/user/user_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ import (
)

const (
GhostUserID = -1
GhostUserName = "Ghost"
GhostUserLowerName = "ghost"
GhostUserID = -1
GhostUserName = "Ghost"
)

// NewGhostUser creates and returns a fake user for someone has deleted their account.
func NewGhostUser() *User {
return &User{
ID: GhostUserID,
Name: GhostUserName,
LowerName: GhostUserLowerName,
LowerName: strings.ToLower(GhostUserName),
}
}

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 {
Expand All @@ -32,20 +35,10 @@ func (u *User) IsGhost() bool {
return u.ID == GhostUserID && u.Name == GhostUserName
}

// NewReplaceUser creates and returns a fake user for external user
func NewReplaceUser(name string) *User {
return &User{
ID: 0,
Name: name,
LowerName: strings.ToLower(name),
}
}

const (
ActionsUserID = -2
ActionsUserName = "gitea-actions"
ActionsFullName = "Gitea Actions"
ActionsEmail = "teabot@gitea.io"
ActionsUserID = -2
ActionsUserName = "gitea-actions"
ActionsUserEmail = "teabot@gitea.io"
)

// NewActionsUser creates and returns a fake user for running the actions.
Expand All @@ -55,8 +48,8 @@ func NewActionsUser() *User {
Name: ActionsUserName,
LowerName: ActionsUserName,
IsActive: true,
FullName: ActionsFullName,
Email: ActionsEmail,
FullName: "Gitea Actions",
Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
Type: UserTypeIndividual,
Expand Down
3 changes: 1 addition & 2 deletions routers/web/user/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package user

import (
"strings"
"time"

"code.gitea.io/gitea/models/avatars"
Expand All @@ -27,7 +26,7 @@ func AvatarByUserName(ctx *context.Context) {
size := int(ctx.PathParamInt64("size"))

var user *user_model.User
if strings.ToLower(userName) != user_model.GhostUserLowerName {
if !user_model.IsGhostUserName(userName) {
var err error
if user, err = user_model.GetUserByName(ctx, userName); err != nil {
if user_model.IsErrUserNotExist(err) {
Expand Down
Loading