loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor system setting (#27000)

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.

authored by

wxiaoguang and committed by
GitHub
9f8d5985 976d1760

+411 -507
+58 -23
models/avatars/avatar.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "fmt" 8 9 "net/url" 9 10 "path" 10 11 "strconv" 11 12 "strings" 12 - "sync" 13 + "sync/atomic" 13 14 14 15 "code.gitea.io/gitea/models/db" 15 - system_model "code.gitea.io/gitea/models/system" 16 16 "code.gitea.io/gitea/modules/base" 17 17 "code.gitea.io/gitea/modules/cache" 18 18 "code.gitea.io/gitea/modules/log" 19 19 "code.gitea.io/gitea/modules/setting" 20 + 21 + "strk.kbt.io/projects/go/libravatar" 20 22 ) 21 23 22 24 const ( ··· 36 38 db.RegisterModel(new(EmailHash)) 37 39 } 38 40 39 - var ( 41 + type avatarSettingStruct struct { 40 42 defaultAvatarLink string 41 - once sync.Once 42 - ) 43 + gravatarSource string 44 + gravatarSourceURL *url.URL 45 + libravatar *libravatar.Libravatar 46 + } 43 47 44 - // DefaultAvatarLink the default avatar link 45 - func DefaultAvatarLink() string { 46 - once.Do(func() { 48 + var avatarSettingAtomic atomic.Pointer[avatarSettingStruct] 49 + 50 + func loadAvatarSetting() (*avatarSettingStruct, error) { 51 + s := avatarSettingAtomic.Load() 52 + if s == nil || s.gravatarSource != setting.GravatarSource { 53 + s = &avatarSettingStruct{} 47 54 u, err := url.Parse(setting.AppSubURL) 48 55 if err != nil { 49 - log.Error("Can not parse AppSubURL: %v", err) 50 - return 56 + return nil, fmt.Errorf("unable to parse AppSubURL: %w", err) 51 57 } 52 58 53 59 u.Path = path.Join(u.Path, "/assets/img/avatar_default.png") 54 - defaultAvatarLink = u.String() 55 - }) 56 - return defaultAvatarLink 60 + s.defaultAvatarLink = u.String() 61 + 62 + s.gravatarSourceURL, err = url.Parse(setting.GravatarSource) 63 + if err != nil { 64 + return nil, fmt.Errorf("unable to parse GravatarSource %q: %w", setting.GravatarSource, err) 65 + } 66 + 67 + s.libravatar = libravatar.New() 68 + if s.gravatarSourceURL.Scheme == "https" { 69 + s.libravatar.SetUseHTTPS(true) 70 + s.libravatar.SetSecureFallbackHost(s.gravatarSourceURL.Host) 71 + } else { 72 + s.libravatar.SetUseHTTPS(false) 73 + s.libravatar.SetFallbackHost(s.gravatarSourceURL.Host) 74 + } 75 + 76 + avatarSettingAtomic.Store(s) 77 + } 78 + return s, nil 79 + } 80 + 81 + // DefaultAvatarLink the default avatar link 82 + func DefaultAvatarLink() string { 83 + a, err := loadAvatarSetting() 84 + if err != nil { 85 + log.Error("Failed to loadAvatarSetting: %v", err) 86 + return "" 87 + } 88 + return a.defaultAvatarLink 57 89 } 58 90 59 91 // HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/ ··· 76 108 // LibravatarURL returns the URL for the given email. Slow due to the DNS lookup. 77 109 // This function should only be called if a federated avatar service is enabled. 78 110 func LibravatarURL(email string) (*url.URL, error) { 79 - urlStr, err := system_model.LibravatarService.FromEmail(email) 111 + a, err := loadAvatarSetting() 112 + if err != nil { 113 + return nil, err 114 + } 115 + urlStr, err := a.libravatar.FromEmail(email) 80 116 if err != nil { 81 117 log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err) 82 118 return nil, err ··· 153 189 return DefaultAvatarLink() 154 190 } 155 191 156 - disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, 157 - setting.GetDefaultDisableGravatar(), 158 - ) 159 - 160 - enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar, 161 - setting.GetDefaultEnableFederatedAvatar(disableGravatar)) 192 + avatarSetting, err := loadAvatarSetting() 193 + if err != nil { 194 + return DefaultAvatarLink() 195 + } 162 196 163 - var err error 164 - if enableFederatedAvatar && system_model.LibravatarService != nil { 197 + enableFederatedAvatar := setting.Config().Picture.EnableFederatedAvatar.Value(ctx) 198 + if enableFederatedAvatar { 165 199 emailHash := saveEmailHash(email) 166 200 if final { 167 201 // for final link, we can spend more time on slow external query ··· 179 213 return urlStr 180 214 } 181 215 216 + disableGravatar := setting.Config().Picture.DisableGravatar.Value(ctx) 182 217 if !disableGravatar { 183 218 // copy GravatarSourceURL, because we will modify its Path. 184 - avatarURLCopy := *system_model.GravatarSourceURL 219 + avatarURLCopy := *avatarSetting.gravatarSourceURL 185 220 avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email)) 186 221 return generateRecognizedAvatarURL(avatarURLCopy, size) 187 222 }
+6 -6
models/avatars/avatar_test.go
··· 10 10 "code.gitea.io/gitea/models/db" 11 11 system_model "code.gitea.io/gitea/models/system" 12 12 "code.gitea.io/gitea/modules/setting" 13 + "code.gitea.io/gitea/modules/setting/config" 13 14 14 15 "github.com/stretchr/testify/assert" 15 16 ) ··· 17 18 const gravatarSource = "https://secure.gravatar.com/avatar/" 18 19 19 20 func disableGravatar(t *testing.T) { 20 - err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureEnableFederatedAvatar, "false") 21 + err := system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.EnableFederatedAvatar.DynKey(): "false"}) 21 22 assert.NoError(t, err) 22 - err = system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "true") 23 + err = system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.DisableGravatar.DynKey(): "true"}) 23 24 assert.NoError(t, err) 24 - system_model.LibravatarService = nil 25 25 } 26 26 27 27 func enableGravatar(t *testing.T) { 28 - err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "false") 28 + err := system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.DisableGravatar.DynKey(): "false"}) 29 29 assert.NoError(t, err) 30 30 setting.GravatarSource = gravatarSource 31 - err = system_model.Init(db.DefaultContext) 32 - assert.NoError(t, err) 33 31 } 34 32 35 33 func TestHashEmail(t *testing.T) { ··· 47 45 setting.AppSubURL = "/testsuburl" 48 46 49 47 disableGravatar(t) 48 + config.GetDynGetter().InvalidateCache() 50 49 assert.Equal(t, "/testsuburl/assets/img/avatar_default.png", 51 50 avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100)) 52 51 53 52 enableGravatar(t) 53 + config.GetDynGetter().InvalidateCache() 54 54 assert.Equal(t, 55 55 "https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100", 56 56 avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100),
+1 -41
models/migrations/v1_18/v227.go
··· 4 4 package v1_18 //nolint 5 5 6 6 import ( 7 - "fmt" 8 - "strconv" 9 - 10 - "code.gitea.io/gitea/modules/setting" 11 7 "code.gitea.io/gitea/modules/timeutil" 12 8 13 9 "xorm.io/xorm" ··· 22 18 Updated timeutil.TimeStamp `xorm:"updated"` 23 19 } 24 20 25 - func insertSettingsIfNotExist(x *xorm.Engine, sysSettings []*SystemSetting) error { 26 - sess := x.NewSession() 27 - defer sess.Close() 28 - if err := sess.Begin(); err != nil { 29 - return err 30 - } 31 - for _, setting := range sysSettings { 32 - exist, err := sess.Table("system_setting").Where("setting_key=?", setting.SettingKey).Exist() 33 - if err != nil { 34 - return err 35 - } 36 - if !exist { 37 - if _, err := sess.Insert(setting); err != nil { 38 - return err 39 - } 40 - } 41 - } 42 - return sess.Commit() 43 - } 44 - 45 21 func CreateSystemSettingsTable(x *xorm.Engine) error { 46 - if err := x.Sync(new(SystemSetting)); err != nil { 47 - return fmt.Errorf("sync2: %w", err) 48 - } 49 - 50 - // migrate xx to database 51 - sysSettings := []*SystemSetting{ 52 - { 53 - SettingKey: "picture.disable_gravatar", 54 - SettingValue: strconv.FormatBool(setting.DisableGravatar), 55 - }, 56 - { 57 - SettingKey: "picture.enable_federated_avatar", 58 - SettingValue: strconv.FormatBool(setting.EnableFederatedAvatar), 59 - }, 60 - } 61 - 62 - return insertSettingsIfNotExist(x, sysSettings) 22 + return x.Sync(new(SystemSetting)) 63 23 }
+1 -5
models/repo.go
··· 16 16 issues_model "code.gitea.io/gitea/models/issues" 17 17 access_model "code.gitea.io/gitea/models/perm/access" 18 18 repo_model "code.gitea.io/gitea/models/repo" 19 - system_model "code.gitea.io/gitea/models/system" 20 19 "code.gitea.io/gitea/models/unit" 21 20 user_model "code.gitea.io/gitea/models/user" 22 21 "code.gitea.io/gitea/modules/log" ··· 24 23 25 24 // Init initialize model 26 25 func Init(ctx context.Context) error { 27 - if err := unit.LoadUnitConfig(); err != nil { 28 - return err 29 - } 30 - return system_model.Init(ctx) 26 + return unit.LoadUnitConfig() 31 27 } 32 28 33 29 type repoChecker struct {
+89 -256
models/system/setting.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "fmt" 9 - "net/url" 10 - "strconv" 11 - "strings" 8 + "math" 9 + "sync" 10 + "time" 12 11 13 12 "code.gitea.io/gitea/models/db" 14 - "code.gitea.io/gitea/modules/cache" 15 - setting_module "code.gitea.io/gitea/modules/setting" 13 + "code.gitea.io/gitea/modules/log" 14 + "code.gitea.io/gitea/modules/setting/config" 16 15 "code.gitea.io/gitea/modules/timeutil" 17 - 18 - "strk.kbt.io/projects/go/libravatar" 19 - "xorm.io/builder" 20 16 ) 21 17 22 - // Setting is a key value store of user settings 23 18 type Setting struct { 24 19 ID int64 `xorm:"pk autoincr"` 25 - SettingKey string `xorm:"varchar(255) unique"` // ensure key is always lowercase 20 + SettingKey string `xorm:"varchar(255) unique"` // key should be lowercase 26 21 SettingValue string `xorm:"text"` 27 - Version int `xorm:"version"` // prevent to override 22 + Version int `xorm:"version"` 28 23 Created timeutil.TimeStamp `xorm:"created"` 29 24 Updated timeutil.TimeStamp `xorm:"updated"` 30 25 } ··· 34 29 return "system_setting" 35 30 } 36 31 37 - func (s *Setting) GetValueBool() bool { 38 - if s == nil { 39 - return false 40 - } 41 - 42 - b, _ := strconv.ParseBool(s.SettingValue) 43 - return b 44 - } 45 - 46 32 func init() { 47 33 db.RegisterModel(new(Setting)) 48 34 } 49 35 50 - // ErrSettingIsNotExist represents an error that a setting is not exist with special key 51 - type ErrSettingIsNotExist struct { 52 - Key string 53 - } 36 + const keyRevision = "revision" 54 37 55 - // Error implements error 56 - func (err ErrSettingIsNotExist) Error() string { 57 - return fmt.Sprintf("System setting[%s] is not exist", err.Key) 58 - } 59 - 60 - // IsErrSettingIsNotExist return true if err is ErrSettingIsNotExist 61 - func IsErrSettingIsNotExist(err error) bool { 62 - _, ok := err.(ErrSettingIsNotExist) 63 - return ok 64 - } 65 - 66 - // ErrDataExpired represents an error that update a record which has been updated by another thread 67 - type ErrDataExpired struct { 68 - Key string 69 - } 70 - 71 - // Error implements error 72 - func (err ErrDataExpired) Error() string { 73 - return fmt.Sprintf("System setting[%s] has been updated by another thread", err.Key) 74 - } 75 - 76 - // IsErrDataExpired return true if err is ErrDataExpired 77 - func IsErrDataExpired(err error) bool { 78 - _, ok := err.(ErrDataExpired) 79 - return ok 80 - } 81 - 82 - // GetSetting returns specific setting without using the cache 83 - func GetSetting(ctx context.Context, key string) (*Setting, error) { 84 - v, err := GetSettings(ctx, []string{key}) 85 - if err != nil { 86 - return nil, err 38 + func GetRevision(ctx context.Context) int { 39 + revision := &Setting{SettingKey: keyRevision} 40 + if has, err := db.GetByBean(ctx, revision); err != nil { 41 + return 0 42 + } else if !has { 43 + err = db.Insert(ctx, &Setting{SettingKey: keyRevision, Version: 1}) 44 + if err != nil { 45 + return 0 46 + } 47 + return 1 48 + } else if revision.Version <= 0 || revision.Version >= math.MaxInt-1 { 49 + _, err = db.Exec(ctx, "UPDATE system_setting SET version=1 WHERE setting_key=?", keyRevision) 50 + if err != nil { 51 + return 0 52 + } 53 + return 1 87 54 } 88 - if len(v) == 0 { 89 - return nil, ErrSettingIsNotExist{key} 90 - } 91 - return v[strings.ToLower(key)], nil 55 + return revision.Version 92 56 } 93 57 94 - const contextCacheKey = "system_setting" 95 - 96 - // GetSettingWithCache returns the setting value via the key 97 - func GetSettingWithCache(ctx context.Context, key, defaultVal string) (string, error) { 98 - return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) { 99 - return cache.GetString(genSettingCacheKey(key), func() (string, error) { 100 - res, err := GetSetting(ctx, key) 101 - if err != nil { 102 - if IsErrSettingIsNotExist(err) { 103 - return defaultVal, nil 104 - } 105 - return "", err 106 - } 107 - return res.SettingValue, nil 108 - }) 109 - }) 110 - } 111 - 112 - // GetSettingBool return bool value of setting, 113 - // none existing keys and errors are ignored and result in false 114 - func GetSettingBool(ctx context.Context, key string, defaultVal bool) (bool, error) { 115 - s, err := GetSetting(ctx, key) 116 - switch { 117 - case err == nil: 118 - v, _ := strconv.ParseBool(s.SettingValue) 119 - return v, nil 120 - case IsErrSettingIsNotExist(err): 121 - return defaultVal, nil 122 - default: 123 - return false, err 124 - } 125 - } 126 - 127 - func GetSettingWithCacheBool(ctx context.Context, key string, defaultVal bool) bool { 128 - s, _ := GetSettingWithCache(ctx, key, strconv.FormatBool(defaultVal)) 129 - v, _ := strconv.ParseBool(s) 130 - return v 131 - } 132 - 133 - // GetSettings returns specific settings 134 - func GetSettings(ctx context.Context, keys []string) (map[string]*Setting, error) { 135 - for i := 0; i < len(keys); i++ { 136 - keys[i] = strings.ToLower(keys[i]) 137 - } 138 - settings := make([]*Setting, 0, len(keys)) 58 + func GetAllSettings(ctx context.Context) (revision int, res map[string]string, err error) { 59 + _ = GetRevision(ctx) // prepare the "revision" key ahead 60 + var settings []*Setting 139 61 if err := db.GetEngine(ctx). 140 - Where(builder.In("setting_key", keys)). 141 62 Find(&settings); err != nil { 142 - return nil, err 63 + return 0, nil, err 143 64 } 144 - settingsMap := make(map[string]*Setting) 65 + res = make(map[string]string) 145 66 for _, s := range settings { 146 - settingsMap[s.SettingKey] = s 147 - } 148 - return settingsMap, nil 149 - } 150 - 151 - type AllSettings map[string]*Setting 152 - 153 - func (settings AllSettings) Get(key string) Setting { 154 - if v, ok := settings[strings.ToLower(key)]; ok { 155 - return *v 156 - } 157 - return Setting{} 158 - } 159 - 160 - func (settings AllSettings) GetBool(key string) bool { 161 - b, _ := strconv.ParseBool(settings.Get(key).SettingValue) 162 - return b 163 - } 164 - 165 - func (settings AllSettings) GetVersion(key string) int { 166 - return settings.Get(key).Version 167 - } 168 - 169 - // GetAllSettings returns all settings from user 170 - func GetAllSettings(ctx context.Context) (AllSettings, error) { 171 - settings := make([]*Setting, 0, 5) 172 - if err := db.GetEngine(ctx). 173 - Find(&settings); err != nil { 174 - return nil, err 175 - } 176 - settingsMap := make(map[string]*Setting) 177 - for _, s := range settings { 178 - settingsMap[s.SettingKey] = s 179 - } 180 - return settingsMap, nil 181 - } 182 - 183 - // DeleteSetting deletes a specific setting for a user 184 - func DeleteSetting(ctx context.Context, setting *Setting) error { 185 - cache.RemoveContextData(ctx, contextCacheKey, setting.SettingKey) 186 - cache.Remove(genSettingCacheKey(setting.SettingKey)) 187 - _, err := db.GetEngine(ctx).Delete(setting) 188 - return err 189 - } 190 - 191 - func SetSettingNoVersion(ctx context.Context, key, value string) error { 192 - s, err := GetSetting(ctx, key) 193 - if IsErrSettingIsNotExist(err) { 194 - return SetSetting(ctx, &Setting{ 195 - SettingKey: key, 196 - SettingValue: value, 197 - }) 198 - } 199 - if err != nil { 200 - return err 201 - } 202 - s.SettingValue = value 203 - return SetSetting(ctx, s) 204 - } 205 - 206 - // SetSetting updates a users' setting for a specific key 207 - func SetSetting(ctx context.Context, setting *Setting) error { 208 - if err := upsertSettingValue(ctx, strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil { 209 - return err 210 - } 211 - 212 - setting.Version++ 213 - 214 - cc := cache.GetCache() 215 - if cc != nil { 216 - if err := cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds()); err != nil { 217 - return err 67 + if s.SettingKey == keyRevision { 68 + revision = s.Version 218 69 } 70 + res[s.SettingKey] = s.SettingValue 219 71 } 220 - cache.SetContextData(ctx, contextCacheKey, setting.SettingKey, setting.SettingValue) 221 - return nil 72 + return revision, res, nil 222 73 } 223 74 224 - func upsertSettingValue(parentCtx context.Context, key, value string, version int) error { 225 - return db.WithTx(parentCtx, func(ctx context.Context) error { 75 + func SetSettings(ctx context.Context, settings map[string]string) error { 76 + _ = GetRevision(ctx) // prepare the "revision" key ahead 77 + return db.WithTx(ctx, func(ctx context.Context) error { 226 78 e := db.GetEngine(ctx) 227 - 228 - // here we use a general method to do a safe upsert for different databases (and most transaction levels) 229 - // 1. try to UPDATE the record and acquire the transaction write lock 230 - // if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly 231 - // if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed 232 - // 2. do a SELECT to check if the row exists or not (we already have the transaction lock) 233 - // 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe) 234 - // 235 - // to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1` 236 - // to make sure the UPDATE always returns a non-zero value for existing (unchanged) records. 237 - 238 - res, err := e.Exec("UPDATE system_setting SET setting_value=?, version = version+1 WHERE setting_key=? AND version=?", value, key, version) 79 + _, err := db.Exec(ctx, "UPDATE system_setting SET version=version+1 WHERE setting_key=?", keyRevision) 239 80 if err != nil { 240 81 return err 241 82 } 242 - rows, _ := res.RowsAffected() 243 - if rows > 0 { 244 - // the existing row is updated, so we can return 245 - return nil 246 - } 247 - 248 - // in case the value isn't changed, update would return 0 rows changed, so we need this check 249 - has, err := e.Exist(&Setting{SettingKey: key}) 250 - if err != nil { 251 - return err 252 - } 253 - if has { 254 - return ErrDataExpired{Key: key} 83 + for k, v := range settings { 84 + res, err := e.Exec("UPDATE system_setting SET setting_value=? WHERE setting_key=?", v, k) 85 + if err != nil { 86 + return err 87 + } 88 + rows, _ := res.RowsAffected() 89 + if rows == 0 { // if no existing row, insert a new row 90 + if _, err = e.Insert(&Setting{SettingKey: k, SettingValue: v}); err != nil { 91 + return err 92 + } 93 + } 255 94 } 256 - 257 - // if no existing row, insert a new row 258 - _, err = e.Insert(&Setting{SettingKey: key, SettingValue: value}) 259 - return err 95 + return nil 260 96 }) 261 97 } 262 98 263 - var ( 264 - GravatarSourceURL *url.URL 265 - LibravatarService *libravatar.Libravatar 266 - ) 99 + type dbConfigCachedGetter struct { 100 + mu sync.RWMutex 267 101 268 - func Init(ctx context.Context) error { 269 - disableGravatar, err := GetSettingBool(ctx, KeyPictureDisableGravatar, setting_module.GetDefaultDisableGravatar()) 270 - if err != nil { 271 - return err 272 - } 102 + cacheTime time.Time 103 + revision int 104 + settings map[string]string 105 + } 273 106 274 - enableFederatedAvatar, err := GetSettingBool(ctx, KeyPictureEnableFederatedAvatar, setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)) 275 - if err != nil { 276 - return err 277 - } 107 + var _ config.DynKeyGetter = (*dbConfigCachedGetter)(nil) 278 108 279 - if setting_module.OfflineMode { 280 - if !disableGravatar { 281 - if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil { 282 - return fmt.Errorf("failed to set setting %q: %w", KeyPictureDisableGravatar, err) 283 - } 284 - } 285 - disableGravatar = true 109 + func (d *dbConfigCachedGetter) GetValue(ctx context.Context, key string) (v string, has bool) { 110 + d.mu.RLock() 111 + defer d.mu.RUnlock() 112 + v, has = d.settings[key] 113 + return v, has 114 + } 286 115 287 - if enableFederatedAvatar { 288 - if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil { 289 - return fmt.Errorf("failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err) 290 - } 291 - } 292 - enableFederatedAvatar = false 116 + func (d *dbConfigCachedGetter) GetRevision(ctx context.Context) int { 117 + d.mu.RLock() 118 + defer d.mu.RUnlock() 119 + if time.Since(d.cacheTime) < time.Second { 120 + return d.revision 293 121 } 294 - 295 - if enableFederatedAvatar || !disableGravatar { 296 - var err error 297 - GravatarSourceURL, err = url.Parse(setting_module.GravatarSource) 122 + if GetRevision(ctx) != d.revision { 123 + d.mu.RUnlock() 124 + d.mu.Lock() 125 + rev, set, err := GetAllSettings(ctx) 298 126 if err != nil { 299 - return fmt.Errorf("failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err) 127 + log.Error("Unable to get all settings: %v", err) 128 + } else { 129 + d.cacheTime = time.Now() 130 + d.revision = rev 131 + d.settings = set 300 132 } 133 + d.mu.Unlock() 134 + d.mu.RLock() 301 135 } 136 + return d.revision 137 + } 302 138 303 - if GravatarSourceURL != nil && enableFederatedAvatar { 304 - LibravatarService = libravatar.New() 305 - if GravatarSourceURL.Scheme == "https" { 306 - LibravatarService.SetUseHTTPS(true) 307 - LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host) 308 - } else { 309 - LibravatarService.SetUseHTTPS(false) 310 - LibravatarService.SetFallbackHost(GravatarSourceURL.Host) 311 - } 312 - } 313 - return nil 139 + func (d *dbConfigCachedGetter) InvalidateCache() { 140 + d.mu.Lock() 141 + d.cacheTime = time.Time{} 142 + d.mu.Unlock() 143 + } 144 + 145 + func NewDatabaseDynKeyGetter() config.DynKeyGetter { 146 + return &dbConfigCachedGetter{} 314 147 }
-15
models/system/setting_key.go
··· 1 - // Copyright 2022 The Gitea Authors. All rights reserved. 2 - // SPDX-License-Identifier: MIT 3 - 4 - package system 5 - 6 - // enumerate all system setting keys 7 - const ( 8 - KeyPictureDisableGravatar = "picture.disable_gravatar" 9 - KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar" 10 - ) 11 - 12 - // genSettingCacheKey returns the cache key for some configuration 13 - func genSettingCacheKey(key string) string { 14 - return "system.setting." + key 15 - }
+14 -29
models/system/setting_test.go
··· 4 4 package system_test 5 5 6 6 import ( 7 - "strings" 8 7 "testing" 9 8 10 9 "code.gitea.io/gitea/models/db" ··· 15 14 ) 16 15 17 16 func TestSettings(t *testing.T) { 18 - keyName := "server.LFS_LOCKS_PAGING_NUM" 17 + keyName := "test.key" 19 18 assert.NoError(t, unittest.PrepareTestDatabase()) 20 19 21 - newSetting := &system.Setting{SettingKey: keyName, SettingValue: "50"} 20 + assert.NoError(t, db.TruncateBeans(db.DefaultContext, &system.Setting{})) 22 21 23 - // create setting 24 - err := system.SetSetting(db.DefaultContext, newSetting) 25 - assert.NoError(t, err) 26 - // test about saving unchanged values 27 - err = system.SetSetting(db.DefaultContext, newSetting) 22 + rev, settings, err := system.GetAllSettings(db.DefaultContext) 28 23 assert.NoError(t, err) 24 + assert.EqualValues(t, 1, rev) 25 + assert.Len(t, settings, 1) // there is only one "revision" key 29 26 30 - // get specific setting 31 - settings, err := system.GetSettings(db.DefaultContext, []string{keyName}) 27 + err = system.SetSettings(db.DefaultContext, map[string]string{keyName: "true"}) 32 28 assert.NoError(t, err) 33 - assert.Len(t, settings, 1) 34 - assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue) 35 - 36 - // updated setting 37 - updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: settings[strings.ToLower(keyName)].Version} 38 - err = system.SetSetting(db.DefaultContext, updatedSetting) 29 + rev, settings, err = system.GetAllSettings(db.DefaultContext) 39 30 assert.NoError(t, err) 31 + assert.EqualValues(t, 2, rev) 32 + assert.Len(t, settings, 2) 33 + assert.EqualValues(t, "true", settings[keyName]) 40 34 41 - value, err := system.GetSetting(db.DefaultContext, keyName) 35 + err = system.SetSettings(db.DefaultContext, map[string]string{keyName: "false"}) 42 36 assert.NoError(t, err) 43 - assert.EqualValues(t, updatedSetting.SettingValue, value.SettingValue) 44 - 45 - // get all settings 46 - settings, err = system.GetAllSettings(db.DefaultContext) 37 + rev, settings, err = system.GetAllSettings(db.DefaultContext) 47 38 assert.NoError(t, err) 48 - assert.Len(t, settings, 3) 49 - assert.EqualValues(t, updatedSetting.SettingValue, settings[strings.ToLower(updatedSetting.SettingKey)].SettingValue) 50 - 51 - // delete setting 52 - err = system.DeleteSetting(db.DefaultContext, &system.Setting{SettingKey: strings.ToLower(keyName)}) 53 - assert.NoError(t, err) 54 - settings, err = system.GetAllSettings(db.DefaultContext) 55 - assert.NoError(t, err) 39 + assert.EqualValues(t, 3, rev) 56 40 assert.Len(t, settings, 2) 41 + assert.EqualValues(t, "false", settings[keyName]) 57 42 }
+4 -5
models/unittest/testdb.go
··· 13 13 "testing" 14 14 15 15 "code.gitea.io/gitea/models/db" 16 - system_model "code.gitea.io/gitea/models/system" 16 + "code.gitea.io/gitea/models/system" 17 17 "code.gitea.io/gitea/modules/auth/password/hash" 18 18 "code.gitea.io/gitea/modules/base" 19 19 "code.gitea.io/gitea/modules/git" 20 20 "code.gitea.io/gitea/modules/setting" 21 + "code.gitea.io/gitea/modules/setting/config" 21 22 "code.gitea.io/gitea/modules/storage" 22 23 "code.gitea.io/gitea/modules/util" 23 24 ··· 145 146 146 147 setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost" 147 148 149 + config.SetDynGetter(system.NewDatabaseDynKeyGetter()) 150 + 148 151 if err = storage.Init(); err != nil { 149 152 fatalTestError("storage.Init: %v\n", err) 150 153 } 151 - if err = system_model.Init(db.DefaultContext); err != nil { 152 - fatalTestError("models.Init: %v\n", err) 153 - } 154 - 155 154 if err = util.RemoveAll(repoRootPath); err != nil { 156 155 fatalTestError("util.RemoveAll: %v\n", err) 157 156 }
+1 -4
models/user/avatar.go
··· 13 13 14 14 "code.gitea.io/gitea/models/avatars" 15 15 "code.gitea.io/gitea/models/db" 16 - system_model "code.gitea.io/gitea/models/system" 17 16 "code.gitea.io/gitea/modules/avatar" 18 17 "code.gitea.io/gitea/modules/log" 19 18 "code.gitea.io/gitea/modules/setting" ··· 67 66 useLocalAvatar := false 68 67 autoGenerateAvatar := false 69 68 70 - disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, 71 - setting.GetDefaultDisableGravatar(), 72 - ) 69 + disableGravatar := setting.Config().Picture.DisableGravatar.Value(ctx) 73 70 74 71 switch { 75 72 case u.UseCustomAvatar:
+1 -8
modules/repository/commits_test.go
··· 12 12 13 13 "code.gitea.io/gitea/models/db" 14 14 repo_model "code.gitea.io/gitea/models/repo" 15 - system_model "code.gitea.io/gitea/models/system" 16 15 "code.gitea.io/gitea/models/unittest" 17 16 "code.gitea.io/gitea/modules/git" 18 17 "code.gitea.io/gitea/modules/setting" ··· 103 102 assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified) 104 103 } 105 104 106 - func initGravatarSource(t *testing.T) { 107 - setting.GravatarSource = "https://secure.gravatar.com/avatar" 108 - err := system_model.Init(db.DefaultContext) 109 - assert.NoError(t, err) 110 - } 111 - 112 105 func TestPushCommits_AvatarLink(t *testing.T) { 113 106 assert.NoError(t, unittest.PrepareTestDatabase()) 114 107 ··· 132 125 }, 133 126 } 134 127 135 - initGravatarSource(t) 128 + setting.GravatarSource = "https://secure.gravatar.com/avatar" 136 129 137 130 assert.Equal(t, 138 131 "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor),
+55
modules/setting/config.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package setting 5 + 6 + import ( 7 + "sync" 8 + 9 + "code.gitea.io/gitea/modules/log" 10 + "code.gitea.io/gitea/modules/setting/config" 11 + ) 12 + 13 + type PictureStruct struct { 14 + DisableGravatar *config.Value[bool] 15 + EnableFederatedAvatar *config.Value[bool] 16 + } 17 + 18 + type ConfigStruct struct { 19 + Picture *PictureStruct 20 + } 21 + 22 + var ( 23 + defaultConfig *ConfigStruct 24 + defaultConfigOnce sync.Once 25 + ) 26 + 27 + func initDefaultConfig() { 28 + config.SetCfgSecKeyGetter(&cfgSecKeyGetter{}) 29 + defaultConfig = &ConfigStruct{ 30 + Picture: &PictureStruct{ 31 + DisableGravatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"), 32 + EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"), 33 + }, 34 + } 35 + } 36 + 37 + func Config() *ConfigStruct { 38 + defaultConfigOnce.Do(initDefaultConfig) 39 + return defaultConfig 40 + } 41 + 42 + type cfgSecKeyGetter struct{} 43 + 44 + func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) { 45 + cfgSec, err := CfgProvider.GetSection(sec) 46 + if err != nil { 47 + log.Error("Unable to get config section: %q", sec) 48 + return "", false 49 + } 50 + cfgKey := ConfigSectionKey(cfgSec, key) 51 + if cfgKey == nil { 52 + return "", false 53 + } 54 + return cfgKey.Value(), true 55 + }
+49
modules/setting/config/getter.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package config 5 + 6 + import ( 7 + "context" 8 + "sync" 9 + ) 10 + 11 + var getterMu sync.RWMutex 12 + 13 + type CfgSecKeyGetter interface { 14 + GetValue(sec, key string) (v string, has bool) 15 + } 16 + 17 + var cfgSecKeyGetterInternal CfgSecKeyGetter 18 + 19 + func SetCfgSecKeyGetter(p CfgSecKeyGetter) { 20 + getterMu.Lock() 21 + cfgSecKeyGetterInternal = p 22 + getterMu.Unlock() 23 + } 24 + 25 + func GetCfgSecKeyGetter() CfgSecKeyGetter { 26 + getterMu.RLock() 27 + defer getterMu.RUnlock() 28 + return cfgSecKeyGetterInternal 29 + } 30 + 31 + type DynKeyGetter interface { 32 + GetValue(ctx context.Context, key string) (v string, has bool) 33 + GetRevision(ctx context.Context) int 34 + InvalidateCache() 35 + } 36 + 37 + var dynKeyGetterInternal DynKeyGetter 38 + 39 + func SetDynGetter(p DynKeyGetter) { 40 + getterMu.Lock() 41 + dynKeyGetterInternal = p 42 + getterMu.Unlock() 43 + } 44 + 45 + func GetDynGetter() DynKeyGetter { 46 + getterMu.RLock() 47 + defer getterMu.RUnlock() 48 + return dynKeyGetterInternal 49 + }
+81
modules/setting/config/value.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package config 5 + 6 + import ( 7 + "context" 8 + "strconv" 9 + "sync" 10 + ) 11 + 12 + type CfgSecKey struct { 13 + Sec, Key string 14 + } 15 + 16 + type Value[T any] struct { 17 + mu sync.RWMutex 18 + 19 + cfgSecKey CfgSecKey 20 + dynKey string 21 + 22 + def, value T 23 + revision int 24 + } 25 + 26 + func (value *Value[T]) parse(s string) (v T) { 27 + switch any(v).(type) { 28 + case bool: 29 + b, _ := strconv.ParseBool(s) 30 + return any(b).(T) 31 + default: 32 + panic("unsupported config type, please complete the code") 33 + } 34 + } 35 + 36 + func (value *Value[T]) Value(ctx context.Context) (v T) { 37 + dg := GetDynGetter() 38 + if dg == nil { 39 + // this is an edge case: the database is not initialized but the system setting is going to be used 40 + // it should panic to avoid inconsistent config values (from config / system setting) and fix the code 41 + panic("no config dyn value getter") 42 + } 43 + 44 + rev := dg.GetRevision(ctx) 45 + 46 + // if the revision in database doesn't change, use the last value 47 + value.mu.RLock() 48 + if rev == value.revision { 49 + v = value.value 50 + value.mu.RUnlock() 51 + return v 52 + } 53 + value.mu.RUnlock() 54 + 55 + // try to parse the config and cache it 56 + var valStr *string 57 + if dynVal, has := dg.GetValue(ctx, value.dynKey); has { 58 + valStr = &dynVal 59 + } else if cfgVal, has := GetCfgSecKeyGetter().GetValue(value.cfgSecKey.Sec, value.cfgSecKey.Key); has { 60 + valStr = &cfgVal 61 + } 62 + if valStr == nil { 63 + v = value.def 64 + } else { 65 + v = value.parse(*valStr) 66 + } 67 + 68 + value.mu.Lock() 69 + value.value = v 70 + value.revision = rev 71 + value.mu.Unlock() 72 + return v 73 + } 74 + 75 + func (value *Value[T]) DynKey() string { 76 + return value.dynKey 77 + } 78 + 79 + func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] { 80 + return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey} 81 + }
-1
options/locale/locale_en-US.ini
··· 3157 3157 config.access_log_template = Access Log Template 3158 3158 config.xorm_log_sql = Log SQL 3159 3159 3160 - config.get_setting_failed = Get setting %s failed 3161 3160 config.set_setting_failed = Set setting %s failed 3162 3161 3163 3162 monitor.stats = Stats
+3
routers/common/db.go
··· 10 10 11 11 "code.gitea.io/gitea/models/db" 12 12 "code.gitea.io/gitea/models/migrations" 13 + system_model "code.gitea.io/gitea/models/system" 13 14 "code.gitea.io/gitea/modules/log" 14 15 "code.gitea.io/gitea/modules/setting" 16 + "code.gitea.io/gitea/modules/setting/config" 15 17 16 18 "xorm.io/xorm" 17 19 ) ··· 36 38 time.Sleep(setting.Database.DBConnectBackoff) 37 39 } 38 40 db.HasEngine = true 41 + config.SetDynGetter(system_model.NewDatabaseDynKeyGetter()) 39 42 return nil 40 43 } 41 44
+5 -6
routers/install/install.go
··· 430 430 cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(fmt.Sprint(form.MailNotify)) 431 431 432 432 cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode)) 433 - // if you are reinstalling, this maybe not right because of missing version 434 - if err := system_model.SetSettingNoVersion(ctx, system_model.KeyPictureDisableGravatar, strconv.FormatBool(form.DisableGravatar)); err != nil { 433 + if err := system_model.SetSettings(ctx, map[string]string{ 434 + setting.Config().Picture.DisableGravatar.DynKey(): strconv.FormatBool(form.DisableGravatar), 435 + setting.Config().Picture.EnableFederatedAvatar.DynKey(): strconv.FormatBool(form.EnableFederatedAvatar), 436 + }); err != nil { 435 437 ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) 436 438 return 437 439 } 438 - if err := system_model.SetSettingNoVersion(ctx, system_model.KeyPictureEnableFederatedAvatar, strconv.FormatBool(form.EnableFederatedAvatar)); err != nil { 439 - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) 440 - return 441 - } 440 + 442 441 cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn)) 443 442 cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp)) 444 443 cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration))
+13 -54
routers/web/admin/config.go
··· 5 5 package admin 6 6 7 7 import ( 8 - "fmt" 9 8 "net/http" 10 9 "net/url" 11 - "strconv" 12 10 "strings" 13 11 14 12 system_model "code.gitea.io/gitea/models/system" 15 13 "code.gitea.io/gitea/modules/base" 14 + "code.gitea.io/gitea/modules/container" 16 15 "code.gitea.io/gitea/modules/context" 17 16 "code.gitea.io/gitea/modules/git" 18 17 "code.gitea.io/gitea/modules/json" 19 18 "code.gitea.io/gitea/modules/log" 20 19 "code.gitea.io/gitea/modules/setting" 20 + "code.gitea.io/gitea/modules/setting/config" 21 21 "code.gitea.io/gitea/modules/util" 22 22 "code.gitea.io/gitea/services/mailer" 23 23 ··· 101 101 ctx.Data["Title"] = ctx.Tr("admin.config") 102 102 ctx.Data["PageIsAdminConfig"] = true 103 103 104 - systemSettings, err := system_model.GetAllSettings(ctx) 105 - if err != nil { 106 - ctx.ServerError("system_model.GetAllSettings", err) 107 - return 108 - } 109 - 110 - // All editable settings from UI 111 - ctx.Data["SystemSettings"] = systemSettings 112 - ctx.PageData["adminConfigPage"] = true 113 - 114 104 ctx.Data["CustomConf"] = setting.CustomConf 115 105 ctx.Data["AppUrl"] = setting.AppURL 116 106 ctx.Data["AppBuiltWith"] = setting.AppBuiltWith ··· 170 160 ctx.Data["LogSQL"] = setting.Database.LogSQL 171 161 172 162 ctx.Data["Loggers"] = log.GetManager().DumpLoggers() 173 - 163 + config.GetDynGetter().InvalidateCache() 164 + ctx.Data["SystemConfig"] = setting.Config() 174 165 prepareDeprecatedWarningsAlert(ctx) 175 166 176 167 ctx.HTML(http.StatusOK, tplConfig) ··· 178 169 179 170 func ChangeConfig(ctx *context.Context) { 180 171 key := strings.TrimSpace(ctx.FormString("key")) 181 - if key == "" { 182 - ctx.JSONRedirect(ctx.Req.URL.String()) 172 + value := ctx.FormString("value") 173 + cfg := setting.Config() 174 + allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey()) 175 + if !allowedKeys.Contains(key) { 176 + ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) 183 177 return 184 178 } 185 - value := ctx.FormString("value") 186 - version := ctx.FormInt("version") 187 - 188 - if check, ok := changeConfigChecks[key]; ok { 189 - if err := check(ctx, value); err != nil { 190 - log.Warn("refused to set setting: %v", err) 191 - ctx.JSON(http.StatusOK, map[string]string{ 192 - "err": ctx.Tr("admin.config.set_setting_failed", key), 193 - }) 194 - return 195 - } 196 - } 197 - 198 - if err := system_model.SetSetting(ctx, &system_model.Setting{ 199 - SettingKey: key, 200 - SettingValue: value, 201 - Version: version, 202 - }); err != nil { 179 + if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil { 203 180 log.Error("set setting failed: %v", err) 204 - ctx.JSON(http.StatusOK, map[string]string{ 205 - "err": ctx.Tr("admin.config.set_setting_failed", key), 206 - }) 181 + ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) 207 182 return 208 183 } 209 184 210 - ctx.JSON(http.StatusOK, map[string]any{ 211 - "version": version + 1, 212 - }) 213 - } 214 - 215 - var changeConfigChecks = map[string]func(ctx *context.Context, newValue string) error{ 216 - system_model.KeyPictureDisableGravatar: func(_ *context.Context, newValue string) error { 217 - if v, _ := strconv.ParseBool(newValue); setting.OfflineMode && !v { 218 - return fmt.Errorf("%q should be true when OFFLINE_MODE is true", system_model.KeyPictureDisableGravatar) 219 - } 220 - return nil 221 - }, 222 - system_model.KeyPictureEnableFederatedAvatar: func(_ *context.Context, newValue string) error { 223 - if v, _ := strconv.ParseBool(newValue); setting.OfflineMode && v { 224 - return fmt.Errorf("%q cannot be false when OFFLINE_MODE is true", system_model.KeyPictureEnableFederatedAvatar) 225 - } 226 - return nil 227 - }, 185 + config.GetDynGetter().InvalidateCache() 186 + ctx.JSONOK() 228 187 }
+8 -14
routers/web/admin/users.go
··· 15 15 "code.gitea.io/gitea/models/db" 16 16 org_model "code.gitea.io/gitea/models/organization" 17 17 repo_model "code.gitea.io/gitea/models/repo" 18 - system_model "code.gitea.io/gitea/models/system" 19 18 user_model "code.gitea.io/gitea/models/user" 20 19 "code.gitea.io/gitea/modules/auth/password" 21 20 "code.gitea.io/gitea/modules/base" ··· 308 307 ctx.HTML(http.StatusOK, tplUserView) 309 308 } 310 309 311 - // EditUser show editing user page 312 - func EditUser(ctx *context.Context) { 310 + func editUserCommon(ctx *context.Context) { 313 311 ctx.Data["Title"] = ctx.Tr("admin.users.edit_account") 314 312 ctx.Data["PageIsAdminUsers"] = true 315 313 ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation 316 314 ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations 317 315 ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() 318 - ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, 319 - setting.GetDefaultDisableGravatar(), 320 - ) 316 + ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) 317 + } 321 318 319 + // EditUser show editing user page 320 + func EditUser(ctx *context.Context) { 321 + editUserCommon(ctx) 322 322 prepareUserInfo(ctx) 323 323 if ctx.Written() { 324 324 return ··· 329 329 330 330 // EditUserPost response for editing user 331 331 func EditUserPost(ctx *context.Context) { 332 - form := web.GetForm(ctx).(*forms.AdminEditUserForm) 333 - ctx.Data["Title"] = ctx.Tr("admin.users.edit_account") 334 - ctx.Data["PageIsAdminUsers"] = true 335 - ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations 336 - ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() 337 - ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, 338 - setting.GetDefaultDisableGravatar()) 339 - 332 + editUserCommon(ctx) 340 333 u := prepareUserInfo(ctx) 341 334 if ctx.Written() { 342 335 return 343 336 } 344 337 338 + form := web.GetForm(ctx).(*forms.AdminEditUserForm) 345 339 if ctx.HasError() { 346 340 ctx.HTML(http.StatusOK, tplUserEdit) 347 341 return
+2 -7
routers/web/user/setting/profile.go
··· 17 17 "code.gitea.io/gitea/models/db" 18 18 "code.gitea.io/gitea/models/organization" 19 19 repo_model "code.gitea.io/gitea/models/repo" 20 - system_model "code.gitea.io/gitea/models/system" 21 20 user_model "code.gitea.io/gitea/models/user" 22 21 "code.gitea.io/gitea/modules/base" 23 22 "code.gitea.io/gitea/modules/context" ··· 44 43 ctx.Data["Title"] = ctx.Tr("settings.profile") 45 44 ctx.Data["PageIsSettingsProfile"] = true 46 45 ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() 47 - ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, 48 - setting.GetDefaultDisableGravatar(), 49 - ) 46 + ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) 50 47 51 48 ctx.HTML(http.StatusOK, tplSettingsProfile) 52 49 } ··· 88 85 ctx.Data["Title"] = ctx.Tr("settings") 89 86 ctx.Data["PageIsSettingsProfile"] = true 90 87 ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() 91 - ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, 92 - setting.GetDefaultDisableGravatar(), 93 - ) 88 + ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) 94 89 95 90 if ctx.HasError() { 96 91 ctx.HTML(http.StatusOK, tplSettingsProfile)
+4 -4
templates/admin/config.tmpl
··· 292 292 <dl class="admin-dl-horizontal"> 293 293 <dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt> 294 294 <dd> 295 - <div class="ui toggle checkbox"> 296 - <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"}}"> 295 + <div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}"> 296 + <input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}> 297 297 </div> 298 298 </dd> 299 299 <div class="divider"></div> 300 300 <dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt> 301 301 <dd> 302 - <div class="ui toggle checkbox"> 303 - <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"}}"> 302 + <div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}"> 303 + <input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}> 304 304 </div> 305 305 </dd> 306 306 </dl>
+16 -29
web_src/js/features/admin/config.js
··· 1 - import $ from 'jquery'; 2 1 import {showTemporaryTooltip} from '../../modules/tippy.js'; 2 + import {POST} from '../../modules/fetch.js'; 3 3 4 - const {appSubUrl, csrfToken, pageData} = window.config; 4 + const {appSubUrl} = window.config; 5 5 6 6 export function initAdminConfigs() { 7 - const isAdminConfigPage = pageData?.adminConfigPage; 8 - if (!isAdminConfigPage) return; 7 + const elAdminConfig = document.querySelector('.page-content.admin.config'); 8 + if (!elAdminConfig) return; 9 9 10 - $("input[type='checkbox']").on('change', (e) => { 11 - const $this = $(e.currentTarget); 12 - $.ajax({ 13 - url: `${appSubUrl}/admin/config`, 14 - type: 'POST', 15 - data: { 16 - _csrf: csrfToken, 17 - key: $this.attr('name'), 18 - value: $this.is(':checked'), 19 - version: $this.attr('version'), 20 - } 21 - }).done((resp) => { 22 - if (resp) { 23 - if (resp.redirect) { 24 - window.location.href = resp.redirect; 25 - } else if (resp.version) { 26 - $this.attr('version', resp.version); 27 - } else if (resp.err) { 28 - showTemporaryTooltip(e.currentTarget, resp.err); 29 - $this.prop('checked', !$this.is(':checked')); 30 - } 10 + for (const el of elAdminConfig.querySelectorAll('input[type="checkbox"][data-config-dyn-key]')) { 11 + el.addEventListener('change', async () => { 12 + try { 13 + const resp = await POST(`${appSubUrl}/admin/config`, { 14 + data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key'), value: el.checked}), 15 + }); 16 + const json = await resp.json(); 17 + if (json.errorMessage) throw new Error(json.errorMessage); 18 + } catch (ex) { 19 + showTemporaryTooltip(el, ex.toString()); 20 + el.checked = !el.checked; 31 21 } 32 22 }); 33 - 34 - e.preventDefault(); 35 - return false; 36 - }); 23 + } 37 24 }