1346 lines · 38.9 KB · Go
RawDownload
// Copyright 2014 The Gogs Authors. All rights reserved.// Copyright 2019 The Gitea Authors. All rights reserved.// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.// SPDX-License-Identifier: MIT package user import (	"context"	"crypto/subtle"	"encoding/hex"	"errors"	"fmt"	"net/mail"	"net/url"	"path/filepath"	"regexp"	"strings"	"time"	"unicode" 	_ "image/jpeg" // Needed for jpeg support 	"forgejo.org/models/auth"	"forgejo.org/models/db"	"forgejo.org/modules/auth/openid"	"forgejo.org/modules/auth/password/hash"	"forgejo.org/modules/base"	"forgejo.org/modules/container"	"forgejo.org/modules/git"	"forgejo.org/modules/log"	"forgejo.org/modules/optional"	"forgejo.org/modules/setting"	"forgejo.org/modules/structs"	"forgejo.org/modules/timeutil"	"forgejo.org/modules/util"	"forgejo.org/modules/validation" 	"golang.org/x/text/runes"	"golang.org/x/text/transform"	"golang.org/x/text/unicode/norm"	"xorm.io/builder") // UserType defines the user typetype UserType int //revive:disable-line:exported const (	// UserTypeIndividual defines an individual user	UserTypeIndividual UserType = iota // Historic reason to make it starts at 0. 	// UserTypeOrganization defines an organization	UserTypeOrganization // 1 	// UserTypeUserReserved reserves a (non-existing) user, i.e. to prevent a spam user from re-registering after being deleted, or to reserve the name until the user is actually created later on	UserTypeUserReserved // 2 	// UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved	UserTypeOrganizationReserved // 3 	// UserTypeBot defines a bot user	UserTypeBot // 4 	// UserTypeRemoteUser defines a remote user for federated users	UserTypeRemoteUser // 5) const (	// EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own	EmailNotificationsEnabled = "enabled"	// EmailNotificationsOnMention indicates that the user would like to be notified via email when mentioned.	EmailNotificationsOnMention = "onmention"	// EmailNotificationsDisabled indicates that the user would not like to be notified via email.	EmailNotificationsDisabled = "disabled"	// EmailNotificationsAndYourOwn indicates that the user would like to receive all email notifications and your own	EmailNotificationsAndYourOwn = "andyourown") // User represents the object of individual and member of organization.type User struct {	ID        int64  `xorm:"pk autoincr"`	LowerName string `xorm:"UNIQUE NOT NULL"`	Name      string `xorm:"UNIQUE NOT NULL"`	FullName  string	// Email is the primary email address (to be used for communication)	Email                        string `xorm:"NOT NULL"`	KeepEmailPrivate             bool	EmailNotificationsPreference string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'enabled'"`	Passwd                       string `xorm:"NOT NULL"`	PasswdHashAlgo               string `xorm:"NOT NULL DEFAULT 'argon2'"` 	// MustChangePassword is an attribute that determines if a user	// is to change their password after registration.	MustChangePassword bool `xorm:"NOT NULL DEFAULT false"` 	LoginType   auth.Type	LoginSource int64 `xorm:"NOT NULL DEFAULT 0"`	LoginName   string	Type        UserType	Location    string	Website     string	Pronouns    string	Rands       string `xorm:"VARCHAR(32)"`	Salt        string `xorm:"VARCHAR(32)"`	Language    string `xorm:"VARCHAR(5)"`	Description string 	CreatedUnix   timeutil.TimeStamp `xorm:"INDEX created"`	UpdatedUnix   timeutil.TimeStamp `xorm:"INDEX updated"`	LastLoginUnix timeutil.TimeStamp `xorm:"INDEX"` 	// Remember visibility choice for convenience, true for private	LastRepoVisibility bool	// Maximum repository creation limit, -1 means use global default	MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"` 	// IsActive true: primary email is activated, user can access Web UI and Git SSH.	// false: an inactive user can only log in Web UI for account operations (ex: activate the account by email), no other access.	IsActive bool `xorm:"INDEX"`	// the user is a Gitea admin, who can access all repositories and the admin pages.	IsAdmin bool	// true: the user is only allowed to see organizations/repositories that they has explicit rights to.	// (ex: in private Gitea instances user won't be allowed to see even organizations/repositories that are set as public)	IsRestricted bool `xorm:"NOT NULL DEFAULT false"` 	AllowGitHook            bool	AllowImportLocal        bool // Allow migrate repository by local path	AllowCreateOrganization bool `xorm:"DEFAULT true"` 	// true: the user is not allowed to log in Web UI. Git/SSH access could still be allowed (please refer to Git/SSH access related code/documents)	ProhibitLogin bool `xorm:"NOT NULL DEFAULT false"` 	// Avatar	Avatar          string `xorm:"VARCHAR(2048) NOT NULL"`	AvatarEmail     string `xorm:"NOT NULL"`	UseCustomAvatar bool 	// Counters	NumFollowers int	NumFollowing int `xorm:"NOT NULL DEFAULT 0"`	NumStars     int	NumRepos     int 	// For organization	NumTeams                  int	NumMembers                int	Visibility                structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`	RepoAdminChangeTeamAccess bool                `xorm:"NOT NULL DEFAULT false"` 	// Preferences	DiffViewStyle       string `xorm:"NOT NULL DEFAULT ''"`	Theme               string `xorm:"NOT NULL DEFAULT ''"`	KeepActivityPrivate bool   `xorm:"NOT NULL DEFAULT false"`	KeepPronounsPrivate bool   `xorm:"NOT NULL DEFAULT false"`	EnableRepoUnitHints bool   `xorm:"NOT NULL DEFAULT true"`} func init() {	db.RegisterModel(new(User))} // SearchOrganizationsOptions options to filter organizationstype SearchOrganizationsOptions struct {	db.ListOptions	All bool} func (u *User) LogString() string {	if u == nil {		return "<User nil>"	}	return fmt.Sprintf("<User %d:%s>", u.ID, u.Name)} // BeforeUpdate is invoked from XORM before updating this object.func (u *User) BeforeUpdate() {	if u.MaxRepoCreation < -1 {		u.MaxRepoCreation = -1	} 	// Organization does not need email	u.Email = strings.ToLower(u.Email)	if !u.IsOrganization() {		if len(u.AvatarEmail) == 0 {			u.AvatarEmail = u.Email		}	} 	u.LowerName = strings.ToLower(u.Name)	u.Location = base.TruncateString(u.Location, 255)	u.Website = base.TruncateString(u.Website, 255)	u.Description = base.TruncateString(u.Description, 255)} // AfterLoad is invoked from XORM after filling all the fields of this object.func (u *User) AfterLoad() {	if u.Theme == "" {		u.Theme = setting.UI.DefaultTheme	}} // SetLastLogin set time to last loginfunc (u *User) SetLastLogin() {	u.LastLoginUnix = timeutil.TimeStampNow()} // GetPlaceholderEmail returns an noreply emailfunc (u *User) GetPlaceholderEmail() string {	return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)} // GetEmail returns an noreply email, if the user has set to keep his// email address private, otherwise the primary email address.func (u *User) GetEmail() string {	if u.KeepEmailPrivate {		return u.GetPlaceholderEmail()	}	return u.Email} // GetAllUsers returns a slice of all individual users found in DB.func GetAllUsers(ctx context.Context) ([]*User, error) {	users := make([]*User, 0)	return users, db.GetEngine(ctx).OrderBy("id").In("type", UserTypeIndividual, UserTypeRemoteUser).Find(&users)} // GetAllAdmins returns a slice of all adminusers found in DB.func GetAllAdmins(ctx context.Context) ([]*User, error) {	users := make([]*User, 0)	return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).And("is_admin = ?", true).Find(&users)} // IsLocal returns true if user login type is LoginPlain.func (u *User) IsLocal() bool {	return u.LoginType <= auth.Plain} // IsOAuth2 returns true if user login type is LoginOAuth2.func (u *User) IsOAuth2() bool {	return u.LoginType == auth.OAuth2} // MaxCreationLimit returns the number of repositories a user is allowed to createfunc (u *User) MaxCreationLimit() int {	if u.MaxRepoCreation <= -1 {		return setting.Repository.MaxCreationLimit	}	return u.MaxRepoCreation} // CanCreateRepo returns if user login can create a repository// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revisedfunc (u *User) CanCreateRepo() bool {	if u.IsAdmin {		return true	}	if u.MaxRepoCreation <= -1 {		if setting.Repository.MaxCreationLimit <= -1 {			return true		}		return u.NumRepos < setting.Repository.MaxCreationLimit	}	return u.NumRepos < u.MaxRepoCreation} // CanCreateOrganization returns true if user can create organisation.func (u *User) CanCreateOrganization() bool {	return u.IsAdmin || (u.AllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation)} // CanEditGitHook returns true if user can edit Git hooks.func (u *User) CanEditGitHook() bool {	return !setting.DisableGitHooks && (u.IsAdmin || u.AllowGitHook)} // CanForkRepo returns if user login can fork a repository// It checks especially that the user can create repos, and potentially morefunc (u *User) CanForkRepo() bool {	if setting.Repository.AllowForkWithoutMaximumLimit {		return true	}	return u.CanCreateRepo()} // CanImportLocal returns true if user can migrate repository by local path.func (u *User) CanImportLocal() bool {	if !setting.ImportLocalPaths || u == nil {		return false	}	return u.IsAdmin || u.AllowImportLocal} // DashboardLink returns the user dashboard page link.func (u *User) DashboardLink() string {	if u.IsOrganization() {		return u.OrganisationLink() + "/dashboard"	}	return setting.AppSubURL + "/"} // HomeLink returns the user or organization home page link.func (u *User) HomeLink() string {	return setting.AppSubURL + "/" + url.PathEscape(u.Name)} // HTMLURL returns the user or organization's full link.func (u *User) HTMLURL() string {	return setting.AppURL + url.PathEscape(u.Name)} // OrganisationLink returns the organization sub page link.func (u *User) OrganisationLink() string {	return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)} // GenerateEmailAuthorizationCode generates an activation code based for the user for the specified purpose.// The standard expiry is ActiveCodeLives minutes.func (u *User) GenerateEmailAuthorizationCode(ctx context.Context, purpose auth.AuthorizationPurpose) (string, error) {	lookup, validator, err := auth.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(setting.Service.ActiveCodeLives)*60), purpose)	if err != nil {		return "", err	}	return lookup + ":" + validator, nil} // GetUserFollowers returns range of user's followers.func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {	sess := db.GetEngine(ctx).		Select("`user`.*").		Join("LEFT", "follow", "`user`.id=follow.user_id").		Where("follow.follow_id=?", u.ID).		And("`user`.type=?", UserTypeIndividual).		And(isUserVisibleToViewerCond(viewer)) 	if listOptions.Page > 0 {		sess = db.SetSessionPagination(sess, &listOptions) 		users := make([]*User, 0, listOptions.PageSize)		count, err := sess.FindAndCount(&users)		return users, count, err	} 	users := make([]*User, 0, 8)	count, err := sess.FindAndCount(&users)	return users, count, err} // GetUserFollowing returns range of user's following.func GetUserFollowing(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {	sess := db.GetEngine(ctx).		Select("`user`.*").		Join("LEFT", "follow", "`user`.id=follow.follow_id").		Where("follow.user_id=?", u.ID).		And("`user`.type IN (?, ?)", UserTypeIndividual, UserTypeOrganization).		And(isUserVisibleToViewerCond(viewer)) 	if listOptions.Page > 0 {		sess = db.SetSessionPagination(sess, &listOptions) 		users := make([]*User, 0, listOptions.PageSize)		count, err := sess.FindAndCount(&users)		return users, count, err	} 	users := make([]*User, 0, 8)	count, err := sess.FindAndCount(&users)	return users, count, err} // NewGitSig generates and returns the signature of given user.func (u *User) NewGitSig() *git.Signature {	return &git.Signature{		Name:  u.GitName(),		Email: u.GetEmail(),		When:  time.Now(),	}} // SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO// change passwd, salt and passwd_hash_algo fieldsfunc (u *User) SetPassword(passwd string) (err error) {	// Invalidate all authentication tokens for this user.	if err := auth.DeleteAuthTokenByUser(db.DefaultContext, u.ID); err != nil {		return err	} 	u.Salt = GetUserSalt()	if u.Passwd, err = hash.Parse(setting.PasswordHashAlgo).Hash(passwd, u.Salt); err != nil {		return err	}	u.PasswdHashAlgo = setting.PasswordHashAlgo 	return nil} // ValidatePassword checks if the given password matches the one belonging to the user.func (u *User) ValidatePassword(passwd string) bool {	return hash.Parse(u.PasswdHashAlgo).VerifyPassword(passwd, u.Passwd, u.Salt)} // IsPasswordSet checks if the password is set or left emptyfunc (u *User) IsPasswordSet() bool {	return len(u.Passwd) != 0} // IsOrganization returns true if user is actually a organization.func (u *User) IsOrganization() bool {	return u.Type == UserTypeOrganization} // IsIndividual returns true if user is actually a individual user.func (u *User) IsIndividual() bool {	return u.Type == UserTypeIndividual} func (u *User) IsUser() bool {	return u.Type == UserTypeIndividual || u.Type == UserTypeBot} // IsBot returns whether or not the user is of type botfunc (u *User) IsBot() bool {	return u.Type == UserTypeBot} func (u *User) IsRemote() bool {	return u.Type == UserTypeRemoteUser} // 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} var emailToReplacer = strings.NewReplacer(	"\n", "",	"\r", "",	"<", "",	">", "",	",", "",	":", "",	";", "",) // EmailTo returns a string suitable to be put into a e-mail `To:` header.func (u *User) EmailTo(overrideMail ...string) string {	sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName()) 	email := u.Email	if len(overrideMail) > 0 {		email = overrideMail[0]	} 	// should be an edge case but nice to have	if sanitizedDisplayName == email {		return email	} 	address, err := mail.ParseAddress(fmt.Sprintf("%s <%s>", sanitizedDisplayName, email))	if err != nil {		return email	} 	return address.String()} // GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,// returns username otherwise.func (u *User) GetDisplayName() string {	if setting.UI.DefaultShowFullName {		trimmed := strings.TrimSpace(u.FullName)		if len(trimmed) > 0 {			return trimmed		}	}	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)	}	return u.Name} // GetPronouns returns an empty string, if the user has set to keep his// pronouns private from non-logged in users, otherwise the pronouns// are returned.func (u *User) GetPronouns(signed bool) string {	if u.KeepPronounsPrivate && !signed {		return ""	}

Lines 1–500 of 1346.