2021-11-24 17:49:20 +08:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2021-11-24 17:49:20 +08:00
package user
import (
"context"
"encoding/hex"
2026-04-27 00:33:09 +08:00
"errors"
2021-11-24 17:49:20 +08:00
"fmt"
2026-03-04 21:23:17 +08:00
"html/template"
2024-07-08 01:38:45 -07:00
"mime"
"net/mail"
2021-11-24 17:49:20 +08:00
"net/url"
"path/filepath"
2024-01-03 16:48:20 -08:00
"regexp"
2026-02-14 17:51:03 +01:00
"strconv"
2021-11-24 17:49:20 +08:00
"strings"
2025-01-29 07:14:35 +08:00
"sync"
2021-11-24 17:49:20 +08:00
"time"
2024-01-03 16:48:20 -08:00
"unicode"
2021-11-24 17:49:20 +08:00
_ "image/jpeg" // Needed for jpeg support
2022-01-02 21:12:35 +08:00
"code.gitea.io/gitea/models/auth"
2021-11-24 17:49:20 +08:00
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/auth/openid"
2023-02-19 07:35:20 +00:00
"code.gitea.io/gitea/modules/auth/password/hash"
2021-11-24 17:49:20 +08:00
"code.gitea.io/gitea/modules/base"
2023-05-25 10:06:27 +08:00
"code.gitea.io/gitea/modules/container"
2021-11-24 17:49:20 +08:00
"code.gitea.io/gitea/modules/git"
2026-03-04 21:23:17 +08:00
"code.gitea.io/gitea/modules/htmlutil"
2025-08-27 19:00:01 +08:00
"code.gitea.io/gitea/modules/httplib"
2021-11-24 17:49:20 +08:00
"code.gitea.io/gitea/modules/log"
2024-02-23 03:18:33 +01:00
"code.gitea.io/gitea/modules/optional"
2021-11-24 17:49:20 +08:00
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
2022-11-04 17:04:08 +08:00
"code.gitea.io/gitea/modules/validation"
2021-11-24 17:49:20 +08:00
2024-01-03 16:48:20 -08:00
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
2021-11-24 17:49:20 +08:00
"xorm.io/builder"
)
// UserType defines the user type
type 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
2024-11-20 20:55:32 -08:00
UserTypeOrganization // 1
2023-04-17 12:36:50 -04:00
2023-10-02 16:31:03 +08:00
// 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
2024-11-20 20:55:32 -08:00
UserTypeUserReserved // 2
2023-04-17 12:36:50 -04:00
// UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved
2024-11-20 20:55:32 -08:00
UserTypeOrganizationReserved // 3
2023-04-17 12:36:50 -04:00
// UserTypeBot defines a bot user
2024-11-20 20:55:32 -08:00
UserTypeBot // 4
2023-04-17 12:36:50 -04:00
// UserTypeRemoteUser defines a remote user for federated users
2024-11-20 20:55:32 -08:00
UserTypeRemoteUser // 5
2021-11-24 17:49:20 +08:00
)
const (
2022-07-28 16:30:12 +08:00
// EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own
2021-11-24 17:49:20 +08:00
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"
2023-04-21 10:27:32 -04:00
// EmailNotificationsAndYourOwn indicates that the user would like to receive all email notifications and your own
2022-07-28 16:30:12 +08:00
EmailNotificationsAndYourOwn = "andyourown"
2021-11-24 17:49:20 +08:00
)
// 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
2022-06-26 00:50:12 +02:00
// is to change their password after registration.
2021-11-24 17:49:20 +08:00
MustChangePassword bool ` xorm:"NOT NULL DEFAULT false" `
2022-01-02 21:12:35 +08:00
LoginType auth . Type
2021-11-24 17:49:20 +08:00
LoginSource int64 ` xorm:"NOT NULL DEFAULT 0" `
LoginName string
Type UserType
Location string
Website string
2022-01-04 15:13:52 +00:00
Rands string ` xorm:"VARCHAR(32)" `
Salt string ` xorm:"VARCHAR(32)" `
2021-11-24 17:49:20 +08:00
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" `
}
2024-09-09 17:05:16 -04:00
// Meta defines the meta information of a user, to be stored in the K/V table
type Meta struct {
// Store the initial registration of the user, to aid in spam prevention
// Ensure that one IP isn't creating many accounts (following mediawiki approach)
InitialIP string
InitialUserAgent string
}
2021-11-24 17:49:20 +08:00
func init ( ) {
db . RegisterModel ( new ( User ) )
}
// SearchOrganizationsOptions options to filter organizations
type SearchOrganizationsOptions struct {
db . ListOptions
All bool
}
2023-05-22 06:35:11 +08:00
func ( u * User ) LogString ( ) string {
2021-12-16 19:01:14 +00:00
if u == nil {
2023-05-22 06:35:11 +08:00
return "<User nil>"
2021-12-16 19:01:14 +00:00
}
2023-05-22 06:35:11 +08:00
return fmt . Sprintf ( "<User %d:%s>" , u . ID , u . Name )
2021-11-24 17:49:20 +08:00
}
// BeforeUpdate is invoked from XORM before updating this object.
func ( u * User ) BeforeUpdate ( ) {
if u . MaxRepoCreation < - 1 {
u . MaxRepoCreation = - 1
}
2024-12-27 19:16:23 +08:00
// FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field
2026-02-28 20:23:20 +01:00
// This trick could be removed in new releases to display the user inputted email as-is.
2021-11-24 17:49:20 +08:00
u . Email = strings . ToLower ( u . Email )
if ! u . IsOrganization ( ) {
if len ( u . AvatarEmail ) == 0 {
u . AvatarEmail = u . Email
}
}
u . LowerName = strings . ToLower ( u . Name )
2024-12-26 11:56:03 +08:00
u . Location = util . TruncateRunes ( u . Location , 255 )
u . Website = util . TruncateRunes ( u . Website , 255 )
u . Description = util . TruncateRunes ( u . Description , 255 )
2021-11-24 17:49:20 +08:00
}
// 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 login
func ( u * User ) SetLastLogin ( ) {
u . LastLoginUnix = timeutil . TimeStampNow ( )
}
2023-07-31 03:18:38 +02:00
// GetPlaceholderEmail returns an noreply email
func ( u * User ) GetPlaceholderEmail ( ) string {
2026-02-16 02:42:22 +01:00
return fmt . Sprintf ( "%d+%s@%s" , u . ID , u . LowerName , setting . Service . NoReplyAddress )
2023-07-31 03:18:38 +02:00
}
2025-01-30 09:24:57 +08:00
// GetEmail returns a noreply email, if the user has set to keep his
2021-11-24 17:49:20 +08:00
// email address private, otherwise the primary email address.
func ( u * User ) GetEmail ( ) string {
if u . KeepEmailPrivate {
2023-07-31 03:18:38 +02:00
return u . GetPlaceholderEmail ( )
2021-11-24 17:49:20 +08:00
}
return u . Email
}
// GetAllUsers returns a slice of all individual users found in DB.
2023-09-14 19:09:32 +02:00
func GetAllUsers ( ctx context . Context ) ( [ ] * User , error ) {
2021-11-24 17:49:20 +08:00
users := make ( [ ] * User , 0 )
2023-09-14 19:09:32 +02:00
return users , db . GetEngine ( ctx ) . OrderBy ( "id" ) . Where ( "type = ?" , UserTypeIndividual ) . Find ( & users )
2021-11-24 17:49:20 +08:00
}
// IsLocal returns true if user login type is LoginPlain.
func ( u * User ) IsLocal ( ) bool {
2022-01-02 21:12:35 +08:00
return u . LoginType <= auth . Plain
2021-11-24 17:49:20 +08:00
}
// IsOAuth2 returns true if user login type is LoginOAuth2.
func ( u * User ) IsOAuth2 ( ) bool {
2022-01-02 21:12:35 +08:00
return u . LoginType == auth . OAuth2
2021-11-24 17:49:20 +08:00
}
// MaxCreationLimit returns the number of repositories a user is allowed to create
func ( u * User ) MaxCreationLimit ( ) int {
if u . MaxRepoCreation <= - 1 {
return setting . Repository . MaxCreationLimit
}
return u . MaxRepoCreation
}
2025-04-08 06:45:31 +00:00
// CanCreateRepoIn checks whether the doer(u) can create a repository in the owner
2025-10-22 13:12:31 +02:00
// 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.
2025-04-08 06:45:31 +00:00
func ( u * User ) CanCreateRepoIn ( owner * User ) bool {
2025-10-22 13:12:31 +02:00
if u . ID <= 0 || owner . ID <= 0 {
return false // fake user like Ghost or Actions user
}
2021-11-24 17:49:20 +08:00
if u . IsAdmin {
return true
}
2025-04-08 06:45:31 +00:00
const noLimit = - 1
if owner . MaxRepoCreation == noLimit {
if setting . Repository . MaxCreationLimit == noLimit {
2021-11-24 17:49:20 +08:00
return true
}
2025-04-08 06:45:31 +00:00
return owner . NumRepos < setting . Repository . MaxCreationLimit
2021-11-24 17:49:20 +08:00
}
2025-04-08 06:45:31 +00:00
return owner . NumRepos < owner . MaxRepoCreation
2021-11-24 17:49:20 +08:00
}
// 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 )
}
2025-04-08 06:45:31 +00:00
// CanForkRepoIn ONLY checks repository count limit
func ( u * User ) CanForkRepoIn ( owner * User ) bool {
2022-12-28 05:21:14 +08:00
if setting . Repository . AllowForkWithoutMaximumLimit {
return true
}
2025-04-08 06:45:31 +00:00
return u . CanCreateRepoIn ( owner )
2022-12-28 05:21:14 +08:00
}
2021-11-24 17:49:20 +08:00
// 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 + "/"
}
2026-05-11 10:36:07 -07:00
func ( u * User ) SettingsLink ( ) string {
if u . IsOrganization ( ) {
return u . OrganisationLink ( ) + "/settings"
}
return setting . AppSubURL + "/user/settings"
}
2021-11-24 17:49:20 +08:00
// 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.
2025-08-27 19:00:01 +08:00
func ( u * User ) HTMLURL ( ctx context . Context ) string {
return httplib . MakeAbsoluteURL ( ctx , u . HomeLink ( ) )
2021-11-24 17:49:20 +08:00
}
// OrganisationLink returns the organization sub page link.
func ( u * User ) OrganisationLink ( ) string {
return setting . AppSubURL + "/org/" + url . PathEscape ( u . Name )
}
// GetUserFollowers returns range of user's followers.
2022-07-05 16:47:45 +01:00
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" ) .
2021-11-24 17:49:20 +08:00
Where ( "follow.follow_id=?" , u . ID ) .
2023-04-17 12:36:50 -04:00
And ( "`user`.type=?" , UserTypeIndividual ) .
2022-07-05 16:47:45 +01:00
And ( isUserVisibleToViewerCond ( viewer ) )
2021-11-24 17:49:20 +08:00
2024-11-24 17:56:50 -08:00
if listOptions . Page > 0 {
2021-11-24 17:49:20 +08:00
sess = db . SetSessionPagination ( sess , & listOptions )
users := make ( [ ] * User , 0 , listOptions . PageSize )
2022-07-05 16:47:45 +01:00
count , err := sess . FindAndCount ( & users )
return users , count , err
2021-11-24 17:49:20 +08:00
}
users := make ( [ ] * User , 0 , 8 )
2022-07-05 16:47:45 +01:00
count , err := sess . FindAndCount ( & users )
return users , count , err
2021-11-24 17:49:20 +08:00
}
// GetUserFollowing returns range of user's following.
2022-07-05 16:47:45 +01:00
func GetUserFollowing ( ctx context . Context , u , viewer * User , listOptions db . ListOptions ) ( [ ] * User , int64 , error ) {
2023-07-07 00:29:24 +05:30
sess := db . GetEngine ( ctx ) .
2022-07-05 16:47:45 +01:00
Select ( "`user`.*" ) .
Join ( "LEFT" , "follow" , "`user`.id=follow.follow_id" ) .
2021-11-24 17:49:20 +08:00
Where ( "follow.user_id=?" , u . ID ) .
2023-04-30 04:13:58 +09:00
And ( "`user`.type IN (?, ?)" , UserTypeIndividual , UserTypeOrganization ) .
2022-07-05 16:47:45 +01:00
And ( isUserVisibleToViewerCond ( viewer ) )
2021-11-24 17:49:20 +08:00
2024-11-24 17:56:50 -08:00
if listOptions . Page > 0 {
2021-11-24 17:49:20 +08:00
sess = db . SetSessionPagination ( sess , & listOptions )
users := make ( [ ] * User , 0 , listOptions . PageSize )
2022-07-05 16:47:45 +01:00
count , err := sess . FindAndCount ( & users )
return users , count , err
2021-11-24 17:49:20 +08:00
}
users := make ( [ ] * User , 0 , 8 )
2022-07-05 16:47:45 +01:00
count , err := sess . FindAndCount ( & users )
return users , count , err
2021-11-24 17:49:20 +08:00
}
// 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 fields
func ( u * User ) SetPassword ( passwd string ) ( err error ) {
if u . Salt , err = GetUserSalt ( ) ; err != nil {
return err
}
2023-02-19 07:35:20 +00:00
if u . Passwd , err = hash . Parse ( setting . PasswordHashAlgo ) . Hash ( passwd , u . Salt ) ; err != nil {
2022-01-04 15:13:52 +00:00
return err
}
2021-11-24 17:49:20 +08:00
u . PasswdHashAlgo = setting . PasswordHashAlgo
return nil
}
2023-02-19 07:35:20 +00:00
// ValidatePassword checks if the given password matches the one belonging to the user.
2021-11-24 17:49:20 +08:00
func ( u * User ) ValidatePassword ( passwd string ) bool {
2023-02-19 07:35:20 +00:00
return hash . Parse ( u . PasswdHashAlgo ) . VerifyPassword ( passwd , u . Passwd , u . Salt )
2021-11-24 17:49:20 +08:00
}
// IsPasswordSet checks if the password is set or left empty
2025-02-07 09:41:55 +01:00
// TODO: It's better to clarify the "password" behavior for different types (individual, bot)
2021-11-24 17:49:20 +08:00
func ( u * User ) IsPasswordSet ( ) bool {
2025-02-07 09:41:55 +01:00
return u . Passwd != ""
2021-11-24 17:49:20 +08:00
}
2025-02-07 09:41:55 +01:00
// IsOrganization returns true if user is actually an organization.
2021-11-24 17:49:20 +08:00
func ( u * User ) IsOrganization ( ) bool {
return u . Type == UserTypeOrganization
}
2023-03-11 00:18:20 +09:00
// IsIndividual returns true if user is actually a individual user.
func ( u * User ) IsIndividual ( ) bool {
return u . Type == UserTypeIndividual
}
2025-02-07 09:41:55 +01:00
// IsTypeBot returns whether the user is of type bot
func ( u * User ) IsTypeBot ( ) bool {
return u . Type == UserTypeBot
2024-10-08 17:51:09 +08:00
}
2025-02-07 09:41:55 +01:00
// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access)
func ( u * User ) IsTokenAccessAllowed ( ) bool {
return u . Type == UserTypeIndividual || u . Type == UserTypeBot
2023-05-26 19:58:11 +03:00
}
2024-07-08 01:38:45 -07:00
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
func ( u * User ) EmailTo ( ) string {
2025-01-29 07:14:35 +08:00
sanitizedDisplayName := globalVars ( ) . emailToReplacer . Replace ( u . DisplayName ( ) )
2024-07-08 01:38:45 -07:00
// should be an edge case but nice to have
if sanitizedDisplayName == u . Email {
return u . Email
}
to := fmt . Sprintf ( "%s <%s>" , sanitizedDisplayName , u . Email )
add , err := mail . ParseAddress ( to )
if err != nil {
return u . Email
}
return fmt . Sprintf ( "%s <%s>" , mime . QEncoding . Encode ( "utf-8" , add . Name ) , add . Address )
}
2026-03-04 21:23:17 +08:00
// 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.
2021-11-24 17:49:20 +08:00
func ( u * User ) GetDisplayName ( ) string {
if setting . UI . DefaultShowFullName {
2026-03-04 21:23:17 +08:00
fullName := strings . TrimSpace ( u . FullName )
if fullName != "" {
return fullName
2021-11-24 17:49:20 +08:00
}
}
return u . Name
}
2026-03-04 21:23:17 +08:00
// 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
2024-02-03 00:41:27 +00:00
}
2026-03-04 21:23:17 +08:00
return htmlutil . HTMLFormat ( ` <a class="muted" href="%s" data-tooltip-content="%s">%s</a> ` , u . HomeLink ( ) , displayTooltip , displayName )
2024-02-03 00:41:27 +00:00
}
2021-11-24 17:49:20 +08:00
func gitSafeName ( name string ) string {
return strings . TrimSpace ( strings . NewReplacer ( "\n" , "" , "<" , "" , ">" , "" ) . Replace ( name ) )
}
// GitName returns a git safe name
func ( u * User ) GitName ( ) string {
gitName := gitSafeName ( u . FullName )
if len ( gitName ) > 0 {
return gitName
}
// Although u.Name should be safe if created in our system
// LDAP users may have bad names
gitName = gitSafeName ( u . Name )
if len ( gitName ) > 0 {
return gitName
}
// Totally pathological name so it's got to be:
return fmt . Sprintf ( "user-%d" , u . ID )
}
2026-02-16 00:47:02 +01:00
// IsMailable checks if a user is eligible to receive emails.
// System users like Ghost and Gitea Actions are excluded.
2021-11-24 17:49:20 +08:00
func ( u * User ) IsMailable ( ) bool {
2026-02-16 00:47:02 +01:00
return u . IsActive && ! u . IsGiteaActions ( ) && ! u . IsGhost ( )
2021-11-24 17:49:20 +08:00
}
2025-01-25 22:36:47 +08:00
// IsUserExist checks if given username exist,
// the username should be non-cased unique.
2022-05-20 22:08:52 +08:00
// If uid is presented, then check will rule out that one,
2025-01-25 22:36:47 +08:00
// it is used when update a username in settings page.
2022-05-20 22:08:52 +08:00
func IsUserExist ( ctx context . Context , uid int64 , name string ) ( bool , error ) {
2021-11-24 17:49:20 +08:00
if len ( name ) == 0 {
return false , nil
}
2022-05-20 22:08:52 +08:00
return db . GetEngine ( ctx ) .
2021-11-24 17:49:20 +08:00
Where ( "id!=?" , uid ) .
Get ( & User { LowerName : strings . ToLower ( name ) } )
}
2025-01-25 22:36:47 +08:00
// SaltByteLength as of the beginning of 2022, it is recommended to use at least
2022-01-04 15:13:52 +00:00
// 64 bits of salt, but NIST is already recommending to use to 128 bits.
// (16 bytes = 16 * 8 = 128 bits)
const SaltByteLength = 16
2021-11-24 17:49:20 +08:00
// GetUserSalt returns a random user salt token.
func GetUserSalt ( ) ( string , error ) {
2022-01-26 12:10:10 +08:00
rBytes , err := util . CryptoRandomBytes ( SaltByteLength )
2022-01-04 15:13:52 +00:00
if err != nil {
return "" , err
}
2025-01-29 07:14:35 +08:00
// Returns a 32-byte long string.
2022-01-04 15:13:52 +00:00
return hex . EncodeToString ( rBytes ) , nil
2021-11-24 17:49:20 +08:00
}
2025-01-29 07:14:35 +08:00
type globalVarsStruct struct {
customCharsReplacement * strings . Replacer
removeCharsRE * regexp . Regexp
transformDiacritics transform . Transformer
replaceCharsHyphenRE * regexp . Regexp
emailToReplacer * strings . Replacer
emailRegexp * regexp . Regexp
2025-01-30 07:33:50 +08:00
systemUserNewFuncs map [ int64 ] func ( ) * User
2025-01-29 07:14:35 +08:00
}
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])?)*$" ) ,
2025-01-30 07:33:50 +08:00
systemUserNewFuncs : map [ int64 ] func ( ) * User {
GhostUserID : NewGhostUser ,
ActionsUserID : NewActionsUser ,
} ,
2025-01-29 07:14:35 +08:00
}
} )
2024-01-03 16:48:20 -08:00
2024-04-25 19:22:32 +08:00
// 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
2024-01-03 16:48:20 -08:00
func NormalizeUserName ( s string ) ( string , error ) {
2025-01-29 07:14:35 +08:00
vars := globalVars ( )
2024-04-25 19:22:32 +08:00
s , _ , _ = strings . Cut ( s , "@" )
2025-01-29 07:14:35 +08:00
strDiacriticsRemoved , n , err := transform . String ( vars . transformDiacritics , vars . customCharsReplacement . Replace ( s ) )
2024-01-03 16:48:20 -08:00
if err != nil {
2024-04-25 19:22:32 +08:00
return "" , fmt . Errorf ( "failed to normalize the string of provided username %q at position %d" , s , n )
2024-01-03 16:48:20 -08:00
}
2025-01-29 07:14:35 +08:00
return vars . replaceCharsHyphenRE . ReplaceAllLiteralString ( vars . removeCharsRE . ReplaceAllLiteralString ( strDiacriticsRemoved , "" ) , "-" ) , nil
2024-01-03 16:48:20 -08:00
}
2021-11-24 17:49:20 +08:00
var (
reservedUsernames = [ ] string {
"." ,
".." ,
".well-known" ,
2024-10-10 17:04:42 +08:00
"api" , // gitea api
"metrics" , // prometheus metrics api
"v2" , // container registry api
"assets" , // static asset files
"attachments" , // issue attachments
"avatar" , // avatar by email hash
"avatars" , // user avatars by file name
"repo-avatars" ,
2021-11-24 17:49:20 +08:00
"captcha" ,
2024-10-10 17:04:42 +08:00
"login" , // oauth2 login
"org" , // org create/manage, or "/org/{org}", BUT if an org is named as "invite" then it goes wrong
"repo" , // repo create/migrate, etc
"user" , // user login/activate/settings, etc
2021-11-24 17:49:20 +08:00
"explore" ,
"issues" ,
2024-10-10 17:04:42 +08:00
"pulls" ,
2021-11-24 17:49:20 +08:00
"milestones" ,
"notifications" ,
2024-10-10 17:04:42 +08:00
"favicon.ico" ,
"manifest.json" , // web app manifests
"robots.txt" , // search engine robots
"sitemap.xml" , // search engine sitemap
"ssh_info" , // agit info
2022-03-31 06:02:13 +02:00
"swagger.v1.json" ,
2024-10-10 17:04:42 +08:00
"ghost" , // reserved name for deleted users (id: -1)
"gitea-actions" , // gitea builtin user (id: -2)
2021-11-24 17:49:20 +08:00
}
2024-10-10 17:04:42 +08:00
// These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc.
// DO NOT add any new stuff! The paths with these names are processed by `/{username}` handler (UsernameSubRoute) manually.
2023-04-10 22:14:16 +02:00
reservedUserPatterns = [ ] string { "*.keys" , "*.gpg" , "*.rss" , "*.atom" , "*.png" }
2021-11-24 17:49:20 +08:00
)
// IsUsableUsername returns an error when a username is reserved
func IsUsableUsername ( name string ) error {
// Validate username make sure it satisfies requirement.
2022-11-04 17:04:08 +08:00
if ! validation . IsValidUsername ( name ) {
2021-11-24 17:49:20 +08:00
// Note: usually this error is normally caught up earlier in the UI
return db . ErrNameCharsNotAllowed { Name : name }
}
return db . IsUsableName ( reservedUsernames , reservedUserPatterns , name )
}
// CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation
type CreateUserOverwriteOptions struct {
2024-02-23 03:18:33 +01:00
KeepEmailPrivate optional . Option [ bool ]
2022-04-29 21:38:11 +02:00
Visibility * structs . VisibleType
2024-02-23 03:18:33 +01:00
AllowCreateOrganization optional . Option [ bool ]
2022-04-29 21:38:11 +02:00
EmailNotificationsPreference * string
MaxRepoCreation * int
Theme * string
2024-02-23 03:18:33 +01:00
IsRestricted optional . Option [ bool ]
IsActive optional . Option [ bool ]
2021-11-24 17:49:20 +08:00
}
// CreateUser creates record of a new user.
2024-09-09 17:05:16 -04:00
func CreateUser ( ctx context . Context , u * User , meta * Meta , overwriteDefault ... * CreateUserOverwriteOptions ) ( err error ) {
return createUser ( ctx , u , meta , false , overwriteDefault ... )
2024-03-05 13:55:47 +08:00
}
// AdminCreateUser is used by admins to manually create users
2024-09-09 17:05:16 -04:00
func AdminCreateUser ( ctx context . Context , u * User , meta * Meta , overwriteDefault ... * CreateUserOverwriteOptions ) ( err error ) {
return createUser ( ctx , u , meta , true , overwriteDefault ... )
2024-03-05 13:55:47 +08:00
}
// createUser creates record of a new user.
2024-09-09 17:05:16 -04:00
func createUser ( ctx context . Context , u * User , meta * Meta , createdByAdmin bool , overwriteDefault ... * CreateUserOverwriteOptions ) ( err error ) {
2021-11-24 17:49:20 +08:00
if err = IsUsableUsername ( u . Name ) ; err != nil {
return err
}
// set system defaults
u . KeepEmailPrivate = setting . Service . DefaultKeepEmailPrivate
u . Visibility = setting . Service . DefaultUserVisibilityMode
u . AllowCreateOrganization = setting . Service . DefaultAllowCreateOrganization && ! setting . Admin . DisableRegularOrgCreation
u . EmailNotificationsPreference = setting . Admin . DefaultEmailNotification
u . MaxRepoCreation = - 1
u . Theme = setting . UI . DefaultTheme
2022-04-29 21:38:11 +02:00
u . IsRestricted = setting . Service . DefaultUserIsRestricted
u . IsActive = ! ( setting . Service . RegisterEmailConfirm || setting . Service . RegisterManualConfirm )
2021-11-24 17:49:20 +08:00
2023-02-16 17:32:01 +01:00
// Ensure consistency of the dates.
if u . UpdatedUnix < u . CreatedUnix {
u . UpdatedUnix = u . CreatedUnix
}
2021-11-24 17:49:20 +08:00
// overwrite defaults if set
if len ( overwriteDefault ) != 0 && overwriteDefault [ 0 ] != nil {
2022-04-29 21:38:11 +02:00
overwrite := overwriteDefault [ 0 ]
2024-02-23 03:18:33 +01:00
if overwrite . KeepEmailPrivate . Has ( ) {
u . KeepEmailPrivate = overwrite . KeepEmailPrivate . Value ( )
2022-04-29 21:38:11 +02:00
}
if overwrite . Visibility != nil {
u . Visibility = * overwrite . Visibility
}
2024-02-23 03:18:33 +01:00
if overwrite . AllowCreateOrganization . Has ( ) {
u . AllowCreateOrganization = overwrite . AllowCreateOrganization . Value ( )
2022-04-29 21:38:11 +02:00
}
if overwrite . EmailNotificationsPreference != nil {
u . EmailNotificationsPreference = * overwrite . EmailNotificationsPreference
}
if overwrite . MaxRepoCreation != nil {
u . MaxRepoCreation = * overwrite . MaxRepoCreation
}
if overwrite . Theme != nil {
u . Theme = * overwrite . Theme
}
2024-02-23 03:18:33 +01:00
if overwrite . IsRestricted . Has ( ) {
u . IsRestricted = overwrite . IsRestricted . Value ( )
2022-04-29 21:38:11 +02:00
}
2024-02-23 03:18:33 +01:00
if overwrite . IsActive . Has ( ) {
u . IsActive = overwrite . IsActive . Value ( )
2022-04-29 21:38:11 +02:00
}
2021-11-24 17:49:20 +08:00
}
2022-03-15 01:39:54 +08:00
// validate data
2023-05-24 15:30:55 +08:00
if err := ValidateUser ( u ) ; err != nil {
2022-03-15 01:39:54 +08:00
return err
}
2024-03-05 13:55:47 +08:00
if createdByAdmin {
if err := ValidateEmailForAdmin ( u . Email ) ; err != nil {
return err
}
} else {
if err := ValidateEmail ( u . Email ) ; err != nil {
return err
}
2022-03-15 01:39:54 +08:00
}
2025-09-09 20:15:01 -07:00
return db . WithTx ( ctx , func ( ctx context . Context ) error {
isExist , err := IsUserExist ( ctx , 0 , u . Name )
if err != nil {
return err
} else if isExist {
return ErrUserAlreadyExist { u . Name }
2021-11-24 17:49:20 +08:00
}
2025-09-09 20:15:01 -07:00
isExist , err = IsEmailUsed ( ctx , u . Email )
if err != nil {
2024-02-04 14:29:09 +01:00
return err
2025-09-09 20:15:01 -07:00
} else if isExist {
return ErrEmailAlreadyUsed {
Email : u . Email ,
}
2024-02-04 14:29:09 +01:00
}
2021-11-24 17:49:20 +08:00
2025-09-09 20:15:01 -07:00
// prepare for database
2021-11-24 17:49:20 +08:00
2025-09-09 20:15:01 -07:00
u . LowerName = strings . ToLower ( u . Name )
u . AvatarEmail = u . Email
if u . Rands , err = GetUserSalt ( ) ; err != nil {
return err
}
if u . Passwd != "" {
if err = u . SetPassword ( u . Passwd ) ; err != nil {
return err
}
} else {
u . Salt = ""
u . PasswdHashAlgo = ""
}
2021-11-24 17:49:20 +08:00
2025-09-09 20:15:01 -07:00
// save changes to database
2021-11-24 17:49:20 +08:00
2025-09-09 20:15:01 -07:00
if err = DeleteUserRedirect ( ctx , u . Name ) ; err != nil {
2024-09-09 17:05:16 -04:00
return err
}
2025-09-09 20:15:01 -07:00
if u . CreatedUnix == 0 {
// Caller expects auto-time for creation & update timestamps.
err = db . Insert ( ctx , u )
} else {
// Caller sets the timestamps themselves. They are responsible for ensuring
// both `CreatedUnix` and `UpdatedUnix` are set appropriately.
_ , err = db . GetEngine ( ctx ) . NoAutoTime ( ) . Insert ( u )
2024-09-09 17:05:16 -04:00
}
2025-09-09 20:15:01 -07:00
if err != nil {
2024-09-09 17:05:16 -04:00
return err
}
2025-09-09 20:15:01 -07:00
if setting . RecordUserSignupMetadata {
// insert initial IP and UserAgent
if err = SetUserSetting ( ctx , u . ID , SignupIP , meta . InitialIP ) ; err != nil {
return err
}
// trim user agent string to a reasonable length, if necessary
userAgent := strings . TrimSpace ( meta . InitialUserAgent )
if len ( userAgent ) > 255 {
userAgent = userAgent [ : 255 ]
}
if err = SetUserSetting ( ctx , u . ID , SignupUserAgent , userAgent ) ; err != nil {
return err
}
}
2021-11-24 17:49:20 +08:00
2025-09-09 20:15:01 -07:00
// insert email address
return db . Insert ( ctx , & EmailAddress {
UID : u . ID ,
Email : u . Email ,
LowerEmail : strings . ToLower ( u . Email ) ,
IsActivated : u . IsActive ,
IsPrimary : true ,
} )
} )
2021-11-24 17:49:20 +08:00
}
2024-12-20 10:05:29 -08:00
// ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error.
type ErrDeleteLastAdminUser struct {
UID int64
}
// IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser.
func IsErrDeleteLastAdminUser ( err error ) bool {
_ , ok := err . ( ErrDeleteLastAdminUser )
return ok
}
func ( err ErrDeleteLastAdminUser ) Error ( ) string {
return fmt . Sprintf ( "can not delete the last admin user [uid: %d]" , err . UID )
}
2024-01-15 15:51:43 +09:00
// IsLastAdminUser check whether user is the last admin
func IsLastAdminUser ( ctx context . Context , user * User ) bool {
2024-03-02 16:42:31 +01:00
if user . IsAdmin && CountUsers ( ctx , & CountUserFilter { IsAdmin : optional . Some ( true ) } ) <= 1 {
2024-01-15 15:51:43 +09:00
return true
}
return false
}
2022-05-02 15:35:45 +02:00
// CountUserFilter represent optional filters for CountUsers
type CountUserFilter struct {
LastLoginSince * int64
2024-03-02 16:42:31 +01:00
IsAdmin optional . Option [ bool ]
2025-05-07 20:00:53 +02:00
IsActive optional . Option [ bool ]
2021-11-24 17:49:20 +08:00
}
2025-06-22 02:48:06 +08:00
// HasUsers checks whether there are any users in the database, or only one user exists.
func HasUsers ( ctx context . Context ) ( ret struct {
HasAnyUser , HasOnlyOneUser bool
} , err error ,
) {
res , err := db . GetEngine ( ctx ) . Table ( & User { } ) . Cols ( "id" ) . Limit ( 2 ) . Query ( )
if err != nil {
return ret , fmt . Errorf ( "error checking user existence: %w" , err )
}
ret . HasAnyUser = len ( res ) != 0
ret . HasOnlyOneUser = len ( res ) == 1
return ret , nil
}
2021-11-24 17:49:20 +08:00
// CountUsers returns number of users.
2023-09-14 19:09:32 +02:00
func CountUsers ( ctx context . Context , opts * CountUserFilter ) int64 {
return countUsers ( ctx , opts )
2022-05-02 15:35:45 +02:00
}
func countUsers ( ctx context . Context , opts * CountUserFilter ) int64 {
2024-01-15 15:51:43 +09:00
sess := db . GetEngine ( ctx )
cond := builder . NewCond ( )
cond = cond . And ( builder . Eq { "type" : UserTypeIndividual } )
if opts != nil {
if opts . LastLoginSince != nil {
cond = cond . And ( builder . Gte { "last_login_unix" : * opts . LastLoginSince } )
}
2022-05-02 15:35:45 +02:00
2024-03-02 16:42:31 +01:00
if opts . IsAdmin . Has ( ) {
cond = cond . And ( builder . Eq { "is_admin" : opts . IsAdmin . Value ( ) } )
2024-01-15 15:51:43 +09:00
}
2025-05-07 20:00:53 +02:00
if opts . IsActive . Has ( ) {
cond = cond . And ( builder . Eq { "is_active" : opts . IsActive . Value ( ) } )
}
2024-01-15 15:51:43 +09:00
}
count , err := sess . Where ( cond ) . Count ( new ( User ) )
if err != nil {
log . Error ( "user.countUsers: %v" , err )
2022-05-02 15:35:45 +02:00
}
return count
2021-11-24 17:49:20 +08:00
}
// GetVerifyUser get user by verify code
2023-09-14 19:09:32 +02:00
func GetVerifyUser ( ctx context . Context , code string ) ( user * User ) {
2021-11-24 17:49:20 +08:00
if len ( code ) <= base . TimeLimitCodeLength {
return nil
}
// use tail hex username query user
hexStr := code [ base . TimeLimitCodeLength : ]
if b , err := hex . DecodeString ( hexStr ) ; err == nil {
2023-09-14 19:09:32 +02:00
if user , err = GetUserByName ( ctx , string ( b ) ) ; user != nil {
2021-11-24 17:49:20 +08:00
return user
}
log . Error ( "user.getVerifyUser: %v" , err )
}
return nil
}
2024-12-27 19:16:23 +08:00
type TimeLimitCodePurpose string
const (
TimeLimitCodeActivateAccount TimeLimitCodePurpose = "activate_account"
TimeLimitCodeActivateEmail TimeLimitCodePurpose = "activate_email"
TimeLimitCodeResetPassword TimeLimitCodePurpose = "reset_password"
)
type TimeLimitCodeOptions struct {
Purpose TimeLimitCodePurpose
NewEmail string
}
func makeTimeLimitCodeHashData ( opts * TimeLimitCodeOptions , u * User ) string {
return fmt . Sprintf ( "%s|%d|%s|%s|%s|%s" , opts . Purpose , u . ID , strings . ToLower ( util . IfZero ( opts . NewEmail , u . Email ) ) , u . LowerName , u . Passwd , u . Rands )
}
// GenerateUserTimeLimitCode generates a time-limit code based on user information and given e-mail.
// TODO: need to use cache or db to store it to make sure a code can only be consumed once
func GenerateUserTimeLimitCode ( opts * TimeLimitCodeOptions , u * User ) string {
data := makeTimeLimitCodeHashData ( opts , u )
code := base . CreateTimeLimitCode ( data , setting . Service . ActiveCodeLives , time . Now ( ) , nil )
code += hex . EncodeToString ( [ ] byte ( u . LowerName ) ) // Add tail hex username
return code
}
// VerifyUserTimeLimitCode verifies the time-limit code
func VerifyUserTimeLimitCode ( ctx context . Context , opts * TimeLimitCodeOptions , code string ) ( user * User ) {
2023-09-14 19:09:32 +02:00
if user = GetVerifyUser ( ctx , code ) ; user != nil {
2021-11-24 17:49:20 +08:00
// time limit code
prefix := code [ : base . TimeLimitCodeLength ]
2024-12-27 19:16:23 +08:00
data := makeTimeLimitCodeHashData ( opts , user )
2024-05-20 23:12:50 +08:00
if base . VerifyTimeLimitCode ( time . Now ( ) , data , setting . Service . ActiveCodeLives , prefix ) {
2021-11-24 17:49:20 +08:00
return user
}
}
return nil
}
2023-05-24 15:30:55 +08:00
// ValidateUser check if user is valid to insert / update into database
func ValidateUser ( u * User , cols ... string ) error {
if len ( cols ) == 0 || util . SliceContainsString ( cols , "visibility" , true ) {
if ! setting . Service . AllowedUserVisibilityModesSlice . IsAllowedVisibility ( u . Visibility ) && ! u . IsOrganization ( ) {
return fmt . Errorf ( "visibility Mode not allowed: %s" , u . Visibility . String ( ) )
}
2021-11-24 17:49:20 +08:00
}
2023-05-24 15:30:55 +08:00
return nil
2021-11-24 17:49:20 +08:00
}
// UpdateUserCols update user according special columns
func UpdateUserCols ( ctx context . Context , u * User , cols ... string ) error {
2023-05-24 15:30:55 +08:00
if err := ValidateUser ( u , cols ... ) ; err != nil {
2021-11-24 17:49:20 +08:00
return err
}
2022-05-20 22:08:52 +08:00
_ , err := db . GetEngine ( ctx ) . ID ( u . ID ) . Cols ( cols ... ) . Update ( u )
2021-11-24 17:49:20 +08:00
return err
}
2025-08-20 20:57:42 -07:00
// UpdateUserColsNoAutoTime update user according special columns
func UpdateUserColsNoAutoTime ( ctx context . Context , u * User , cols ... string ) error {
if err := ValidateUser ( u , cols ... ) ; err != nil {
return err
}
_ , err := db . GetEngine ( ctx ) . ID ( u . ID ) . Cols ( cols ... ) . NoAutoTime ( ) . Update ( u )
return err
}
2021-11-24 17:49:20 +08:00
// GetInactiveUsers gets all inactive users
func GetInactiveUsers ( ctx context . Context , olderThan time . Duration ) ( [ ] * User , error ) {
2024-11-20 20:55:32 -08:00
cond := builder . And (
builder . Eq { "is_active" : false } ,
builder . Or ( // only plain user
builder . Eq { "`type`" : UserTypeIndividual } ,
builder . Eq { "`type`" : UserTypeUserReserved } ,
) ,
)
2021-11-24 17:49:20 +08:00
if olderThan > 0 {
cond = cond . And ( builder . Lt { "created_unix" : time . Now ( ) . Add ( - olderThan ) . Unix ( ) } )
}
users := make ( [ ] * User , 0 , 10 )
return users , db . GetEngine ( ctx ) .
Where ( cond ) .
Find ( & users )
}
// UserPath returns the path absolute path of user repositories.
func UserPath ( userName string ) string { //revive:disable-line:exported
2025-10-04 01:21:26 +08:00
return filepath . Join ( setting . RepoRootPath , filepath . Clean ( strings . ToLower ( userName ) ) )
2021-11-24 17:49:20 +08:00
}
// GetUserByID returns the user object by given ID if exists.
2022-12-03 10:48:26 +08:00
func GetUserByID ( ctx context . Context , id int64 ) ( * User , error ) {
2022-05-20 22:08:52 +08:00
u := new ( User )
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( u )
if err != nil {
return nil , err
} else if ! has {
2024-02-14 18:50:10 +01:00
return nil , ErrUserNotExist { UID : id }
2022-05-20 22:08:52 +08:00
}
return u , nil
2021-11-24 17:49:20 +08:00
}
2023-05-25 10:06:27 +08:00
// GetUserByIDs returns the user objects by given IDs if exists.
func GetUserByIDs ( ctx context . Context , ids [ ] int64 ) ( [ ] * User , error ) {
2024-05-31 20:10:11 +08:00
if len ( ids ) == 0 {
return nil , nil
}
2023-05-25 10:06:27 +08:00
users := make ( [ ] * User , 0 , len ( ids ) )
err := db . GetEngine ( ctx ) . In ( "id" , ids ) .
Table ( "user" ) .
Find ( & users )
return users , err
}
2026-04-27 00:33:09 +08:00
// 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 ) {
2025-01-30 07:33:50 +08:00
if id < 0 {
if newFunc , ok := globalVars ( ) . systemUserNewFuncs [ id ] ; ok {
2026-04-27 00:33:09 +08:00
u = newFunc ( )
}
}
if u == nil {
u , err = GetUserByID ( ctx , id )
if errors . Is ( err , util . ErrNotExist ) {
u = NewGhostUser ( )
} else if err != nil {
return 0 , nil , err
2025-01-30 07:33:50 +08:00
}
2023-01-31 09:45:19 +08:00
}
2026-04-27 00:33:09 +08:00
return u . ID , u , nil
2023-01-31 09:45:19 +08:00
}
2025-01-30 07:33:50 +08:00
// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
2023-05-25 10:06:27 +08:00
func GetPossibleUserByIDs ( ctx context . Context , ids [ ] int64 ) ( [ ] * User , error ) {
uniqueIDs := container . SetOf ( ids ... )
users := make ( [ ] * User , 0 , len ( ids ) )
_ = uniqueIDs . Remove ( 0 )
2025-01-30 07:33:50 +08:00
for systemUID , newFunc := range globalVars ( ) . systemUserNewFuncs {
if uniqueIDs . Remove ( systemUID ) {
users = append ( users , newFunc ( ) )
}
2023-05-25 10:06:27 +08:00
}
res , err := GetUserByIDs ( ctx , uniqueIDs . Values ( ) )
if err != nil {
return nil , err
}
users = append ( users , res ... )
return users , nil
}
2026-05-07 08:43:45 -07:00
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 )
2021-11-24 17:49:20 +08:00
}
2026-05-07 08:43:45 -07:00
has , err := sess . Get ( u )
2021-11-24 17:49:20 +08:00
if err != nil {
return nil , err
} else if ! has {
2024-02-14 18:50:10 +01:00
return nil , ErrUserNotExist { Name : name }
2021-11-24 17:49:20 +08:00
}
return u , nil
}
2026-05-07 08:43:45 -07:00
// 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 )
}
2021-11-24 17:49:20 +08:00
// GetUserEmailsByNames returns a list of e-mails corresponds to names of users
// that have their email notifications set to enabled or onmention.
2022-05-20 22:08:52 +08:00
func GetUserEmailsByNames ( ctx context . Context , names [ ] string ) [ ] string {
2021-11-24 17:49:20 +08:00
mails := make ( [ ] string , 0 , len ( names ) )
for _ , name := range names {
2022-05-20 22:08:52 +08:00
u , err := GetUserByName ( ctx , name )
2021-11-24 17:49:20 +08:00
if err != nil {
continue
}
2024-02-04 14:29:09 +01:00
if u . IsMailable ( ) && u . EmailNotificationsPreference != EmailNotificationsDisabled {
2021-11-24 17:49:20 +08:00
mails = append ( mails , u . Email )
}
}
return mails
}
2025-01-30 07:33:50 +08:00
// GetMailableUsersByIDs gets users from ids, but only if they can receive mails
func GetMailableUsersByIDs ( ctx context . Context , ids [ ] int64 , isMention bool ) ( [ ] * User , error ) {
2021-11-24 17:49:20 +08:00
if len ( ids ) == 0 {
return nil , nil
}
ous := make ( [ ] * User , 0 , len ( ids ) )
if isMention {
2022-11-19 09:12:33 +01:00
return ous , db . GetEngine ( ctx ) .
In ( "id" , ids ) .
2021-11-24 17:49:20 +08:00
Where ( "`type` = ?" , UserTypeIndividual ) .
And ( "`prohibit_login` = ?" , false ) .
And ( "`is_active` = ?" , true ) .
2022-07-28 16:30:12 +08:00
In ( "`email_notifications_preference`" , EmailNotificationsEnabled , EmailNotificationsOnMention , EmailNotificationsAndYourOwn ) .
2021-11-24 17:49:20 +08:00
Find ( & ous )
}
2022-11-19 09:12:33 +01:00
return ous , db . GetEngine ( ctx ) .
In ( "id" , ids ) .
2021-11-24 17:49:20 +08:00
Where ( "`type` = ?" , UserTypeIndividual ) .
And ( "`prohibit_login` = ?" , false ) .
And ( "`is_active` = ?" , true ) .
2022-07-28 16:30:12 +08:00
In ( "`email_notifications_preference`" , EmailNotificationsEnabled , EmailNotificationsAndYourOwn ) .
2021-11-24 17:49:20 +08:00
Find ( & ous )
}
// GetUserIDsByNames returns a slice of ids corresponds to names.
2022-11-19 09:12:33 +01:00
func GetUserIDsByNames ( ctx context . Context , names [ ] string , ignoreNonExistent bool ) ( [ ] int64 , error ) {
2021-11-24 17:49:20 +08:00
ids := make ( [ ] int64 , 0 , len ( names ) )
for _ , name := range names {
2022-11-19 09:12:33 +01:00
u , err := GetUserByName ( ctx , name )
2021-11-24 17:49:20 +08:00
if err != nil {
if ignoreNonExistent {
continue
}
2024-04-22 13:48:42 +02:00
return nil , err
2021-11-24 17:49:20 +08:00
}
ids = append ( ids , u . ID )
}
return ids , nil
}
// GetUsersBySource returns a list of Users for a login source
2023-09-14 19:09:32 +02:00
func GetUsersBySource ( ctx context . Context , s * auth . Source ) ( [ ] * User , error ) {
2021-11-24 17:49:20 +08:00
var users [ ] * User
2023-09-14 19:09:32 +02:00
err := db . GetEngine ( ctx ) . Where ( "login_type = ? AND login_source = ?" , s . Type , s . ID ) . Find ( & users )
2021-11-24 17:49:20 +08:00
return users , err
}
// UserCommit represents a commit with validation of user.
type UserCommit struct { //revive:disable-line:exported
User * User
* git . Commit
}
// ValidateCommitWithEmail check if author's e-mail of commit is corresponding to a user.
2023-02-15 21:37:34 +08:00
func ValidateCommitWithEmail ( ctx context . Context , c * git . Commit ) * User {
2021-11-24 17:49:20 +08:00
if c . Author == nil {
return nil
}
2023-02-15 21:37:34 +08:00
u , err := GetUserByEmail ( ctx , c . Author . Email )
2021-11-24 17:49:20 +08:00
if err != nil {
return nil
}
return u
}
// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
2025-02-14 00:05:55 -08:00
func ValidateCommitsWithEmails ( ctx context . Context , oldCommits [ ] * git . Commit ) ( [ ] * UserCommit , error ) {
2021-11-24 17:49:20 +08:00
var (
newCommits = make ( [ ] * UserCommit , 0 , len ( oldCommits ) )
2025-02-14 00:05:55 -08:00
emailSet = make ( container . Set [ string ] )
2021-11-24 17:49:20 +08:00
)
for _ , c := range oldCommits {
if c . Author != nil {
2025-02-14 00:05:55 -08:00
emailSet . Add ( c . Author . Email )
2021-11-24 17:49:20 +08:00
}
2025-02-14 00:05:55 -08:00
}
emailUserMap , err := GetUsersByEmails ( ctx , emailSet . Values ( ) )
if err != nil {
return nil , err
}
2021-11-24 17:49:20 +08:00
2025-02-14 00:05:55 -08:00
for _ , c := range oldCommits {
2025-06-08 02:30:36 +08:00
user := emailUserMap . GetByEmail ( c . Author . Email ) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
2021-11-24 17:49:20 +08:00
newCommits = append ( newCommits , & UserCommit {
2025-02-14 00:05:55 -08:00
User : user ,
2021-11-24 17:49:20 +08:00
Commit : c ,
} )
}
2025-02-14 00:05:55 -08:00
return newCommits , nil
}
2025-06-08 02:30:36 +08:00
type EmailUserMap struct {
m map [ string ] * User
}
func ( eum * EmailUserMap ) GetByEmail ( email string ) * User {
return eum . m [ strings . ToLower ( email ) ]
}
func GetUsersByEmails ( ctx context . Context , emails [ ] string ) ( * EmailUserMap , error ) {
2025-02-14 00:05:55 -08:00
if len ( emails ) == 0 {
2026-02-16 10:57:18 +01:00
return nil , nil //nolint:nilnil // return nil when there are no emails to look up
2025-02-14 00:05:55 -08:00
}
needCheckEmails := make ( container . Set [ string ] )
needCheckUserNames := make ( container . Set [ string ] )
2026-02-14 17:51:03 +01:00
needCheckUserIDs := make ( container . Set [ int64 ] )
2025-07-11 03:03:36 +08:00
noReplyAddressSuffix := "@" + strings . ToLower ( setting . Service . NoReplyAddress )
2025-02-14 00:05:55 -08:00
for _ , email := range emails {
2025-07-11 03:03:36 +08:00
emailLower := strings . ToLower ( email )
2026-02-14 17:51:03 +01:00
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 )
}
2025-02-14 00:05:55 -08:00
}
}
emailAddresses := make ( [ ] * EmailAddress , 0 , len ( needCheckEmails ) )
if err := db . GetEngine ( ctx ) . In ( "lower_email" , needCheckEmails . Values ( ) ) .
And ( "is_activated=?" , true ) .
Find ( & emailAddresses ) ; err != nil {
return nil , err
}
userIDs := make ( container . Set [ int64 ] )
for _ , email := range emailAddresses {
userIDs . Add ( email . UID )
}
results := make ( map [ string ] * User , len ( emails ) )
2025-04-09 09:34:38 -07:00
if len ( userIDs ) > 0 {
users , err := GetUsersMapByIDs ( ctx , userIDs . Values ( ) )
if err != nil {
return nil , err
}
for _ , email := range emailAddresses {
user := users [ email . UID ]
if user != nil {
2025-06-08 02:30:36 +08:00
results [ email . LowerEmail ] = user
2025-02-15 15:16:19 -08:00
}
2025-02-14 00:05:55 -08:00
}
}
2025-02-15 15:16:19 -08:00
2026-02-14 17:51:03 +01:00
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
}
2025-02-14 00:05:55 -08:00
}
2026-02-14 17:51:03 +01:00
usersByName := make ( map [ string ] * User )
for _ , user := range usersByIDs {
usersByName [ user . LowerName ] = user
2025-02-14 00:05:55 -08:00
}
2026-02-14 17:51:03 +01:00
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
}
}
2025-06-08 02:30:36 +08:00
return & EmailUserMap { results } , nil
2021-11-24 17:49:20 +08:00
}
2026-02-16 02:42:22 +01:00
// parseLocalPartToNameID attempts to unparse local-part of email that's in format id+user
2026-02-14 17:51:03 +01:00
// returns user and id if possible
func parseLocalPartToNameID ( localPart string ) ( string , int64 ) {
var id int64
2026-02-16 02:42:22 +01:00
idstr , name , hasPlus := strings . Cut ( localPart , "+" )
2026-02-14 17:51:03 +01:00
if hasPlus {
id , _ = strconv . ParseInt ( idstr , 10 , 64 )
2026-02-16 02:42:22 +01:00
} else {
name = idstr
2026-02-14 17:51:03 +01:00
}
return name , id
}
2021-11-24 17:49:20 +08:00
// GetUserByEmail returns the user object by given e-mail if exists.
2023-02-15 21:37:34 +08:00
func GetUserByEmail ( ctx context . Context , email string ) ( * User , error ) {
2021-11-24 17:49:20 +08:00
if len ( email ) == 0 {
2024-02-14 18:50:10 +01:00
return nil , ErrUserNotExist { Name : email }
2021-11-24 17:49:20 +08:00
}
email = strings . ToLower ( email )
// Otherwise, check in alternative list for activated email addresses
2022-02-21 22:20:34 +08:00
emailAddress := & EmailAddress { LowerEmail : email , IsActivated : true }
has , err := db . GetEngine ( ctx ) . Get ( emailAddress )
2021-11-24 17:49:20 +08:00
if err != nil {
return nil , err
}
if has {
2022-12-03 10:48:26 +08:00
return GetUserByID ( ctx , emailAddress . UID )
2021-11-24 17:49:20 +08:00
}
// Finally, if email address is the protected email address:
2026-02-14 17:51:03 +01:00
if localPart , ok := strings . CutSuffix ( email , strings . ToLower ( "@" + setting . Service . NoReplyAddress ) ) ; ok {
name , id := parseLocalPartToNameID ( localPart )
if id != 0 {
return GetUserByID ( ctx , id )
2021-11-24 17:49:20 +08:00
}
2026-05-07 08:43:45 -07:00
return GetIndividualUserByName ( ctx , name )
2021-11-24 17:49:20 +08:00
}
2024-02-14 18:50:10 +01:00
return nil , ErrUserNotExist { Name : email }
2021-11-24 17:49:20 +08:00
}
2026-04-03 20:19:04 +08:00
func GetIndividualUser ( ctx context . Context , user * User ) ( bool , error ) {
2026-05-07 08:43:45 -07:00
// FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future
2026-04-03 20:19:04 +08:00
has , err := db . GetEngine ( ctx ) . Get ( user )
if has && user . Type != UserTypeIndividual {
has = false
}
return has , err
2021-11-24 17:49:20 +08:00
}
// GetUserByOpenID returns the user object by given OpenID if exists.
2023-09-14 19:09:32 +02:00
func GetUserByOpenID ( ctx context . Context , uri string ) ( * User , error ) {
2021-11-24 17:49:20 +08:00
if len ( uri ) == 0 {
2024-02-14 18:50:10 +01:00
return nil , ErrUserNotExist { Name : uri }
2021-11-24 17:49:20 +08:00
}
uri , err := openid . Normalize ( uri )
if err != nil {
return nil , err
}
log . Trace ( "Normalized OpenID URI: " + uri )
// Otherwise, check in openid table
oid := & UserOpenID { }
2023-09-14 19:09:32 +02:00
has , err := db . GetEngine ( ctx ) . Where ( "uri=?" , uri ) . Get ( oid )
2021-11-24 17:49:20 +08:00
if err != nil {
return nil , err
}
if has {
2023-09-14 19:09:32 +02:00
return GetUserByID ( ctx , oid . UID )
2021-11-24 17:49:20 +08:00
}
2024-02-14 18:50:10 +01:00
return nil , ErrUserNotExist { Name : uri }
2021-11-24 17:49:20 +08:00
}
// GetAdminUser returns the first administrator
2023-06-29 18:03:20 +08:00
func GetAdminUser ( ctx context . Context ) ( * User , error ) {
2021-11-24 17:49:20 +08:00
var admin User
2023-06-29 18:03:20 +08:00
has , err := db . GetEngine ( ctx ) .
2023-01-18 17:57:16 +01:00
Where ( "is_admin=?" , true ) .
Asc ( "id" ) . // Reliably get the admin with the lowest ID.
Get ( & admin )
2021-11-24 17:49:20 +08:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrUserNotExist { }
}
return & admin , nil
}
2022-03-29 14:29:02 +08:00
2022-07-05 16:47:45 +01:00
func isUserVisibleToViewerCond ( viewer * User ) builder . Cond {
if viewer != nil && viewer . IsAdmin {
return builder . NewCond ( )
}
if viewer == nil || viewer . IsRestricted {
return builder . Eq {
"`user`.visibility" : structs . VisibleTypePublic ,
}
}
return builder . Neq {
"`user`.visibility" : structs . VisibleTypePrivate ,
} . Or (
2024-01-15 14:43:14 +09:00
// viewer self
builder . Eq { "`user`.id" : viewer . ID } ,
2023-04-30 04:13:58 +09:00
// viewer's following
2022-07-05 16:47:45 +01:00
builder . In ( "`user`.id" ,
builder .
Select ( "`follow`.user_id" ) .
From ( "follow" ) .
Where ( builder . Eq { "`follow`.follow_id" : viewer . ID } ) ) ,
2023-04-30 04:13:58 +09:00
// viewer's org user
2022-07-05 16:47:45 +01:00
builder . In ( "`user`.id" ,
builder .
Select ( "`team_user`.uid" ) .
From ( "team_user" ) .
2023-04-30 04:13:58 +09:00
Join ( "INNER" , "`team_user` AS t2" , "`team_user`.org_id = `t2`.org_id" ) .
2022-07-05 16:47:45 +01:00
Where ( builder . Eq { "`t2`.uid" : viewer . ID } ) ) ,
2023-04-30 04:13:58 +09:00
// viewer's org
2022-07-05 16:47:45 +01:00
builder . In ( "`user`.id" ,
builder .
2023-04-30 04:13:58 +09:00
Select ( "`team_user`.org_id" ) .
2022-07-05 16:47:45 +01:00
From ( "team_user" ) .
2023-04-30 04:13:58 +09:00
Where ( builder . Eq { "`team_user`.uid" : viewer . ID } ) ) )
2022-07-05 16:47:45 +01:00
}
2022-03-29 14:29:02 +08:00
// IsUserVisibleToViewer check if viewer is able to see user profile
2022-05-20 22:08:52 +08:00
func IsUserVisibleToViewer ( ctx context . Context , u , viewer * User ) bool {
2022-09-20 09:59:20 +02:00
if viewer != nil && ( viewer . IsAdmin || viewer . ID == u . ID ) {
2022-03-29 14:29:02 +08:00
return true
}
switch u . Visibility {
case structs . VisibleTypePublic :
return true
case structs . VisibleTypeLimited :
if viewer == nil || viewer . IsRestricted {
return false
}
return true
case structs . VisibleTypePrivate :
if viewer == nil || viewer . IsRestricted {
return false
}
2024-03-04 09:16:03 +01:00
// If they follow - they see each other
2023-09-16 16:39:12 +02:00
follower := IsFollowing ( ctx , u . ID , viewer . ID )
2022-03-29 14:29:02 +08:00
if follower {
return true
}
// Now we need to check if they in some organization together
2022-05-20 22:08:52 +08:00
count , err := db . GetEngine ( ctx ) . Table ( "team_user" ) .
2022-03-29 14:29:02 +08:00
Where (
builder . And (
builder . Eq { "uid" : viewer . ID } ,
builder . Or (
builder . Eq { "org_id" : u . ID } ,
builder . In ( "org_id" ,
builder . Select ( "org_id" ) .
From ( "team_user" , "t2" ) .
Where ( builder . Eq { "uid" : u . ID } ) ) ) ) ) .
Count ( )
if err != nil {
return false
}
2022-09-20 09:59:20 +02:00
if count == 0 {
2022-03-29 14:29:02 +08:00
// No common organization
return false
}
// they are in an organization together
return true
}
return false
}
2022-07-15 00:00:10 +08:00
2022-08-25 10:31:57 +08:00
// CountWrongUserType count OrgUser who have wrong type
2023-09-14 19:09:32 +02:00
func CountWrongUserType ( ctx context . Context ) ( int64 , error ) {
return db . GetEngine ( ctx ) . Where ( builder . Eq { "type" : 0 } . And ( builder . Neq { "num_teams" : 0 } ) ) . Count ( new ( User ) )
2022-08-25 10:31:57 +08:00
}
// FixWrongUserType fix OrgUser who have wrong type
2023-09-14 19:09:32 +02:00
func FixWrongUserType ( ctx context . Context ) ( int64 , error ) {
return db . GetEngine ( ctx ) . Where ( builder . Eq { "type" : 0 } . And ( builder . Neq { "num_teams" : 0 } ) ) . Cols ( "type" ) . NoAutoTime ( ) . Update ( & User { Type : 1 } )
2022-08-25 10:31:57 +08:00
}
2022-07-15 00:00:10 +08:00
func GetOrderByName ( ) string {
if setting . UI . DefaultShowFullName {
return "full_name, name"
}
return "name"
}
2024-03-29 11:05:41 -04:00
2024-07-09 12:36:31 -05:00
// IsFeatureDisabledWithLoginType checks if a user features are disabled, taking into account the login type of the
2024-03-29 11:05:41 -04:00
// user if applicable
2024-07-09 12:36:31 -05:00
func IsFeatureDisabledWithLoginType ( user * User , features ... string ) bool {
2024-03-29 11:05:41 -04:00
// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
2024-07-09 12:36:31 -05:00
if user != nil && user . LoginType > auth . Plain {
return setting . Admin . ExternalUserDisableFeatures . Contains ( features ... )
}
return setting . Admin . UserDisabledFeatures . Contains ( features ... )
2024-03-29 11:05:41 -04:00
}
// DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type
// of the user if applicable
func DisabledFeaturesWithLoginType ( user * User ) * container . Set [ string ] {
// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType
if user != nil && user . LoginType > auth . Plain {
return & setting . Admin . ExternalUserDisableFeatures
}
return & setting . Admin . UserDisabledFeatures
}