mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Backport #27000 by @wxiaoguang This PR reduces the complexity of the system setting system. It only needs one line to introduce a new option, and the option can be used anywhere out-of-box. It is still high-performant (and more performant) because the config values are cached in the config system.  Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -5,18 +5,20 @@ package avatars | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync/atomic" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" |  | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/cache" | 	"code.gitea.io/gitea/modules/cache" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  |  | ||||||
|  | 	"strk.kbt.io/projects/go/libravatar" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -36,24 +38,54 @@ func init() { | |||||||
| 	db.RegisterModel(new(EmailHash)) | 	db.RegisterModel(new(EmailHash)) | ||||||
| } | } | ||||||
|  |  | ||||||
| var ( | type avatarSettingStruct struct { | ||||||
| 	defaultAvatarLink string | 	defaultAvatarLink string | ||||||
| 	once              sync.Once | 	gravatarSource    string | ||||||
| ) | 	gravatarSourceURL *url.URL | ||||||
|  | 	libravatar        *libravatar.Libravatar | ||||||
|  | } | ||||||
|  |  | ||||||
| // DefaultAvatarLink the default avatar link | var avatarSettingAtomic atomic.Pointer[avatarSettingStruct] | ||||||
| func DefaultAvatarLink() string { |  | ||||||
| 	once.Do(func() { | func loadAvatarSetting() (*avatarSettingStruct, error) { | ||||||
|  | 	s := avatarSettingAtomic.Load() | ||||||
|  | 	if s == nil || s.gravatarSource != setting.GravatarSource { | ||||||
|  | 		s = &avatarSettingStruct{} | ||||||
| 		u, err := url.Parse(setting.AppSubURL) | 		u, err := url.Parse(setting.AppSubURL) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("Can not parse AppSubURL: %v", err) | 			return nil, fmt.Errorf("unable to parse AppSubURL: %w", err) | ||||||
| 			return |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") | 		u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") | ||||||
| 		defaultAvatarLink = u.String() | 		s.defaultAvatarLink = u.String() | ||||||
| 	}) |  | ||||||
| 	return defaultAvatarLink | 		s.gravatarSourceURL, err = url.Parse(setting.GravatarSource) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("unable to parse GravatarSource %q: %w", setting.GravatarSource, err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		s.libravatar = libravatar.New() | ||||||
|  | 		if s.gravatarSourceURL.Scheme == "https" { | ||||||
|  | 			s.libravatar.SetUseHTTPS(true) | ||||||
|  | 			s.libravatar.SetSecureFallbackHost(s.gravatarSourceURL.Host) | ||||||
|  | 		} else { | ||||||
|  | 			s.libravatar.SetUseHTTPS(false) | ||||||
|  | 			s.libravatar.SetFallbackHost(s.gravatarSourceURL.Host) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		avatarSettingAtomic.Store(s) | ||||||
|  | 	} | ||||||
|  | 	return s, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DefaultAvatarLink the default avatar link | ||||||
|  | func DefaultAvatarLink() string { | ||||||
|  | 	a, err := loadAvatarSetting() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Failed to loadAvatarSetting: %v", err) | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 	return a.defaultAvatarLink | ||||||
| } | } | ||||||
|  |  | ||||||
| // HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/ | // HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/ | ||||||
| @@ -76,7 +108,11 @@ func GetEmailForHash(md5Sum string) (string, error) { | |||||||
| // LibravatarURL returns the URL for the given email. Slow due to the DNS lookup. | // LibravatarURL returns the URL for the given email. Slow due to the DNS lookup. | ||||||
| // This function should only be called if a federated avatar service is enabled. | // This function should only be called if a federated avatar service is enabled. | ||||||
| func LibravatarURL(email string) (*url.URL, error) { | func LibravatarURL(email string) (*url.URL, error) { | ||||||
| 	urlStr, err := system_model.LibravatarService.FromEmail(email) | 	a, err := loadAvatarSetting() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	urlStr, err := a.libravatar.FromEmail(email) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err) | 		log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err) | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -153,15 +189,13 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final | |||||||
| 		return DefaultAvatarLink() | 		return DefaultAvatarLink() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, | 	avatarSetting, err := loadAvatarSetting() | ||||||
| 		setting.GetDefaultDisableGravatar(), | 	if err != nil { | ||||||
| 	) | 		return DefaultAvatarLink() | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar, | 	enableFederatedAvatar := setting.Config().Picture.EnableFederatedAvatar.Value(ctx) | ||||||
| 		setting.GetDefaultEnableFederatedAvatar(disableGravatar)) | 	if enableFederatedAvatar { | ||||||
|  |  | ||||||
| 	var err error |  | ||||||
| 	if enableFederatedAvatar && system_model.LibravatarService != nil { |  | ||||||
| 		emailHash := saveEmailHash(email) | 		emailHash := saveEmailHash(email) | ||||||
| 		if final { | 		if final { | ||||||
| 			// for final link, we can spend more time on slow external query | 			// for final link, we can spend more time on slow external query | ||||||
| @@ -179,9 +213,10 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final | |||||||
| 		return urlStr | 		return urlStr | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	disableGravatar := setting.Config().Picture.DisableGravatar.Value(ctx) | ||||||
| 	if !disableGravatar { | 	if !disableGravatar { | ||||||
| 		// copy GravatarSourceURL, because we will modify its Path. | 		// copy GravatarSourceURL, because we will modify its Path. | ||||||
| 		avatarURLCopy := *system_model.GravatarSourceURL | 		avatarURLCopy := *avatarSetting.gravatarSourceURL | ||||||
| 		avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email)) | 		avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email)) | ||||||
| 		return generateRecognizedAvatarURL(avatarURLCopy, size) | 		return generateRecognizedAvatarURL(avatarURLCopy, size) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" | 	system_model "code.gitea.io/gitea/models/system" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting/config" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| @@ -17,19 +18,16 @@ import ( | |||||||
| const gravatarSource = "https://secure.gravatar.com/avatar/" | const gravatarSource = "https://secure.gravatar.com/avatar/" | ||||||
|  |  | ||||||
| func disableGravatar(t *testing.T) { | func disableGravatar(t *testing.T) { | ||||||
| 	err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureEnableFederatedAvatar, "false") | 	err := system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.EnableFederatedAvatar.DynKey(): "false"}) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	err = system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "true") | 	err = system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.DisableGravatar.DynKey(): "true"}) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	system_model.LibravatarService = nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func enableGravatar(t *testing.T) { | func enableGravatar(t *testing.T) { | ||||||
| 	err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "false") | 	err := system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.DisableGravatar.DynKey(): "false"}) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	setting.GravatarSource = gravatarSource | 	setting.GravatarSource = gravatarSource | ||||||
| 	err = system_model.Init(db.DefaultContext) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestHashEmail(t *testing.T) { | func TestHashEmail(t *testing.T) { | ||||||
| @@ -47,10 +45,12 @@ func TestSizedAvatarLink(t *testing.T) { | |||||||
| 	setting.AppSubURL = "/testsuburl" | 	setting.AppSubURL = "/testsuburl" | ||||||
|  |  | ||||||
| 	disableGravatar(t) | 	disableGravatar(t) | ||||||
|  | 	config.GetDynGetter().InvalidateCache() | ||||||
| 	assert.Equal(t, "/testsuburl/assets/img/avatar_default.png", | 	assert.Equal(t, "/testsuburl/assets/img/avatar_default.png", | ||||||
| 		avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100)) | 		avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100)) | ||||||
|  |  | ||||||
| 	enableGravatar(t) | 	enableGravatar(t) | ||||||
|  | 	config.GetDynGetter().InvalidateCache() | ||||||
| 	assert.Equal(t, | 	assert.Equal(t, | ||||||
| 		"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100", | 		"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100", | ||||||
| 		avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100), | 		avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100), | ||||||
|   | |||||||
| @@ -4,10 +4,6 @@ | |||||||
| package v1_18 //nolint | package v1_18 //nolint | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"strconv" |  | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" |  | ||||||
| 	"code.gitea.io/gitea/modules/timeutil" | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
|  |  | ||||||
| 	"xorm.io/xorm" | 	"xorm.io/xorm" | ||||||
| @@ -22,42 +18,6 @@ type SystemSetting struct { | |||||||
| 	Updated      timeutil.TimeStamp `xorm:"updated"` | 	Updated      timeutil.TimeStamp `xorm:"updated"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func insertSettingsIfNotExist(x *xorm.Engine, sysSettings []*SystemSetting) error { |  | ||||||
| 	sess := x.NewSession() |  | ||||||
| 	defer sess.Close() |  | ||||||
| 	if err := sess.Begin(); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	for _, setting := range sysSettings { |  | ||||||
| 		exist, err := sess.Table("system_setting").Where("setting_key=?", setting.SettingKey).Exist() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		if !exist { |  | ||||||
| 			if _, err := sess.Insert(setting); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return sess.Commit() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func CreateSystemSettingsTable(x *xorm.Engine) error { | func CreateSystemSettingsTable(x *xorm.Engine) error { | ||||||
| 	if err := x.Sync(new(SystemSetting)); err != nil { | 	return x.Sync(new(SystemSetting)) | ||||||
| 		return fmt.Errorf("sync2: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// migrate xx to database |  | ||||||
| 	sysSettings := []*SystemSetting{ |  | ||||||
| 		{ |  | ||||||
| 			SettingKey:   "picture.disable_gravatar", |  | ||||||
| 			SettingValue: strconv.FormatBool(setting.DisableGravatar), |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			SettingKey:   "picture.enable_federated_avatar", |  | ||||||
| 			SettingValue: strconv.FormatBool(setting.EnableFederatedAvatar), |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return insertSettingsIfNotExist(x, sysSettings) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ import ( | |||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | 	access_model "code.gitea.io/gitea/models/perm/access" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" |  | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| @@ -24,10 +23,7 @@ import ( | |||||||
|  |  | ||||||
| // Init initialize model | // Init initialize model | ||||||
| func Init(ctx context.Context) error { | func Init(ctx context.Context) error { | ||||||
| 	if err := unit.LoadUnitConfig(); err != nil { | 	return unit.LoadUnitConfig() | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return system_model.Init(ctx) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type repoChecker struct { | type repoChecker struct { | ||||||
|   | |||||||
| @@ -5,26 +5,21 @@ package system | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"math" | ||||||
| 	"net/url" | 	"sync" | ||||||
| 	"strconv" | 	"time" | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/modules/cache" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	setting_module "code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting/config" | ||||||
| 	"code.gitea.io/gitea/modules/timeutil" | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
|  |  | ||||||
| 	"strk.kbt.io/projects/go/libravatar" |  | ||||||
| 	"xorm.io/builder" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Setting is a key value store of user settings |  | ||||||
| type Setting struct { | type Setting struct { | ||||||
| 	ID           int64              `xorm:"pk autoincr"` | 	ID           int64              `xorm:"pk autoincr"` | ||||||
| 	SettingKey   string             `xorm:"varchar(255) unique"` // ensure key is always lowercase | 	SettingKey   string             `xorm:"varchar(255) unique"` // key should be lowercase | ||||||
| 	SettingValue string             `xorm:"text"` | 	SettingValue string             `xorm:"text"` | ||||||
| 	Version      int                `xorm:"version"` // prevent to override | 	Version      int                `xorm:"version"` | ||||||
| 	Created      timeutil.TimeStamp `xorm:"created"` | 	Created      timeutil.TimeStamp `xorm:"created"` | ||||||
| 	Updated      timeutil.TimeStamp `xorm:"updated"` | 	Updated      timeutil.TimeStamp `xorm:"updated"` | ||||||
| } | } | ||||||
| @@ -34,281 +29,119 @@ func (s *Setting) TableName() string { | |||||||
| 	return "system_setting" | 	return "system_setting" | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *Setting) GetValueBool() bool { |  | ||||||
| 	if s == nil { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	b, _ := strconv.ParseBool(s.SettingValue) |  | ||||||
| 	return b |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	db.RegisterModel(new(Setting)) | 	db.RegisterModel(new(Setting)) | ||||||
| } | } | ||||||
|  |  | ||||||
| // ErrSettingIsNotExist represents an error that a setting is not exist with special key | const keyRevision = "revision" | ||||||
| type ErrSettingIsNotExist struct { |  | ||||||
| 	Key string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Error implements error | func GetRevision(ctx context.Context) int { | ||||||
| func (err ErrSettingIsNotExist) Error() string { | 	revision := &Setting{SettingKey: keyRevision} | ||||||
| 	return fmt.Sprintf("System setting[%s] is not exist", err.Key) | 	if has, err := db.GetByBean(ctx, revision); err != nil { | ||||||
| } | 		return 0 | ||||||
|  | 	} else if !has { | ||||||
| // IsErrSettingIsNotExist return true if err is ErrSettingIsNotExist | 		err = db.Insert(ctx, &Setting{SettingKey: keyRevision, Version: 1}) | ||||||
| func IsErrSettingIsNotExist(err error) bool { | 		if err != nil { | ||||||
| 	_, ok := err.(ErrSettingIsNotExist) | 			return 0 | ||||||
| 	return ok |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ErrDataExpired represents an error that update a record which has been updated by another thread |  | ||||||
| type ErrDataExpired struct { |  | ||||||
| 	Key string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Error implements error |  | ||||||
| func (err ErrDataExpired) Error() string { |  | ||||||
| 	return fmt.Sprintf("System setting[%s] has been updated by another thread", err.Key) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // IsErrDataExpired return true if err is ErrDataExpired |  | ||||||
| func IsErrDataExpired(err error) bool { |  | ||||||
| 	_, ok := err.(ErrDataExpired) |  | ||||||
| 	return ok |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetSetting returns specific setting without using the cache |  | ||||||
| func GetSetting(ctx context.Context, key string) (*Setting, error) { |  | ||||||
| 	v, err := GetSettings(ctx, []string{key}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if len(v) == 0 { |  | ||||||
| 		return nil, ErrSettingIsNotExist{key} |  | ||||||
| 	} |  | ||||||
| 	return v[strings.ToLower(key)], nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const contextCacheKey = "system_setting" |  | ||||||
|  |  | ||||||
| // GetSettingWithCache returns the setting value via the key |  | ||||||
| func GetSettingWithCache(ctx context.Context, key, defaultVal string) (string, error) { |  | ||||||
| 	return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) { |  | ||||||
| 		return cache.GetString(genSettingCacheKey(key), func() (string, error) { |  | ||||||
| 			res, err := GetSetting(ctx, key) |  | ||||||
| 			if err != nil { |  | ||||||
| 				if IsErrSettingIsNotExist(err) { |  | ||||||
| 					return defaultVal, nil |  | ||||||
| 				} |  | ||||||
| 				return "", err |  | ||||||
| 			} |  | ||||||
| 			return res.SettingValue, nil |  | ||||||
| 		}) |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetSettingBool return bool value of setting, |  | ||||||
| // none existing keys and errors are ignored and result in false |  | ||||||
| func GetSettingBool(ctx context.Context, key string, defaultVal bool) (bool, error) { |  | ||||||
| 	s, err := GetSetting(ctx, key) |  | ||||||
| 	switch { |  | ||||||
| 	case err == nil: |  | ||||||
| 		v, _ := strconv.ParseBool(s.SettingValue) |  | ||||||
| 		return v, nil |  | ||||||
| 	case IsErrSettingIsNotExist(err): |  | ||||||
| 		return defaultVal, nil |  | ||||||
| 	default: |  | ||||||
| 		return false, err |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetSettingWithCacheBool(ctx context.Context, key string, defaultVal bool) bool { |  | ||||||
| 	s, _ := GetSettingWithCache(ctx, key, strconv.FormatBool(defaultVal)) |  | ||||||
| 	v, _ := strconv.ParseBool(s) |  | ||||||
| 	return v |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetSettings returns specific settings |  | ||||||
| func GetSettings(ctx context.Context, keys []string) (map[string]*Setting, error) { |  | ||||||
| 	for i := 0; i < len(keys); i++ { |  | ||||||
| 		keys[i] = strings.ToLower(keys[i]) |  | ||||||
| 	} |  | ||||||
| 	settings := make([]*Setting, 0, len(keys)) |  | ||||||
| 	if err := db.GetEngine(ctx). |  | ||||||
| 		Where(builder.In("setting_key", keys)). |  | ||||||
| 		Find(&settings); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	settingsMap := make(map[string]*Setting) |  | ||||||
| 	for _, s := range settings { |  | ||||||
| 		settingsMap[s.SettingKey] = s |  | ||||||
| 	} |  | ||||||
| 	return settingsMap, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type AllSettings map[string]*Setting |  | ||||||
|  |  | ||||||
| func (settings AllSettings) Get(key string) Setting { |  | ||||||
| 	if v, ok := settings[strings.ToLower(key)]; ok { |  | ||||||
| 		return *v |  | ||||||
| 	} |  | ||||||
| 	return Setting{} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (settings AllSettings) GetBool(key string) bool { |  | ||||||
| 	b, _ := strconv.ParseBool(settings.Get(key).SettingValue) |  | ||||||
| 	return b |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (settings AllSettings) GetVersion(key string) int { |  | ||||||
| 	return settings.Get(key).Version |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetAllSettings returns all settings from user |  | ||||||
| func GetAllSettings(ctx context.Context) (AllSettings, error) { |  | ||||||
| 	settings := make([]*Setting, 0, 5) |  | ||||||
| 	if err := db.GetEngine(ctx). |  | ||||||
| 		Find(&settings); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	settingsMap := make(map[string]*Setting) |  | ||||||
| 	for _, s := range settings { |  | ||||||
| 		settingsMap[s.SettingKey] = s |  | ||||||
| 	} |  | ||||||
| 	return settingsMap, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DeleteSetting deletes a specific setting for a user |  | ||||||
| func DeleteSetting(ctx context.Context, setting *Setting) error { |  | ||||||
| 	cache.RemoveContextData(ctx, contextCacheKey, setting.SettingKey) |  | ||||||
| 	cache.Remove(genSettingCacheKey(setting.SettingKey)) |  | ||||||
| 	_, err := db.GetEngine(ctx).Delete(setting) |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func SetSettingNoVersion(ctx context.Context, key, value string) error { |  | ||||||
| 	s, err := GetSetting(ctx, key) |  | ||||||
| 	if IsErrSettingIsNotExist(err) { |  | ||||||
| 		return SetSetting(ctx, &Setting{ |  | ||||||
| 			SettingKey:   key, |  | ||||||
| 			SettingValue: value, |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	s.SettingValue = value |  | ||||||
| 	return SetSetting(ctx, s) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SetSetting updates a users' setting for a specific key |  | ||||||
| func SetSetting(ctx context.Context, setting *Setting) error { |  | ||||||
| 	if err := upsertSettingValue(ctx, strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	setting.Version++ |  | ||||||
|  |  | ||||||
| 	cc := cache.GetCache() |  | ||||||
| 	if cc != nil { |  | ||||||
| 		if err := cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds()); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} | 		} | ||||||
|  | 		return 1 | ||||||
|  | 	} else if revision.Version <= 0 || revision.Version >= math.MaxInt-1 { | ||||||
|  | 		_, err = db.Exec(ctx, "UPDATE system_setting SET version=1 WHERE setting_key=?", keyRevision) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return 0 | ||||||
|  | 		} | ||||||
|  | 		return 1 | ||||||
| 	} | 	} | ||||||
| 	cache.SetContextData(ctx, contextCacheKey, setting.SettingKey, setting.SettingValue) | 	return revision.Version | ||||||
| 	return nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func upsertSettingValue(parentCtx context.Context, key, value string, version int) error { | func GetAllSettings(ctx context.Context) (revision int, res map[string]string, err error) { | ||||||
| 	return db.WithTx(parentCtx, func(ctx context.Context) error { | 	_ = GetRevision(ctx) // prepare the "revision" key ahead | ||||||
|  | 	var settings []*Setting | ||||||
|  | 	if err := db.GetEngine(ctx). | ||||||
|  | 		Find(&settings); err != nil { | ||||||
|  | 		return 0, nil, err | ||||||
|  | 	} | ||||||
|  | 	res = make(map[string]string) | ||||||
|  | 	for _, s := range settings { | ||||||
|  | 		if s.SettingKey == keyRevision { | ||||||
|  | 			revision = s.Version | ||||||
|  | 		} | ||||||
|  | 		res[s.SettingKey] = s.SettingValue | ||||||
|  | 	} | ||||||
|  | 	return revision, res, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func SetSettings(ctx context.Context, settings map[string]string) error { | ||||||
|  | 	_ = GetRevision(ctx) // prepare the "revision" key ahead | ||||||
|  | 	return db.WithTx(ctx, func(ctx context.Context) error { | ||||||
| 		e := db.GetEngine(ctx) | 		e := db.GetEngine(ctx) | ||||||
|  | 		_, err := db.Exec(ctx, "UPDATE system_setting SET version=version+1 WHERE setting_key=?", keyRevision) | ||||||
| 		// here we use a general method to do a safe upsert for different databases (and most transaction levels) |  | ||||||
| 		// 1. try to UPDATE the record and acquire the transaction write lock |  | ||||||
| 		//    if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly |  | ||||||
| 		//    if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist  (b) value is not changed |  | ||||||
| 		// 2. do a SELECT to check if the row exists or not (we already have the transaction lock) |  | ||||||
| 		// 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe) |  | ||||||
| 		// |  | ||||||
| 		// to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1` |  | ||||||
| 		//    to make sure the UPDATE always returns a non-zero value for existing (unchanged) records. |  | ||||||
|  |  | ||||||
| 		res, err := e.Exec("UPDATE system_setting SET setting_value=?, version = version+1 WHERE setting_key=? AND version=?", value, key, version) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		rows, _ := res.RowsAffected() | 		for k, v := range settings { | ||||||
| 		if rows > 0 { | 			res, err := e.Exec("UPDATE system_setting SET setting_value=? WHERE setting_key=?", v, k) | ||||||
| 			// the existing row is updated, so we can return | 			if err != nil { | ||||||
| 			return nil | 				return err | ||||||
|  | 			} | ||||||
|  | 			rows, _ := res.RowsAffected() | ||||||
|  | 			if rows == 0 { // if no existing row, insert a new row | ||||||
|  | 				if _, err = e.Insert(&Setting{SettingKey: k, SettingValue: v}); err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  | 		return nil | ||||||
| 		// in case the value isn't changed, update would return 0 rows changed, so we need this check |  | ||||||
| 		has, err := e.Exist(&Setting{SettingKey: key}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		if has { |  | ||||||
| 			return ErrDataExpired{Key: key} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// if no existing row, insert a new row |  | ||||||
| 		_, err = e.Insert(&Setting{SettingKey: key, SettingValue: value}) |  | ||||||
| 		return err |  | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| var ( | type dbConfigCachedGetter struct { | ||||||
| 	GravatarSourceURL *url.URL | 	mu sync.RWMutex | ||||||
| 	LibravatarService *libravatar.Libravatar |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func Init(ctx context.Context) error { | 	cacheTime time.Time | ||||||
| 	disableGravatar, err := GetSettingBool(ctx, KeyPictureDisableGravatar, setting_module.GetDefaultDisableGravatar()) | 	revision  int | ||||||
| 	if err != nil { | 	settings  map[string]string | ||||||
| 		return err | } | ||||||
| 	} |  | ||||||
|  | var _ config.DynKeyGetter = (*dbConfigCachedGetter)(nil) | ||||||
| 	enableFederatedAvatar, err := GetSettingBool(ctx, KeyPictureEnableFederatedAvatar, setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)) |  | ||||||
| 	if err != nil { | func (d *dbConfigCachedGetter) GetValue(ctx context.Context, key string) (v string, has bool) { | ||||||
| 		return err | 	d.mu.RLock() | ||||||
| 	} | 	defer d.mu.RUnlock() | ||||||
|  | 	v, has = d.settings[key] | ||||||
| 	if setting_module.OfflineMode { | 	return v, has | ||||||
| 		if !disableGravatar { | } | ||||||
| 			if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil { |  | ||||||
| 				return fmt.Errorf("failed to set setting %q: %w", KeyPictureDisableGravatar, err) | func (d *dbConfigCachedGetter) GetRevision(ctx context.Context) int { | ||||||
| 			} | 	d.mu.RLock() | ||||||
| 		} | 	defer d.mu.RUnlock() | ||||||
| 		disableGravatar = true | 	if time.Since(d.cacheTime) < time.Second { | ||||||
|  | 		return d.revision | ||||||
| 		if enableFederatedAvatar { | 	} | ||||||
| 			if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil { | 	if GetRevision(ctx) != d.revision { | ||||||
| 				return fmt.Errorf("failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err) | 		d.mu.RUnlock() | ||||||
| 			} | 		d.mu.Lock() | ||||||
| 		} | 		rev, set, err := GetAllSettings(ctx) | ||||||
| 		enableFederatedAvatar = false | 		if err != nil { | ||||||
| 	} | 			log.Error("Unable to get all settings: %v", err) | ||||||
|  | 		} else { | ||||||
| 	if enableFederatedAvatar || !disableGravatar { | 			d.cacheTime = time.Now() | ||||||
| 		var err error | 			d.revision = rev | ||||||
| 		GravatarSourceURL, err = url.Parse(setting_module.GravatarSource) | 			d.settings = set | ||||||
| 		if err != nil { | 		} | ||||||
| 			return fmt.Errorf("failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err) | 		d.mu.Unlock() | ||||||
| 		} | 		d.mu.RLock() | ||||||
| 	} | 	} | ||||||
|  | 	return d.revision | ||||||
| 	if GravatarSourceURL != nil && enableFederatedAvatar { | } | ||||||
| 		LibravatarService = libravatar.New() |  | ||||||
| 		if GravatarSourceURL.Scheme == "https" { | func (d *dbConfigCachedGetter) InvalidateCache() { | ||||||
| 			LibravatarService.SetUseHTTPS(true) | 	d.mu.Lock() | ||||||
| 			LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host) | 	d.cacheTime = time.Time{} | ||||||
| 		} else { | 	d.mu.Unlock() | ||||||
| 			LibravatarService.SetUseHTTPS(false) | } | ||||||
| 			LibravatarService.SetFallbackHost(GravatarSourceURL.Host) |  | ||||||
| 		} | func NewDatabaseDynKeyGetter() config.DynKeyGetter { | ||||||
| 	} | 	return &dbConfigCachedGetter{} | ||||||
| 	return nil |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +0,0 @@ | |||||||
| // Copyright 2022 The Gitea Authors. All rights reserved. |  | ||||||
| // SPDX-License-Identifier: MIT |  | ||||||
|  |  | ||||||
| package system |  | ||||||
|  |  | ||||||
| // enumerate all system setting keys |  | ||||||
| const ( |  | ||||||
| 	KeyPictureDisableGravatar       = "picture.disable_gravatar" |  | ||||||
| 	KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // genSettingCacheKey returns the cache key for some configuration |  | ||||||
| func genSettingCacheKey(key string) string { |  | ||||||
| 	return "system.setting." + key |  | ||||||
| } |  | ||||||
| @@ -4,7 +4,6 @@ | |||||||
| package system_test | package system_test | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| @@ -15,43 +14,29 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestSettings(t *testing.T) { | func TestSettings(t *testing.T) { | ||||||
| 	keyName := "server.LFS_LOCKS_PAGING_NUM" | 	keyName := "test.key" | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	newSetting := &system.Setting{SettingKey: keyName, SettingValue: "50"} | 	assert.NoError(t, db.TruncateBeans(db.DefaultContext, &system.Setting{})) | ||||||
|  |  | ||||||
| 	// create setting | 	rev, settings, err := system.GetAllSettings(db.DefaultContext) | ||||||
| 	err := system.SetSetting(db.DefaultContext, newSetting) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	// test about saving unchanged values |  | ||||||
| 	err = system.SetSetting(db.DefaultContext, newSetting) |  | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
|  | 	assert.EqualValues(t, 1, rev) | ||||||
|  | 	assert.Len(t, settings, 1) // there is only one "revision" key | ||||||
|  |  | ||||||
| 	// get specific setting | 	err = system.SetSettings(db.DefaultContext, map[string]string{keyName: "true"}) | ||||||
| 	settings, err := system.GetSettings(db.DefaultContext, []string{keyName}) |  | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Len(t, settings, 1) | 	rev, settings, err = system.GetAllSettings(db.DefaultContext) | ||||||
| 	assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue) |  | ||||||
|  |  | ||||||
| 	// updated setting |  | ||||||
| 	updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: settings[strings.ToLower(keyName)].Version} |  | ||||||
| 	err = system.SetSetting(db.DefaultContext, updatedSetting) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
|  |  | ||||||
| 	value, err := system.GetSetting(db.DefaultContext, keyName) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	assert.EqualValues(t, updatedSetting.SettingValue, value.SettingValue) |  | ||||||
|  |  | ||||||
| 	// get all settings |  | ||||||
| 	settings, err = system.GetAllSettings(db.DefaultContext) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	assert.Len(t, settings, 3) |  | ||||||
| 	assert.EqualValues(t, updatedSetting.SettingValue, settings[strings.ToLower(updatedSetting.SettingKey)].SettingValue) |  | ||||||
|  |  | ||||||
| 	// delete setting |  | ||||||
| 	err = system.DeleteSetting(db.DefaultContext, &system.Setting{SettingKey: strings.ToLower(keyName)}) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	settings, err = system.GetAllSettings(db.DefaultContext) |  | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
|  | 	assert.EqualValues(t, 2, rev) | ||||||
| 	assert.Len(t, settings, 2) | 	assert.Len(t, settings, 2) | ||||||
|  | 	assert.EqualValues(t, "true", settings[keyName]) | ||||||
|  |  | ||||||
|  | 	err = system.SetSettings(db.DefaultContext, map[string]string{keyName: "false"}) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	rev, settings, err = system.GetAllSettings(db.DefaultContext) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.EqualValues(t, 3, rev) | ||||||
|  | 	assert.Len(t, settings, 2) | ||||||
|  | 	assert.EqualValues(t, "false", settings[keyName]) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,11 +13,12 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" | 	"code.gitea.io/gitea/models/system" | ||||||
| 	"code.gitea.io/gitea/modules/auth/password/hash" | 	"code.gitea.io/gitea/modules/auth/password/hash" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting/config" | ||||||
| 	"code.gitea.io/gitea/modules/storage" | 	"code.gitea.io/gitea/modules/storage" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  |  | ||||||
| @@ -134,13 +135,11 @@ func MainTest(m *testing.M, testOpts *TestOptions) { | |||||||
|  |  | ||||||
| 	setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost" | 	setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost" | ||||||
|  |  | ||||||
|  | 	config.SetDynGetter(system.NewDatabaseDynKeyGetter()) | ||||||
|  |  | ||||||
| 	if err = storage.Init(); err != nil { | 	if err = storage.Init(); err != nil { | ||||||
| 		fatalTestError("storage.Init: %v\n", err) | 		fatalTestError("storage.Init: %v\n", err) | ||||||
| 	} | 	} | ||||||
| 	if err = system_model.Init(db.DefaultContext); err != nil { |  | ||||||
| 		fatalTestError("models.Init: %v\n", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err = util.RemoveAll(repoRootPath); err != nil { | 	if err = util.RemoveAll(repoRootPath); err != nil { | ||||||
| 		fatalTestError("util.RemoveAll: %v\n", err) | 		fatalTestError("util.RemoveAll: %v\n", err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -13,7 +13,6 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/avatars" | 	"code.gitea.io/gitea/models/avatars" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" |  | ||||||
| 	"code.gitea.io/gitea/modules/avatar" | 	"code.gitea.io/gitea/modules/avatar" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| @@ -67,9 +66,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { | |||||||
| 	useLocalAvatar := false | 	useLocalAvatar := false | ||||||
| 	autoGenerateAvatar := false | 	autoGenerateAvatar := false | ||||||
|  |  | ||||||
| 	disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, | 	disableGravatar := setting.Config().Picture.DisableGravatar.Value(ctx) | ||||||
| 		setting.GetDefaultDisableGravatar(), |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	switch { | 	switch { | ||||||
| 	case u.UseCustomAvatar: | 	case u.UseCustomAvatar: | ||||||
|   | |||||||
| @@ -12,7 +12,6 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" |  | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| @@ -103,12 +102,6 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) { | |||||||
| 	assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified) | 	assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified) | ||||||
| } | } | ||||||
|  |  | ||||||
| func initGravatarSource(t *testing.T) { |  | ||||||
| 	setting.GravatarSource = "https://secure.gravatar.com/avatar" |  | ||||||
| 	err := system_model.Init(db.DefaultContext) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestPushCommits_AvatarLink(t *testing.T) { | func TestPushCommits_AvatarLink(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| @@ -132,7 +125,7 @@ func TestPushCommits_AvatarLink(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	initGravatarSource(t) | 	setting.GravatarSource = "https://secure.gravatar.com/avatar" | ||||||
|  |  | ||||||
| 	assert.Equal(t, | 	assert.Equal(t, | ||||||
| 		"https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), | 		"https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								modules/setting/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								modules/setting/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package setting | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting/config" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type PictureStruct struct { | ||||||
|  | 	DisableGravatar       *config.Value[bool] | ||||||
|  | 	EnableFederatedAvatar *config.Value[bool] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ConfigStruct struct { | ||||||
|  | 	Picture *PictureStruct | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	defaultConfig     *ConfigStruct | ||||||
|  | 	defaultConfigOnce sync.Once | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func initDefaultConfig() { | ||||||
|  | 	config.SetCfgSecKeyGetter(&cfgSecKeyGetter{}) | ||||||
|  | 	defaultConfig = &ConfigStruct{ | ||||||
|  | 		Picture: &PictureStruct{ | ||||||
|  | 			DisableGravatar:       config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"), | ||||||
|  | 			EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Config() *ConfigStruct { | ||||||
|  | 	defaultConfigOnce.Do(initDefaultConfig) | ||||||
|  | 	return defaultConfig | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type cfgSecKeyGetter struct{} | ||||||
|  |  | ||||||
|  | func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) { | ||||||
|  | 	cfgSec, err := CfgProvider.GetSection(sec) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Unable to get config section: %q", sec) | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	cfgKey := ConfigSectionKey(cfgSec, key) | ||||||
|  | 	if cfgKey == nil { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	return cfgKey.Value(), true | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								modules/setting/config/getter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								modules/setting/config/getter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package config | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"sync" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var getterMu sync.RWMutex | ||||||
|  |  | ||||||
|  | type CfgSecKeyGetter interface { | ||||||
|  | 	GetValue(sec, key string) (v string, has bool) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var cfgSecKeyGetterInternal CfgSecKeyGetter | ||||||
|  |  | ||||||
|  | func SetCfgSecKeyGetter(p CfgSecKeyGetter) { | ||||||
|  | 	getterMu.Lock() | ||||||
|  | 	cfgSecKeyGetterInternal = p | ||||||
|  | 	getterMu.Unlock() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetCfgSecKeyGetter() CfgSecKeyGetter { | ||||||
|  | 	getterMu.RLock() | ||||||
|  | 	defer getterMu.RUnlock() | ||||||
|  | 	return cfgSecKeyGetterInternal | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type DynKeyGetter interface { | ||||||
|  | 	GetValue(ctx context.Context, key string) (v string, has bool) | ||||||
|  | 	GetRevision(ctx context.Context) int | ||||||
|  | 	InvalidateCache() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var dynKeyGetterInternal DynKeyGetter | ||||||
|  |  | ||||||
|  | func SetDynGetter(p DynKeyGetter) { | ||||||
|  | 	getterMu.Lock() | ||||||
|  | 	dynKeyGetterInternal = p | ||||||
|  | 	getterMu.Unlock() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetDynGetter() DynKeyGetter { | ||||||
|  | 	getterMu.RLock() | ||||||
|  | 	defer getterMu.RUnlock() | ||||||
|  | 	return dynKeyGetterInternal | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								modules/setting/config/value.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								modules/setting/config/value.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package config | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"strconv" | ||||||
|  | 	"sync" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type CfgSecKey struct { | ||||||
|  | 	Sec, Key string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Value[T any] struct { | ||||||
|  | 	mu sync.RWMutex | ||||||
|  |  | ||||||
|  | 	cfgSecKey CfgSecKey | ||||||
|  | 	dynKey    string | ||||||
|  |  | ||||||
|  | 	def, value T | ||||||
|  | 	revision   int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (value *Value[T]) parse(s string) (v T) { | ||||||
|  | 	switch any(v).(type) { | ||||||
|  | 	case bool: | ||||||
|  | 		b, _ := strconv.ParseBool(s) | ||||||
|  | 		return any(b).(T) | ||||||
|  | 	default: | ||||||
|  | 		panic("unsupported config type, please complete the code") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (value *Value[T]) Value(ctx context.Context) (v T) { | ||||||
|  | 	dg := GetDynGetter() | ||||||
|  | 	if dg == nil { | ||||||
|  | 		// this is an edge case: the database is not initialized but the system setting is going to be used | ||||||
|  | 		// it should panic to avoid inconsistent config values (from config / system setting) and fix the code | ||||||
|  | 		panic("no config dyn value getter") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rev := dg.GetRevision(ctx) | ||||||
|  |  | ||||||
|  | 	// if the revision in database doesn't change, use the last value | ||||||
|  | 	value.mu.RLock() | ||||||
|  | 	if rev == value.revision { | ||||||
|  | 		v = value.value | ||||||
|  | 		value.mu.RUnlock() | ||||||
|  | 		return v | ||||||
|  | 	} | ||||||
|  | 	value.mu.RUnlock() | ||||||
|  |  | ||||||
|  | 	// try to parse the config and cache it | ||||||
|  | 	var valStr *string | ||||||
|  | 	if dynVal, has := dg.GetValue(ctx, value.dynKey); has { | ||||||
|  | 		valStr = &dynVal | ||||||
|  | 	} else if cfgVal, has := GetCfgSecKeyGetter().GetValue(value.cfgSecKey.Sec, value.cfgSecKey.Key); has { | ||||||
|  | 		valStr = &cfgVal | ||||||
|  | 	} | ||||||
|  | 	if valStr == nil { | ||||||
|  | 		v = value.def | ||||||
|  | 	} else { | ||||||
|  | 		v = value.parse(*valStr) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	value.mu.Lock() | ||||||
|  | 	value.value = v | ||||||
|  | 	value.revision = rev | ||||||
|  | 	value.mu.Unlock() | ||||||
|  | 	return v | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (value *Value[T]) DynKey() string { | ||||||
|  | 	return value.dynKey | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] { | ||||||
|  | 	return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey} | ||||||
|  | } | ||||||
| @@ -3152,7 +3152,6 @@ config.access_log_mode = Access Log Mode | |||||||
| config.access_log_template = Access Log Template | config.access_log_template = Access Log Template | ||||||
| config.xorm_log_sql = Log SQL | config.xorm_log_sql = Log SQL | ||||||
|  |  | ||||||
| config.get_setting_failed = Get setting %s failed |  | ||||||
| config.set_setting_failed = Set setting %s failed | config.set_setting_failed = Set setting %s failed | ||||||
|  |  | ||||||
| monitor.stats = Stats | monitor.stats = Stats | ||||||
|   | |||||||
| @@ -10,8 +10,10 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/migrations" | 	"code.gitea.io/gitea/models/migrations" | ||||||
|  | 	system_model "code.gitea.io/gitea/models/system" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting/config" | ||||||
|  |  | ||||||
| 	"xorm.io/xorm" | 	"xorm.io/xorm" | ||||||
| ) | ) | ||||||
| @@ -36,6 +38,7 @@ func InitDBEngine(ctx context.Context) (err error) { | |||||||
| 		time.Sleep(setting.Database.DBConnectBackoff) | 		time.Sleep(setting.Database.DBConnectBackoff) | ||||||
| 	} | 	} | ||||||
| 	db.HasEngine = true | 	db.HasEngine = true | ||||||
|  | 	config.SetDynGetter(system_model.NewDatabaseDynKeyGetter()) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -430,15 +430,14 @@ func SubmitInstall(ctx *context.Context) { | |||||||
| 	cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(fmt.Sprint(form.MailNotify)) | 	cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(fmt.Sprint(form.MailNotify)) | ||||||
|  |  | ||||||
| 	cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode)) | 	cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode)) | ||||||
| 	// if you are reinstalling, this maybe not right because of missing version | 	if err := system_model.SetSettings(ctx, map[string]string{ | ||||||
| 	if err := system_model.SetSettingNoVersion(ctx, system_model.KeyPictureDisableGravatar, strconv.FormatBool(form.DisableGravatar)); err != nil { | 		setting.Config().Picture.DisableGravatar.DynKey():       strconv.FormatBool(form.DisableGravatar), | ||||||
| 		ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) | 		setting.Config().Picture.EnableFederatedAvatar.DynKey(): strconv.FormatBool(form.EnableFederatedAvatar), | ||||||
| 		return | 	}); err != nil { | ||||||
| 	} |  | ||||||
| 	if err := system_model.SetSettingNoVersion(ctx, system_model.KeyPictureEnableFederatedAvatar, strconv.FormatBool(form.EnableFederatedAvatar)); err != nil { |  | ||||||
| 		ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) | 		ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn)) | 	cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn)) | ||||||
| 	cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp)) | 	cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp)) | ||||||
| 	cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration)) | 	cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration)) | ||||||
|   | |||||||
| @@ -5,19 +5,19 @@ | |||||||
| package admin | package admin | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strconv" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	system_model "code.gitea.io/gitea/models/system" | 	system_model "code.gitea.io/gitea/models/system" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
|  | 	"code.gitea.io/gitea/modules/container" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/json" | 	"code.gitea.io/gitea/modules/json" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting/config" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/services/mailer" | 	"code.gitea.io/gitea/services/mailer" | ||||||
|  |  | ||||||
| @@ -101,16 +101,6 @@ func Config(ctx *context.Context) { | |||||||
| 	ctx.Data["Title"] = ctx.Tr("admin.config") | 	ctx.Data["Title"] = ctx.Tr("admin.config") | ||||||
| 	ctx.Data["PageIsAdminConfig"] = true | 	ctx.Data["PageIsAdminConfig"] = true | ||||||
|  |  | ||||||
| 	systemSettings, err := system_model.GetAllSettings(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("system_model.GetAllSettings", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// All editable settings from UI |  | ||||||
| 	ctx.Data["SystemSettings"] = systemSettings |  | ||||||
| 	ctx.PageData["adminConfigPage"] = true |  | ||||||
|  |  | ||||||
| 	ctx.Data["CustomConf"] = setting.CustomConf | 	ctx.Data["CustomConf"] = setting.CustomConf | ||||||
| 	ctx.Data["AppUrl"] = setting.AppURL | 	ctx.Data["AppUrl"] = setting.AppURL | ||||||
| 	ctx.Data["AppBuiltWith"] = setting.AppBuiltWith | 	ctx.Data["AppBuiltWith"] = setting.AppBuiltWith | ||||||
| @@ -170,7 +160,8 @@ func Config(ctx *context.Context) { | |||||||
| 	ctx.Data["LogSQL"] = setting.Database.LogSQL | 	ctx.Data["LogSQL"] = setting.Database.LogSQL | ||||||
|  |  | ||||||
| 	ctx.Data["Loggers"] = log.GetManager().DumpLoggers() | 	ctx.Data["Loggers"] = log.GetManager().DumpLoggers() | ||||||
|  | 	config.GetDynGetter().InvalidateCache() | ||||||
|  | 	ctx.Data["SystemConfig"] = setting.Config() | ||||||
| 	prepareDeprecatedWarningsAlert(ctx) | 	prepareDeprecatedWarningsAlert(ctx) | ||||||
|  |  | ||||||
| 	ctx.HTML(http.StatusOK, tplConfig) | 	ctx.HTML(http.StatusOK, tplConfig) | ||||||
| @@ -178,51 +169,19 @@ func Config(ctx *context.Context) { | |||||||
|  |  | ||||||
| func ChangeConfig(ctx *context.Context) { | func ChangeConfig(ctx *context.Context) { | ||||||
| 	key := strings.TrimSpace(ctx.FormString("key")) | 	key := strings.TrimSpace(ctx.FormString("key")) | ||||||
| 	if key == "" { |  | ||||||
| 		ctx.JSONRedirect(ctx.Req.URL.String()) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	value := ctx.FormString("value") | 	value := ctx.FormString("value") | ||||||
| 	version := ctx.FormInt("version") | 	cfg := setting.Config() | ||||||
|  | 	allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey()) | ||||||
| 	if check, ok := changeConfigChecks[key]; ok { | 	if !allowedKeys.Contains(key) { | ||||||
| 		if err := check(ctx, value); err != nil { | 		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) | ||||||
| 			log.Warn("refused to set setting: %v", err) | 		return | ||||||
| 			ctx.JSON(http.StatusOK, map[string]string{ |  | ||||||
| 				"err": ctx.Tr("admin.config.set_setting_failed", key), |  | ||||||
| 			}) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  | 	if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil { | ||||||
| 	if err := system_model.SetSetting(ctx, &system_model.Setting{ |  | ||||||
| 		SettingKey:   key, |  | ||||||
| 		SettingValue: value, |  | ||||||
| 		Version:      version, |  | ||||||
| 	}); err != nil { |  | ||||||
| 		log.Error("set setting failed: %v", err) | 		log.Error("set setting failed: %v", err) | ||||||
| 		ctx.JSON(http.StatusOK, map[string]string{ | 		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) | ||||||
| 			"err": ctx.Tr("admin.config.set_setting_failed", key), |  | ||||||
| 		}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.JSON(http.StatusOK, map[string]any{ | 	config.GetDynGetter().InvalidateCache() | ||||||
| 		"version": version + 1, | 	ctx.JSONOK() | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var changeConfigChecks = map[string]func(ctx *context.Context, newValue string) error{ |  | ||||||
| 	system_model.KeyPictureDisableGravatar: func(_ *context.Context, newValue string) error { |  | ||||||
| 		if v, _ := strconv.ParseBool(newValue); setting.OfflineMode && !v { |  | ||||||
| 			return fmt.Errorf("%q should be true when OFFLINE_MODE is true", system_model.KeyPictureDisableGravatar) |  | ||||||
| 		} |  | ||||||
| 		return nil |  | ||||||
| 	}, |  | ||||||
| 	system_model.KeyPictureEnableFederatedAvatar: func(_ *context.Context, newValue string) error { |  | ||||||
| 		if v, _ := strconv.ParseBool(newValue); setting.OfflineMode && v { |  | ||||||
| 			return fmt.Errorf("%q cannot be false when OFFLINE_MODE is true", system_model.KeyPictureEnableFederatedAvatar) |  | ||||||
| 		} |  | ||||||
| 		return nil |  | ||||||
| 	}, |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,7 +15,6 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	org_model "code.gitea.io/gitea/models/organization" | 	org_model "code.gitea.io/gitea/models/organization" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" |  | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/auth/password" | 	"code.gitea.io/gitea/modules/auth/password" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| @@ -308,17 +307,18 @@ func ViewUser(ctx *context.Context) { | |||||||
| 	ctx.HTML(http.StatusOK, tplUserView) | 	ctx.HTML(http.StatusOK, tplUserView) | ||||||
| } | } | ||||||
|  |  | ||||||
| // EditUser show editing user page | func editUserCommon(ctx *context.Context) { | ||||||
| func EditUser(ctx *context.Context) { |  | ||||||
| 	ctx.Data["Title"] = ctx.Tr("admin.users.edit_account") | 	ctx.Data["Title"] = ctx.Tr("admin.users.edit_account") | ||||||
| 	ctx.Data["PageIsAdminUsers"] = true | 	ctx.Data["PageIsAdminUsers"] = true | ||||||
| 	ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation | 	ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation | ||||||
| 	ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations | 	ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations | ||||||
| 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | ||||||
| 	ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, | 	ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) | ||||||
| 		setting.GetDefaultDisableGravatar(), | } | ||||||
| 	) |  | ||||||
|  |  | ||||||
|  | // EditUser show editing user page | ||||||
|  | func EditUser(ctx *context.Context) { | ||||||
|  | 	editUserCommon(ctx) | ||||||
| 	prepareUserInfo(ctx) | 	prepareUserInfo(ctx) | ||||||
| 	if ctx.Written() { | 	if ctx.Written() { | ||||||
| 		return | 		return | ||||||
| @@ -329,19 +329,13 @@ func EditUser(ctx *context.Context) { | |||||||
|  |  | ||||||
| // EditUserPost response for editing user | // EditUserPost response for editing user | ||||||
| func EditUserPost(ctx *context.Context) { | func EditUserPost(ctx *context.Context) { | ||||||
| 	form := web.GetForm(ctx).(*forms.AdminEditUserForm) | 	editUserCommon(ctx) | ||||||
| 	ctx.Data["Title"] = ctx.Tr("admin.users.edit_account") |  | ||||||
| 	ctx.Data["PageIsAdminUsers"] = true |  | ||||||
| 	ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations |  | ||||||
| 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() |  | ||||||
| 	ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, |  | ||||||
| 		setting.GetDefaultDisableGravatar()) |  | ||||||
|  |  | ||||||
| 	u := prepareUserInfo(ctx) | 	u := prepareUserInfo(ctx) | ||||||
| 	if ctx.Written() { | 	if ctx.Written() { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	form := web.GetForm(ctx).(*forms.AdminEditUserForm) | ||||||
| 	if ctx.HasError() { | 	if ctx.HasError() { | ||||||
| 		ctx.HTML(http.StatusOK, tplUserEdit) | 		ctx.HTML(http.StatusOK, tplUserEdit) | ||||||
| 		return | 		return | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" |  | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| @@ -44,9 +43,7 @@ func Profile(ctx *context.Context) { | |||||||
| 	ctx.Data["Title"] = ctx.Tr("settings.profile") | 	ctx.Data["Title"] = ctx.Tr("settings.profile") | ||||||
| 	ctx.Data["PageIsSettingsProfile"] = true | 	ctx.Data["PageIsSettingsProfile"] = true | ||||||
| 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | ||||||
| 	ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, | 	ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) | ||||||
| 		setting.GetDefaultDisableGravatar(), |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	ctx.HTML(http.StatusOK, tplSettingsProfile) | 	ctx.HTML(http.StatusOK, tplSettingsProfile) | ||||||
| } | } | ||||||
| @@ -88,9 +85,7 @@ func ProfilePost(ctx *context.Context) { | |||||||
| 	ctx.Data["Title"] = ctx.Tr("settings") | 	ctx.Data["Title"] = ctx.Tr("settings") | ||||||
| 	ctx.Data["PageIsSettingsProfile"] = true | 	ctx.Data["PageIsSettingsProfile"] = true | ||||||
| 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | 	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() | ||||||
| 	ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, | 	ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) | ||||||
| 		setting.GetDefaultDisableGravatar(), |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	if ctx.HasError() { | 	if ctx.HasError() { | ||||||
| 		ctx.HTML(http.StatusOK, tplSettingsProfile) | 		ctx.HTML(http.StatusOK, tplSettingsProfile) | ||||||
|   | |||||||
| @@ -292,15 +292,15 @@ | |||||||
| 			<dl class="admin-dl-horizontal"> | 			<dl class="admin-dl-horizontal"> | ||||||
| 				<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt> | 				<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt> | ||||||
| 				<dd> | 				<dd> | ||||||
| 					<div class="ui toggle checkbox"> | 					<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}"> | ||||||
| 						<input type="checkbox" name="picture.disable_gravatar" version="{{.SystemSettings.GetVersion "picture.disable_gravatar"}}"{{if .SystemSettings.GetBool "picture.disable_gravatar"}} checked{{end}} title="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}"> | 						<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}> | ||||||
| 					</div> | 					</div> | ||||||
| 				</dd> | 				</dd> | ||||||
| 				<div class="divider"></div> | 				<div class="divider"></div> | ||||||
| 				<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt> | 				<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt> | ||||||
| 				<dd> | 				<dd> | ||||||
| 					<div class="ui toggle checkbox"> | 					<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}"> | ||||||
| 						<input type="checkbox" name="picture.enable_federated_avatar" version="{{.SystemSettings.GetVersion "picture.enable_federated_avatar"}}"{{if .SystemSettings.GetBool "picture.enable_federated_avatar"}} checked{{end}} title="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}"> | 						<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}> | ||||||
| 					</div> | 					</div> | ||||||
| 				</dd> | 				</dd> | ||||||
| 			</dl> | 			</dl> | ||||||
|   | |||||||
| @@ -1,37 +1,24 @@ | |||||||
| import $ from 'jquery'; |  | ||||||
| import {showTemporaryTooltip} from '../../modules/tippy.js'; | import {showTemporaryTooltip} from '../../modules/tippy.js'; | ||||||
|  | import {POST} from '../../modules/fetch.js'; | ||||||
|  |  | ||||||
| const {appSubUrl, csrfToken, pageData} = window.config; | const {appSubUrl} = window.config; | ||||||
|  |  | ||||||
| export function initAdminConfigs() { | export function initAdminConfigs() { | ||||||
|   const isAdminConfigPage = pageData?.adminConfigPage; |   const elAdminConfig = document.querySelector('.page-content.admin.config'); | ||||||
|   if (!isAdminConfigPage) return; |   if (!elAdminConfig) return; | ||||||
|  |  | ||||||
|   $("input[type='checkbox']").on('change', (e) => { |   for (const el of elAdminConfig.querySelectorAll('input[type="checkbox"][data-config-dyn-key]')) { | ||||||
|     const $this = $(e.currentTarget); |     el.addEventListener('change', async () => { | ||||||
|     $.ajax({ |       try { | ||||||
|       url: `${appSubUrl}/admin/config`, |         const resp = await POST(`${appSubUrl}/admin/config`, { | ||||||
|       type: 'POST', |           data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key'), value: el.checked}), | ||||||
|       data: { |         }); | ||||||
|         _csrf: csrfToken, |         const json = await resp.json(); | ||||||
|         key: $this.attr('name'), |         if (json.errorMessage) throw new Error(json.errorMessage); | ||||||
|         value: $this.is(':checked'), |       } catch (ex) { | ||||||
|         version: $this.attr('version'), |         showTemporaryTooltip(el, ex.toString()); | ||||||
|       } |         el.checked = !el.checked; | ||||||
|     }).done((resp) => { |  | ||||||
|       if (resp) { |  | ||||||
|         if (resp.redirect) { |  | ||||||
|           window.location.href = resp.redirect; |  | ||||||
|         } else if (resp.version) { |  | ||||||
|           $this.attr('version', resp.version); |  | ||||||
|         } else if (resp.err) { |  | ||||||
|           showTemporaryTooltip(e.currentTarget, resp.err); |  | ||||||
|           $this.prop('checked', !$this.is(':checked')); |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |   } | ||||||
|     e.preventDefault(); |  | ||||||
|     return false; |  | ||||||
|   }); |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Giteabot
					Giteabot