Add system setting table with cache and also add cache supports for user setting (#18058)
parent
5d3dbffa15
commit
f860a6d2e4
@ -1,117 +0,0 @@ |
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin_test |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"code.gitea.io/gitea/models/admin" |
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/models/unittest" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestNotice_TrStr(t *testing.T) { |
||||
notice := &admin.Notice{ |
||||
Type: admin.NoticeRepository, |
||||
Description: "test description", |
||||
} |
||||
assert.Equal(t, "admin.notices.type_1", notice.TrStr()) |
||||
} |
||||
|
||||
func TestCreateNotice(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
noticeBean := &admin.Notice{ |
||||
Type: admin.NoticeRepository, |
||||
Description: "test description", |
||||
} |
||||
unittest.AssertNotExistsBean(t, noticeBean) |
||||
assert.NoError(t, admin.CreateNotice(db.DefaultContext, noticeBean.Type, noticeBean.Description)) |
||||
unittest.AssertExistsAndLoadBean(t, noticeBean) |
||||
} |
||||
|
||||
func TestCreateRepositoryNotice(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
noticeBean := &admin.Notice{ |
||||
Type: admin.NoticeRepository, |
||||
Description: "test description", |
||||
} |
||||
unittest.AssertNotExistsBean(t, noticeBean) |
||||
assert.NoError(t, admin.CreateRepositoryNotice(noticeBean.Description)) |
||||
unittest.AssertExistsAndLoadBean(t, noticeBean) |
||||
} |
||||
|
||||
// TODO TestRemoveAllWithNotice
|
||||
|
||||
func TestCountNotices(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
assert.Equal(t, int64(3), admin.CountNotices()) |
||||
} |
||||
|
||||
func TestNotices(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
notices, err := admin.Notices(1, 2) |
||||
assert.NoError(t, err) |
||||
if assert.Len(t, notices, 2) { |
||||
assert.Equal(t, int64(3), notices[0].ID) |
||||
assert.Equal(t, int64(2), notices[1].ID) |
||||
} |
||||
|
||||
notices, err = admin.Notices(2, 2) |
||||
assert.NoError(t, err) |
||||
if assert.Len(t, notices, 1) { |
||||
assert.Equal(t, int64(1), notices[0].ID) |
||||
} |
||||
} |
||||
|
||||
func TestDeleteNotice(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
||||
assert.NoError(t, admin.DeleteNotice(3)) |
||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 3}) |
||||
} |
||||
|
||||
func TestDeleteNotices(t *testing.T) { |
||||
// delete a non-empty range
|
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
||||
assert.NoError(t, admin.DeleteNotices(1, 2)) |
||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 1}) |
||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
||||
} |
||||
|
||||
func TestDeleteNotices2(t *testing.T) { |
||||
// delete an empty range
|
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
||||
assert.NoError(t, admin.DeleteNotices(3, 2)) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
||||
} |
||||
|
||||
func TestDeleteNoticesByIDs(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) |
||||
assert.NoError(t, admin.DeleteNoticesByIDs([]int64{1, 3})) |
||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) |
||||
unittest.AssertNotExistsBean(t, &admin.Notice{ID: 3}) |
||||
} |
@ -0,0 +1,15 @@ |
||||
- |
||||
id: 1 |
||||
setting_key: 'disable_gravatar' |
||||
setting_value: 'false' |
||||
version: 1 |
||||
created: 1653533198 |
||||
updated: 1653533198 |
||||
|
||||
- |
||||
id: 2 |
||||
setting_key: 'enable_federated_avatar' |
||||
setting_value: 'false' |
||||
version: 1 |
||||
created: 1653533198 |
||||
updated: 1653533198 |
@ -0,0 +1,64 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strconv" |
||||
|
||||
"code.gitea.io/gitea/modules/setting" |
||||
"code.gitea.io/gitea/modules/timeutil" |
||||
|
||||
"xorm.io/xorm" |
||||
) |
||||
|
||||
type SystemSetting struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
SettingKey string `xorm:"varchar(255) unique"` // ensure key is always lowercase
|
||||
SettingValue string `xorm:"text"` |
||||
Version int `xorm:"version"` // prevent to override
|
||||
Created timeutil.TimeStamp `xorm:"created"` |
||||
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 { |
||||
if err := x.Sync2(new(SystemSetting)); err != nil { |
||||
return fmt.Errorf("sync2: %v", 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) |
||||
} |
@ -0,0 +1,21 @@ |
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package system_test |
||||
|
||||
import ( |
||||
"path/filepath" |
||||
"testing" |
||||
|
||||
"code.gitea.io/gitea/models/unittest" |
||||
|
||||
_ "code.gitea.io/gitea/models" // register models
|
||||
_ "code.gitea.io/gitea/models/system" // register models of system
|
||||
) |
||||
|
||||
func TestMain(m *testing.M) { |
||||
unittest.MainTest(m, &unittest.TestOptions{ |
||||
GiteaRootPath: filepath.Join("..", ".."), |
||||
}) |
||||
} |
@ -0,0 +1,117 @@ |
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package system_test |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/models/system" |
||||
"code.gitea.io/gitea/models/unittest" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestNotice_TrStr(t *testing.T) { |
||||
notice := &system.Notice{ |
||||
Type: system.NoticeRepository, |
||||
Description: "test description", |
||||
} |
||||
assert.Equal(t, "admin.notices.type_1", notice.TrStr()) |
||||
} |
||||
|
||||
func TestCreateNotice(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
noticeBean := &system.Notice{ |
||||
Type: system.NoticeRepository, |
||||
Description: "test description", |
||||
} |
||||
unittest.AssertNotExistsBean(t, noticeBean) |
||||
assert.NoError(t, system.CreateNotice(db.DefaultContext, noticeBean.Type, noticeBean.Description)) |
||||
unittest.AssertExistsAndLoadBean(t, noticeBean) |
||||
} |
||||
|
||||
func TestCreateRepositoryNotice(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
noticeBean := &system.Notice{ |
||||
Type: system.NoticeRepository, |
||||
Description: "test description", |
||||
} |
||||
unittest.AssertNotExistsBean(t, noticeBean) |
||||
assert.NoError(t, system.CreateRepositoryNotice(noticeBean.Description)) |
||||
unittest.AssertExistsAndLoadBean(t, noticeBean) |
||||
} |
||||
|
||||
// TODO TestRemoveAllWithNotice
|
||||
|
||||
func TestCountNotices(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
assert.Equal(t, int64(3), system.CountNotices()) |
||||
} |
||||
|
||||
func TestNotices(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
notices, err := system.Notices(1, 2) |
||||
assert.NoError(t, err) |
||||
if assert.Len(t, notices, 2) { |
||||
assert.Equal(t, int64(3), notices[0].ID) |
||||
assert.Equal(t, int64(2), notices[1].ID) |
||||
} |
||||
|
||||
notices, err = system.Notices(2, 2) |
||||
assert.NoError(t, err) |
||||
if assert.Len(t, notices, 1) { |
||||
assert.Equal(t, int64(1), notices[0].ID) |
||||
} |
||||
} |
||||
|
||||
func TestDeleteNotice(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||
assert.NoError(t, system.DeleteNotice(3)) |
||||
unittest.AssertNotExistsBean(t, &system.Notice{ID: 3}) |
||||
} |
||||
|
||||
func TestDeleteNotices(t *testing.T) { |
||||
// delete a non-empty range
|
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||
assert.NoError(t, system.DeleteNotices(1, 2)) |
||||
unittest.AssertNotExistsBean(t, &system.Notice{ID: 1}) |
||||
unittest.AssertNotExistsBean(t, &system.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||
} |
||||
|
||||
func TestDeleteNotices2(t *testing.T) { |
||||
// delete an empty range
|
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||
assert.NoError(t, system.DeleteNotices(3, 2)) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||
} |
||||
|
||||
func TestDeleteNoticesByIDs(t *testing.T) { |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) |
||||
assert.NoError(t, system.DeleteNoticesByIDs([]int64{1, 3})) |
||||
unittest.AssertNotExistsBean(t, &system.Notice{ID: 1}) |
||||
unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) |
||||
unittest.AssertNotExistsBean(t, &system.Notice{ID: 3}) |
||||
} |
@ -0,0 +1,261 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package system |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net/url" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
"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 { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
SettingKey string `xorm:"varchar(255) unique"` // ensure key is always lowercase
|
||||
SettingValue string `xorm:"text"` |
||||
Version int `xorm:"version"` // prevent to override
|
||||
Created timeutil.TimeStamp `xorm:"created"` |
||||
Updated timeutil.TimeStamp `xorm:"updated"` |
||||
} |
||||
|
||||
// TableName sets the table name for the settings struct
|
||||
func (s *Setting) TableName() string { |
||||
return "system_setting" |
||||
} |
||||
|
||||
func (s *Setting) GetValueBool() bool { |
||||
b, _ := strconv.ParseBool(s.SettingValue) |
||||
return b |
||||
} |
||||
|
||||
func init() { |
||||
db.RegisterModel(new(Setting)) |
||||
} |
||||
|
||||
// ErrSettingIsNotExist represents an error that a setting is not exist with special key
|
||||
type ErrSettingIsNotExist struct { |
||||
Key string |
||||
} |
||||
|
||||
// Error implements error
|
||||
func (err ErrSettingIsNotExist) Error() string { |
||||
return fmt.Sprintf("System setting[%s] is not exist", err.Key) |
||||
} |
||||
|
||||
// IsErrSettingIsNotExist return true if err is ErrSettingIsNotExist
|
||||
func IsErrSettingIsNotExist(err error) bool { |
||||
_, ok := err.(ErrSettingIsNotExist) |
||||
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
|
||||
func GetSetting(key string) (*Setting, error) { |
||||
v, err := GetSettings([]string{key}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if len(v) == 0 { |
||||
return nil, ErrSettingIsNotExist{key} |
||||
} |
||||
return v[key], nil |
||||
} |
||||
|
||||
// GetSettings returns specific settings
|
||||
func GetSettings(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(db.DefaultContext). |
||||
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[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() (AllSettings, error) { |
||||
settings := make([]*Setting, 0, 5) |
||||
if err := db.GetEngine(db.DefaultContext). |
||||
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(setting *Setting) error { |
||||
_, err := db.GetEngine(db.DefaultContext).Delete(setting) |
||||
return err |
||||
} |
||||
|
||||
func SetSettingNoVersion(key, value string) error { |
||||
s, err := GetSetting(key) |
||||
if IsErrSettingIsNotExist(err) { |
||||
return SetSetting(&Setting{ |
||||
SettingKey: key, |
||||
SettingValue: value, |
||||
}) |
||||
} |
||||
if err != nil { |
||||
return err |
||||
} |
||||
s.SettingValue = value |
||||
return SetSetting(s) |
||||
} |
||||
|
||||
// SetSetting updates a users' setting for a specific key
|
||||
func SetSetting(setting *Setting) error { |
||||
if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil { |
||||
return err |
||||
} |
||||
setting.Version++ |
||||
return nil |
||||
} |
||||
|
||||
func upsertSettingValue(key, value string, version int) error { |
||||
return db.WithTx(func(ctx context.Context) error { |
||||
e := db.GetEngine(ctx) |
||||
|
||||
// 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 { |
||||
return err |
||||
} |
||||
rows, _ := res.RowsAffected() |
||||
if rows > 0 { |
||||
// the existing row is updated, so we can return
|
||||
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 ( |
||||
GravatarSourceURL *url.URL |
||||
LibravatarService *libravatar.Libravatar |
||||
) |
||||
|
||||
func Init() error { |
||||
var disableGravatar bool |
||||
disableGravatarSetting, err := GetSetting(KeyPictureDisableGravatar) |
||||
if IsErrSettingIsNotExist(err) { |
||||
disableGravatar = setting.GetDefaultDisableGravatar() |
||||
disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)} |
||||
} else if err != nil { |
||||
return err |
||||
} else { |
||||
disableGravatar = disableGravatarSetting.GetValueBool() |
||||
} |
||||
|
||||
var enableFederatedAvatar bool |
||||
enableFederatedAvatarSetting, err := GetSetting(KeyPictureEnableFederatedAvatar) |
||||
if IsErrSettingIsNotExist(err) { |
||||
enableFederatedAvatar = setting.GetDefaultEnableFederatedAvatar(disableGravatar) |
||||
enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)} |
||||
} else if err != nil { |
||||
return err |
||||
} else { |
||||
enableFederatedAvatar = disableGravatarSetting.GetValueBool() |
||||
} |
||||
|
||||
if setting.OfflineMode { |
||||
disableGravatar = true |
||||
enableFederatedAvatar = false |
||||
} |
||||
|
||||
if disableGravatar || !enableFederatedAvatar { |
||||
var err error |
||||
GravatarSourceURL, err = url.Parse(setting.GravatarSource) |
||||
if err != nil { |
||||
return fmt.Errorf("Failed to parse Gravatar URL(%s): %v", setting.GravatarSource, err) |
||||
} |
||||
} |
||||
|
||||
if enableFederatedAvatarSetting.GetValueBool() { |
||||
LibravatarService = libravatar.New() |
||||
if GravatarSourceURL.Scheme == "https" { |
||||
LibravatarService.SetUseHTTPS(true) |
||||
LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host) |
||||
} else { |
||||
LibravatarService.SetUseHTTPS(false) |
||||
LibravatarService.SetFallbackHost(GravatarSourceURL.Host) |
||||
} |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,11 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package system |
||||
|
||||
// enumerate all system setting keys
|
||||
const ( |
||||
KeyPictureDisableGravatar = "picture.disable_gravatar" |
||||
KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar" |
||||
) |
@ -0,0 +1,53 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package system_test |
||||
|
||||
import ( |
||||
"strings" |
||||
"testing" |
||||
|
||||
"code.gitea.io/gitea/models/system" |
||||
"code.gitea.io/gitea/models/unittest" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestSettings(t *testing.T) { |
||||
keyName := "server.LFS_LOCKS_PAGING_NUM" |
||||
assert.NoError(t, unittest.PrepareTestDatabase()) |
||||
|
||||
newSetting := &system.Setting{SettingKey: keyName, SettingValue: "50"} |
||||
|
||||
// create setting
|
||||
err := system.SetSetting(newSetting) |
||||
assert.NoError(t, err) |
||||
// test about saving unchanged values
|
||||
err = system.SetSetting(newSetting) |
||||
assert.NoError(t, err) |
||||
|
||||
// get specific setting
|
||||
settings, err := system.GetSettings([]string{keyName}) |
||||
assert.NoError(t, err) |
||||
assert.Len(t, settings, 1) |
||||
assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue) |
||||
|
||||
// updated setting
|
||||
updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: newSetting.Version} |
||||
err = system.SetSetting(updatedSetting) |
||||
assert.NoError(t, err) |
||||
|
||||
// get all settings
|
||||
settings, err = system.GetAllSettings() |
||||
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(&system.Setting{SettingKey: strings.ToLower(keyName)}) |
||||
assert.NoError(t, err) |
||||
settings, err = system.GetAllSettings() |
||||
assert.NoError(t, err) |
||||
assert.Len(t, settings, 2) |
||||
} |
@ -0,0 +1,46 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package system |
||||
|
||||
import ( |
||||
"strconv" |
||||
|
||||
"code.gitea.io/gitea/models/system" |
||||
"code.gitea.io/gitea/modules/cache" |
||||
) |
||||
|
||||
func genKey(key string) string { |
||||
return "system.setting." + key |
||||
} |
||||
|
||||
// GetSetting returns the setting value via the key
|
||||
func GetSetting(key string) (string, error) { |
||||
return cache.GetString(genKey(key), func() (string, error) { |
||||
res, err := system.GetSetting(key) |
||||
if err != 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(key string) bool { |
||||
s, _ := GetSetting(key) |
||||
b, _ := strconv.ParseBool(s) |
||||
return b |
||||
} |
||||
|
||||
// SetSetting sets the setting value
|
||||
func SetSetting(key, value string, version int) error { |
||||
cache.Remove(genKey(key)) |
||||
|
||||
return system.SetSetting(&system.Setting{ |
||||
SettingKey: key, |
||||
SettingValue: value, |
||||
Version: version, |
||||
}) |
||||
} |
@ -0,0 +1,34 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package system |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"code.gitea.io/gitea/models/user" |
||||
"code.gitea.io/gitea/modules/cache" |
||||
) |
||||
|
||||
func genUserKey(userID int64, key string) string { |
||||
return fmt.Sprintf("user_%d.setting.%s", userID, key) |
||||
} |
||||
|
||||
// GetUserSetting returns the user setting value via the key
|
||||
func GetUserSetting(userID int64, key string) (string, error) { |
||||
return cache.GetString(genUserKey(userID, key), func() (string, error) { |
||||
res, err := user.GetSetting(userID, key) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return res.SettingValue, nil |
||||
}) |
||||
} |
||||
|
||||
// SetUserSetting sets the user setting value
|
||||
func SetUserSetting(userID int64, key, value string) error { |
||||
cache.Remove(genUserKey(userID, key)) |
||||
|
||||
return user.SetUserSetting(userID, key, value) |
||||
} |
@ -0,0 +1,217 @@ |
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin |
||||
|
||||
import ( |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
"strings" |
||||
|
||||
system_model "code.gitea.io/gitea/models/system" |
||||
"code.gitea.io/gitea/modules/base" |
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/git" |
||||
"code.gitea.io/gitea/modules/json" |
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
system_module "code.gitea.io/gitea/modules/system" |
||||
"code.gitea.io/gitea/modules/util" |
||||
"code.gitea.io/gitea/services/mailer" |
||||
|
||||
"gitea.com/go-chi/session" |
||||
) |
||||
|
||||
const tplConfig base.TplName = "admin/config" |
||||
|
||||
// SendTestMail send test mail to confirm mail service is OK
|
||||
func SendTestMail(ctx *context.Context) { |
||||
email := ctx.FormString("email") |
||||
// Send a test email to the user's email address and redirect back to Config
|
||||
if err := mailer.SendTestMail(email); err != nil { |
||||
ctx.Flash.Error(ctx.Tr("admin.config.test_mail_failed", email, err)) |
||||
} else { |
||||
ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email)) |
||||
} |
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/config") |
||||
} |
||||
|
||||
func shadowPasswordKV(cfgItem, splitter string) string { |
||||
fields := strings.Split(cfgItem, splitter) |
||||
for i := 0; i < len(fields); i++ { |
||||
if strings.HasPrefix(fields[i], "password=") { |
||||
fields[i] = "password=******" |
||||
break |
||||
} |
||||
} |
||||
return strings.Join(fields, splitter) |
||||
} |
||||
|
||||
func shadowURL(provider, cfgItem string) string { |
||||
u, err := url.Parse(cfgItem) |
||||
if err != nil { |
||||
log.Error("Shadowing Password for %v failed: %v", provider, err) |
||||
return cfgItem |
||||
} |
||||
if u.User != nil { |
||||
atIdx := strings.Index(cfgItem, "@") |
||||
if atIdx > 0 { |
||||
colonIdx := strings.LastIndex(cfgItem[:atIdx], ":") |
||||
if colonIdx > 0 { |
||||
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] |
||||
} |
||||
} |
||||
} |
||||
return cfgItem |
||||
} |
||||
|
||||
func shadowPassword(provider, cfgItem string) string { |
||||
switch provider { |
||||
case "redis": |
||||
return shadowPasswordKV(cfgItem, ",") |
||||
case "mysql": |
||||
// root:@tcp(localhost:3306)/macaron?charset=utf8
|
||||
atIdx := strings.Index(cfgItem, "@") |
||||
if atIdx > 0 { |
||||
colonIdx := strings.Index(cfgItem[:atIdx], ":") |
||||
if colonIdx > 0 { |
||||
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] |
||||
} |
||||
} |
||||
return cfgItem |
||||
case "postgres": |
||||
// user=jiahuachen dbname=macaron port=5432 sslmode=disable
|
||||
if !strings.HasPrefix(cfgItem, "postgres://") { |
||||
return shadowPasswordKV(cfgItem, " ") |
||||
} |
||||
fallthrough |
||||
case "couchbase": |
||||
return shadowURL(provider, cfgItem) |
||||
// postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full
|
||||
// Notice: use shadowURL
|
||||
} |
||||
return cfgItem |
||||
} |
||||
|
||||
// Config show admin config page
|
||||
func Config(ctx *context.Context) { |
||||
ctx.Data["Title"] = ctx.Tr("admin.config") |
||||
ctx.Data["PageIsAdmin"] = true |
||||
ctx.Data["PageIsAdminConfig"] = true |
||||
|
||||
systemSettings, err := system_model.GetAllSettings() |
||||
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["AppUrl"] = setting.AppURL |
||||
ctx.Data["Domain"] = setting.Domain |
||||
ctx.Data["OfflineMode"] = setting.OfflineMode |
||||
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog |
||||
ctx.Data["RunUser"] = setting.RunUser |
||||
ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) |
||||
ctx.Data["GitVersion"] = git.VersionInfo() |
||||
|
||||
ctx.Data["RepoRootPath"] = setting.RepoRootPath |
||||
ctx.Data["CustomRootPath"] = setting.CustomPath |
||||
ctx.Data["StaticRootPath"] = setting.StaticRootPath |
||||
ctx.Data["LogRootPath"] = setting.LogRootPath |
||||
ctx.Data["ScriptType"] = setting.ScriptType |
||||
ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser |
||||
ctx.Data["ReverseProxyAuthEmail"] = setting.ReverseProxyAuthEmail |
||||
|
||||
ctx.Data["SSH"] = setting.SSH |
||||
ctx.Data["LFS"] = setting.LFS |
||||
|
||||
ctx.Data["Service"] = setting.Service |
||||
ctx.Data["DbCfg"] = setting.Database |
||||
ctx.Data["Webhook"] = setting.Webhook |
||||
|
||||
ctx.Data["MailerEnabled"] = false |
||||
if setting.MailService != nil { |
||||
ctx.Data["MailerEnabled"] = true |
||||
ctx.Data["Mailer"] = setting.MailService |
||||
} |
||||
|
||||
ctx.Data["CacheAdapter"] = setting.CacheService.Adapter |
||||
ctx.Data["CacheInterval"] = setting.CacheService.Interval |
||||
|
||||
ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn) |
||||
ctx.Data["CacheItemTTL"] = setting.CacheService.TTL |
||||
|
||||
sessionCfg := setting.SessionConfig |
||||
if sessionCfg.Provider == "VirtualSession" { |
||||
var realSession session.Options |
||||
if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil { |
||||
log.Error("Unable to unmarshall session config for virtual provider config: %s\nError: %v", sessionCfg.ProviderConfig, err) |
||||
} |
||||
sessionCfg.Provider = realSession.Provider |
||||
sessionCfg.ProviderConfig = realSession.ProviderConfig |
||||
sessionCfg.CookieName = realSession.CookieName |
||||
sessionCfg.CookiePath = realSession.CookiePath |
||||
sessionCfg.Gclifetime = realSession.Gclifetime |
||||
sessionCfg.Maxlifetime = realSession.Maxlifetime |
||||
sessionCfg.Secure = realSession.Secure |
||||
sessionCfg.Domain = realSession.Domain |
||||
} |
||||
sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig) |
||||
ctx.Data["SessionConfig"] = sessionCfg |
||||
|
||||
ctx.Data["Git"] = setting.Git |
||||
|
||||
type envVar struct { |
||||
Name, Value string |
||||
} |
||||
|
||||
envVars := map[string]*envVar{} |
||||
if len(os.Getenv("GITEA_WORK_DIR")) > 0 { |
||||
envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")} |
||||
} |
||||
if len(os.Getenv("GITEA_CUSTOM")) > 0 { |
||||
envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")} |
||||
} |
||||
|
||||
ctx.Data["EnvVars"] = envVars |
||||
ctx.Data["Loggers"] = setting.GetLogDescriptions() |
||||
ctx.Data["EnableAccessLog"] = setting.EnableAccessLog |
||||
ctx.Data["AccessLogTemplate"] = setting.AccessLogTemplate |
||||
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog |
||||
ctx.Data["EnableXORMLog"] = setting.EnableXORMLog |
||||
ctx.Data["LogSQL"] = setting.Database.LogSQL |
||||
|
||||
ctx.HTML(http.StatusOK, tplConfig) |
||||
} |
||||
|
||||
func ChangeConfig(ctx *context.Context) { |
||||
key := strings.TrimSpace(ctx.FormString("key")) |
||||
if key == "" { |
||||
ctx.JSON(http.StatusOK, map[string]string{ |
||||
"redirect": ctx.Req.URL.String(), |
||||
}) |
||||
return |
||||
} |
||||
value := ctx.FormString("value") |
||||
version := ctx.FormInt("version") |
||||
|
||||
if err := system_module.SetSetting(key, value, version); err != nil { |
||||
log.Error("set setting failed: %v", err) |
||||
ctx.JSON(http.StatusOK, map[string]string{ |
||||
"err": ctx.Tr("admin.config.set_setting_failed", key), |
||||
}) |
||||
return |
||||
} |
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{ |
||||
"version": version + 1, |
||||
}) |
||||
} |
@ -0,0 +1,37 @@ |
||||
import $ from 'jquery'; |
||||
import {showTemporaryTooltip} from '../../modules/tippy.js'; |
||||
|
||||
const {appSubUrl, csrfToken, pageData} = window.config; |
||||
|
||||
export function initAdminConfigs() { |
||||
const isAdminConfigPage = pageData?.adminConfigPage; |
||||
if (!isAdminConfigPage) return; |
||||
|
||||
$("input[type='checkbox']").on('change', (e) => { |
||||
const $this = $(e.currentTarget); |
||||
$.ajax({ |
||||
url: `${appSubUrl}/admin/config`, |
||||
type: 'POST', |
||||
data: { |
||||
_csrf: csrfToken, |
||||
key: $this.attr('name'), |
||||
value: $this.is(':checked'), |
||||
version: $this.attr('version'), |
||||
} |
||||
}).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; |
||||
}); |
||||
} |
Loading…
Reference in new issue