diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index 85476afd0..09510ac2c 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -14,7 +14,6 @@ import ( "fmt" "log" "net/http" - "net/url" "os" "os/exec" "os/user" @@ -62,11 +61,7 @@ func runPR() { } setting.AppWorkPath = curDir setting.StaticRootPath = curDir - setting.GravatarSourceURL, err = url.Parse("https://secure.gravatar.com/avatar/") - if err != nil { - log.Fatalf("url.Parse: %v\n", err) - } - + setting.GravatarSource = "https://secure.gravatar.com/avatar/" setting.AppURL = "http://localhost:8080/" setting.HTTPPort = "8080" setting.SSH.Domain = "localhost" diff --git a/models/admin/notice_test.go b/models/admin/notice_test.go deleted file mode 100644 index f3767392d..000000000 --- a/models/admin/notice_test.go +++ /dev/null @@ -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}) -} diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go index 9f7b0c474..418e9b9cc 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -13,6 +13,7 @@ import ( "sync" "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/cache" "code.gitea.io/gitea/modules/log" @@ -72,7 +73,7 @@ func GetEmailForHash(md5Sum string) (string, error) { // 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. func LibravatarURL(email string) (*url.URL, error) { - urlStr, err := setting.LibravatarService.FromEmail(email) + urlStr, err := system_model.LibravatarService.FromEmail(email) if err != nil { log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err) return nil, err @@ -149,8 +150,10 @@ func generateEmailAvatarLink(email string, size int, final bool) string { return DefaultAvatarLink() } + enableFederatedAvatar, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar) + var err error - if setting.EnableFederatedAvatar && setting.LibravatarService != nil { + if enableFederatedAvatar != nil && enableFederatedAvatar.GetValueBool() && system_model.LibravatarService != nil { emailHash := saveEmailHash(email) if final { // for final link, we can spend more time on slow external query @@ -166,12 +169,16 @@ func generateEmailAvatarLink(email string, size int, final bool) string { urlStr += "?size=" + strconv.Itoa(size) } return urlStr - } else if !setting.DisableGravatar { + } + + disableGravatar, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar) + if disableGravatar != nil && !disableGravatar.GetValueBool() { // copy GravatarSourceURL, because we will modify its Path. - avatarURLCopy := *setting.GravatarSourceURL + avatarURLCopy := *system_model.GravatarSourceURL avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email)) return generateRecognizedAvatarURL(avatarURLCopy, size) } + return DefaultAvatarLink() } diff --git a/models/avatars/avatar_test.go b/models/avatars/avatar_test.go index 4d6255ca5..ace5445fc 100644 --- a/models/avatars/avatar_test.go +++ b/models/avatars/avatar_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package avatars +package avatars_test import ( - "net/url" "testing" + avatars_model "code.gitea.io/gitea/models/avatars" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -15,40 +16,43 @@ import ( const gravatarSource = "https://secure.gravatar.com/avatar/" -func disableGravatar() { - setting.EnableFederatedAvatar = false - setting.LibravatarService = nil - setting.DisableGravatar = true +func disableGravatar(t *testing.T) { + err := system_model.SetSettingNoVersion(system_model.KeyPictureEnableFederatedAvatar, "false") + assert.NoError(t, err) + err = system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "true") + assert.NoError(t, err) + system_model.LibravatarService = nil } func enableGravatar(t *testing.T) { - setting.DisableGravatar = false - var err error - setting.GravatarSourceURL, err = url.Parse(gravatarSource) + err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "false") + assert.NoError(t, err) + setting.GravatarSource = gravatarSource + err = system_model.Init() assert.NoError(t, err) } func TestHashEmail(t *testing.T) { assert.Equal(t, "d41d8cd98f00b204e9800998ecf8427e", - HashEmail(""), + avatars_model.HashEmail(""), ) assert.Equal(t, "353cbad9b58e69c96154ad99f92bedc7", - HashEmail("gitea@example.com"), + avatars_model.HashEmail("gitea@example.com"), ) } func TestSizedAvatarLink(t *testing.T) { setting.AppSubURL = "/testsuburl" - disableGravatar() + disableGravatar(t) assert.Equal(t, "/testsuburl/assets/img/avatar_default.png", - GenerateEmailAvatarFastLink("gitea@example.com", 100)) + avatars_model.GenerateEmailAvatarFastLink("gitea@example.com", 100)) enableGravatar(t) assert.Equal(t, "https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100", - GenerateEmailAvatarFastLink("gitea@example.com", 100), + avatars_model.GenerateEmailAvatarFastLink("gitea@example.com", 100), ) } diff --git a/models/admin/main_test.go b/models/avatars/main_test.go similarity index 95% rename from models/admin/main_test.go rename to models/avatars/main_test.go index 23277fe37..0e98d8f64 100644 --- a/models/admin/main_test.go +++ b/models/avatars/main_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package admin_test +package avatars_test import ( "path/filepath" diff --git a/models/fixtures/system_setting.yml b/models/fixtures/system_setting.yml new file mode 100644 index 000000000..6c960168f --- /dev/null +++ b/models/fixtures/system_setting.yml @@ -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 diff --git a/models/issues/issue.go b/models/issues/issue.go index 737b625ab..786c96952 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -13,7 +13,6 @@ import ( "strconv" "strings" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/foreignreference" "code.gitea.io/gitea/models/organization" @@ -21,6 +20,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" @@ -2470,7 +2470,7 @@ func DeleteOrphanedIssues() error { // Remove issue attachment files. for i := range attachmentPaths { - admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i]) + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i]) } return nil } diff --git a/models/main_test.go b/models/main_test.go index 49b6e3e56..358400156 100644 --- a/models/main_test.go +++ b/models/main_test.go @@ -14,6 +14,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + _ "code.gitea.io/gitea/models/system" + "github.com/stretchr/testify/assert" ) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 2a3877218..afe1445a2 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -415,6 +415,8 @@ var migrations = []Migration{ NewMigration("Alter gpg_key/public_key content TEXT fields to MEDIUMTEXT", alterPublicGPGKeyContentFieldsToMediumText), // v226 -> v227 NewMigration("Conan and generic packages do not need to be semantically versioned", fixPackageSemverField), + // v227 -> v228 + NewMigration("Create key/value table for system settings", createSystemSettingsTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v227.go b/models/migrations/v227.go new file mode 100644 index 000000000..8a3a9c877 --- /dev/null +++ b/models/migrations/v227.go @@ -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) +} diff --git a/models/repo.go b/models/repo.go index 94e624984..65159f14a 100644 --- a/models/repo.go +++ b/models/repo.go @@ -22,6 +22,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" @@ -32,9 +33,13 @@ import ( "xorm.io/builder" ) -// NewRepoContext creates a new repository context -func NewRepoContext() { +// ItemsPerPage maximum items per page in forks, watchers and stars of a repo +var ItemsPerPage = 40 + +// Init initialize model +func Init() error { unit.LoadUnitConfig() + return system_model.Init() } // DeleteRepository deletes a repository for a user or organization. @@ -267,36 +272,36 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { // Remove repository files. repoPath := repo.RepoPath() - admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath) + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath) // Remove wiki files if repo.HasWiki() { - admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath()) + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath()) } // Remove archives for _, archive := range archivePaths { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive) } // Remove lfs objects for _, lfsObj := range lfsPaths { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj) } // Remove issue attachment files. for _, attachment := range attachmentPaths { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment) } // Remove release attachment files. for _, releaseAttachment := range releaseAttachments { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment) } // Remove attachment with no issue_id and release_id. for _, newAttachment := range newAttachmentPaths { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment) } if len(repo.Avatar) > 0 { diff --git a/models/appstate/appstate.go b/models/system/appstate.go similarity index 98% rename from models/appstate/appstate.go rename to models/system/appstate.go index aa5a59e1a..c11a2512a 100644 --- a/models/appstate/appstate.go +++ b/models/system/appstate.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system import ( "context" diff --git a/models/system/main_test.go b/models/system/main_test.go new file mode 100644 index 000000000..a56c76aed --- /dev/null +++ b/models/system/main_test.go @@ -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("..", ".."), + }) +} diff --git a/models/admin/notice.go b/models/system/notice.go similarity index 99% rename from models/admin/notice.go rename to models/system/notice.go index 4d385cf95..3276fa3ff 100644 --- a/models/admin/notice.go +++ b/models/system/notice.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package admin +package system import ( "context" diff --git a/models/system/notice_test.go b/models/system/notice_test.go new file mode 100644 index 000000000..768bcca66 --- /dev/null +++ b/models/system/notice_test.go @@ -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}) +} diff --git a/models/system/setting.go b/models/system/setting.go new file mode 100644 index 000000000..ff8b48e61 --- /dev/null +++ b/models/system/setting.go @@ -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 +} diff --git a/models/system/setting_key.go b/models/system/setting_key.go new file mode 100644 index 000000000..5a6ea6ed7 --- /dev/null +++ b/models/system/setting_key.go @@ -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" +) diff --git a/models/system/setting_test.go b/models/system/setting_test.go new file mode 100644 index 000000000..d25fc05f3 --- /dev/null +++ b/models/system/setting_test.go @@ -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) +} diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 25129137f..2e6c25ae4 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -7,12 +7,12 @@ package unittest import ( "context" "fmt" - "net/url" "os" "path/filepath" "testing" "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/git" "code.gitea.io/gitea/modules/setting" @@ -91,10 +91,8 @@ func MainTest(m *testing.M, testOpts *TestOptions) { setting.AppDataPath = appDataPath setting.AppWorkPath = testOpts.GiteaRootPath setting.StaticRootPath = testOpts.GiteaRootPath - setting.GravatarSourceURL, err = url.Parse("https://secure.gravatar.com/avatar/") - if err != nil { - fatalTestError("url.Parse: %v\n", err) - } + setting.GravatarSource = "https://secure.gravatar.com/avatar/" + setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments") setting.LFS.Storage.Path = filepath.Join(setting.AppDataPath, "lfs") @@ -112,6 +110,9 @@ func MainTest(m *testing.M, testOpts *TestOptions) { if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } + if err = system_model.Init(); err != nil { + fatalTestError("models.Init: %v\n", err) + } if err = util.RemoveAll(repoRootPath); err != nil { fatalTestError("util.RemoveAll: %v\n", err) diff --git a/models/user/avatar.go b/models/user/avatar.go index 6a44a3bcb..1c75c7406 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/avatars" "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/log" "code.gitea.io/gitea/modules/setting" @@ -67,10 +68,16 @@ func (u *User) AvatarLinkWithSize(size int) string { useLocalAvatar := false autoGenerateAvatar := false + var disableGravatar bool + disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar) + if disableGravatarSetting != nil { + disableGravatar = disableGravatarSetting.GetValueBool() + } + switch { case u.UseCustomAvatar: useLocalAvatar = true - case setting.DisableGravatar, setting.OfflineMode: + case disableGravatar, setting.OfflineMode: useLocalAvatar = true autoGenerateAvatar = true } diff --git a/models/user/setting.go b/models/user/setting.go index fbb6fbab3..5fe7c2ec2 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -31,6 +31,34 @@ func init() { db.RegisterModel(new(Setting)) } +// ErrUserSettingIsNotExist represents an error that a setting is not exist with special key +type ErrUserSettingIsNotExist struct { + Key string +} + +// Error implements error +func (err ErrUserSettingIsNotExist) Error() string { + return fmt.Sprintf("Setting[%s] is not exist", err.Key) +} + +// IsErrUserSettingIsNotExist return true if err is ErrSettingIsNotExist +func IsErrUserSettingIsNotExist(err error) bool { + _, ok := err.(ErrUserSettingIsNotExist) + return ok +} + +// GetSetting returns specific setting +func GetSetting(uid int64, key string) (*Setting, error) { + v, err := GetUserSettings(uid, []string{key}) + if err != nil { + return nil, err + } + if len(v) == 0 { + return nil, ErrUserSettingIsNotExist{key} + } + return v[key], nil +} + // GetUserSettings returns specific settings from user func GetUserSettings(uid int64, keys []string) (map[string]*Setting, error) { settings := make([]*Setting, 0, len(keys)) diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index c62e324b6..7bd741d0c 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -11,8 +11,10 @@ import ( "time" 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/modules/git" + "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" ) @@ -100,6 +102,14 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) { assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified) } +func enableGravatar(t *testing.T) { + err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "false") + assert.NoError(t, err) + setting.GravatarSource = "https://secure.gravatar.com/avatar" + err = system_model.Init() + assert.NoError(t, err) +} + func TestPushCommits_AvatarLink(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -123,6 +133,8 @@ func TestPushCommits_AvatarLink(t *testing.T) { }, } + enableGravatar(t) + assert.Equal(t, "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s=84", pushCommits.AvatarLink("user2@example.com")) diff --git a/modules/setting/picture.go b/modules/setting/picture.go index a6d3447dc..af9041ade 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -4,14 +4,6 @@ package setting -import ( - "net/url" - - "code.gitea.io/gitea/modules/log" - - "strk.kbt.io/projects/go/libravatar" -) - // settings var ( // Picture settings @@ -30,10 +22,8 @@ var ( } GravatarSource string - GravatarSourceURL *url.URL - DisableGravatar bool - EnableFederatedAvatar bool - LibravatarService *libravatar.Libravatar + DisableGravatar bool // Depreciated: migrated to database + EnableFederatedAvatar bool // Depreciated: migrated to database RepoAvatar = struct { Storage @@ -69,38 +59,30 @@ func newPictureService() { default: GravatarSource = source } - DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool() - EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(!InstallLock) - if OfflineMode { - DisableGravatar = true - EnableFederatedAvatar = false - } - if DisableGravatar { - EnableFederatedAvatar = false - } - if EnableFederatedAvatar || !DisableGravatar { - var err error - GravatarSourceURL, err = url.Parse(GravatarSource) - if err != nil { - log.Fatal("Failed to parse Gravatar URL(%s): %v", - GravatarSource, err) - } - } - if EnableFederatedAvatar { - LibravatarService = libravatar.New() - if GravatarSourceURL.Scheme == "https" { - LibravatarService.SetUseHTTPS(true) - LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host) - } else { - LibravatarService.SetUseHTTPS(false) - LibravatarService.SetFallbackHost(GravatarSourceURL.Host) - } - } + DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool(GetDefaultDisableGravatar()) + deprecatedSettingDB("", "DISABLE_GRAVATAR") + EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(GetDefaultEnableFederatedAvatar(DisableGravatar)) + deprecatedSettingDB("", "ENABLE_FEDERATED_AVATAR") newRepoAvatarService() } +func GetDefaultDisableGravatar() bool { + return !OfflineMode +} + +func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool { + v := !InstallLock + if OfflineMode { + v = false + } + if disableGravatar { + v = false + } + return v +} + func newRepoAvatarService() { sec := Cfg.Section("picture") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 007e3ef61..f93be2fbd 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -606,6 +606,13 @@ func deprecatedSetting(oldSection, oldKey, newSection, newKey string) { } } +// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini +func deprecatedSettingDB(oldSection, oldKey string) { + if Cfg.Section(oldSection).HasKey(oldKey) { + log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey) + } +} + // loadFromConf initializes configuration context. // NOTE: do not print any log except error. func loadFromConf(allowEmpty bool, extraConfig string) { diff --git a/modules/appstate/appstate.go b/modules/system/appstate.go similarity index 97% rename from modules/appstate/appstate.go rename to modules/system/appstate.go index f65f5367e..deee8cd02 100644 --- a/modules/appstate/appstate.go +++ b/modules/system/appstate.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system // StateStore is the interface to get/set app state items type StateStore interface { diff --git a/modules/appstate/appstate_test.go b/modules/system/appstate_test.go similarity index 98% rename from modules/appstate/appstate_test.go rename to modules/system/appstate_test.go index e4a0d7285..fb0c2aaf9 100644 --- a/modules/appstate/appstate_test.go +++ b/modules/system/appstate_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system import ( "path/filepath" diff --git a/modules/appstate/db.go b/modules/system/db.go similarity index 77% rename from modules/appstate/db.go rename to modules/system/db.go index 2538d1b5c..b1c283c48 100644 --- a/modules/appstate/db.go +++ b/modules/system/db.go @@ -2,10 +2,10 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system import ( - "code.gitea.io/gitea/models/appstate" + "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/json" "github.com/yuin/goldmark/util" @@ -16,7 +16,7 @@ type DBStore struct{} // Get reads the state item func (f *DBStore) Get(item StateItem) error { - content, err := appstate.GetAppStateContent(item.Name()) + content, err := system.GetAppStateContent(item.Name()) if err != nil { return err } @@ -32,5 +32,5 @@ func (f *DBStore) Set(item StateItem) error { if err != nil { return err } - return appstate.SaveAppStateContent(item.Name(), util.BytesToReadOnlyString(b)) + return system.SaveAppStateContent(item.Name(), util.BytesToReadOnlyString(b)) } diff --git a/modules/appstate/item_runtime.go b/modules/system/item_runtime.go similarity index 96% rename from modules/appstate/item_runtime.go rename to modules/system/item_runtime.go index 7fdc53f64..ef758a567 100644 --- a/modules/appstate/item_runtime.go +++ b/modules/system/item_runtime.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system // RuntimeState contains app state for runtime, and we can save remote version for update checker here in future type RuntimeState struct { diff --git a/modules/system/setting.go b/modules/system/setting.go new file mode 100644 index 000000000..aebf24a50 --- /dev/null +++ b/modules/system/setting.go @@ -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, + }) +} diff --git a/modules/system/user_setting.go b/modules/system/user_setting.go new file mode 100644 index 000000000..eaf146c08 --- /dev/null +++ b/modules/system/user_setting.go @@ -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) +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index e10beae1d..7bd2bc0a1 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -29,6 +29,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/emoji" @@ -41,6 +42,7 @@ import ( "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" + system_module "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/gitdiff" @@ -85,7 +87,7 @@ func NewFuncMap() []template.FuncMap { return setting.AssetVersion }, "DisableGravatar": func() bool { - return setting.DisableGravatar + return system_module.GetSettingBool(system_model.KeyPictureDisableGravatar) }, "DefaultShowFullName": func() bool { return setting.UI.DefaultShowFullName diff --git a/modules/updatechecker/update_checker.go b/modules/updatechecker/update_checker.go index 9c1569b15..816fb3764 100644 --- a/modules/updatechecker/update_checker.go +++ b/modules/updatechecker/update_checker.go @@ -8,10 +8,10 @@ import ( "io" "net/http" - "code.gitea.io/gitea/modules/appstate" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/system" "github.com/hashicorp/go-version" ) @@ -64,13 +64,13 @@ func GiteaUpdateChecker(httpEndpoint string) error { // UpdateRemoteVersion updates the latest available version of Gitea func UpdateRemoteVersion(version string) (err error) { - return appstate.AppState.Set(&CheckerState{LatestVersion: version}) + return system.AppState.Set(&CheckerState{LatestVersion: version}) } // GetRemoteVersion returns the current remote version (or currently installed version if fail to fetch from DB) func GetRemoteVersion() string { item := new(CheckerState) - if err := appstate.AppState.Get(item); err != nil { + if err := system.AppState.Get(item); err != nil { return "" } return item.LatestVersion diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index fbf9b7064..e5da074f6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2879,6 +2879,9 @@ config.access_log_template = Template config.xorm_log_mode = XORM Log Mode config.xorm_log_sql = Log SQL +config.get_setting_failed = Get setting %s failed +config.set_setting_failed = Set setting %s failed + monitor.cron = Cron Tasks monitor.name = Name monitor.schedule = Schedule diff --git a/routers/init.go b/routers/init.go index 85a38899e..0f2e99341 100644 --- a/routers/init.go +++ b/routers/init.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" - "code.gitea.io/gitea/modules/appstate" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/git" @@ -27,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/ssh" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -76,8 +76,8 @@ func InitGitServices() { } func syncAppPathForGit(ctx context.Context) error { - runtimeState := new(appstate.RuntimeState) - if err := appstate.AppState.Get(runtimeState); err != nil { + runtimeState := new(system.RuntimeState) + if err := system.AppState.Get(runtimeState); err != nil { return err } if runtimeState.LastAppPath != setting.AppPath { @@ -90,7 +90,7 @@ func syncAppPathForGit(ctx context.Context) error { mustInit(asymkey_model.RewriteAllPublicKeys) runtimeState.LastAppPath = setting.AppPath - return appstate.AppState.Set(runtimeState) + return system.AppState.Set(runtimeState) } return nil } @@ -133,10 +133,10 @@ func GlobalInitInstalled(ctx context.Context) { mustInitCtx(ctx, common.InitDBEngine) log.Info("ORM engine initialization successful!") - mustInit(appstate.Init) + mustInit(system.Init) mustInit(oauth2.Init) - models.NewRepoContext() + mustInit(models.Init) mustInit(repo_service.Init) // Booting long running goroutines. diff --git a/routers/install/install.go b/routers/install/install.go index 890725b9a..8a0d34d97 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -12,12 +12,14 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "time" "code.gitea.io/gitea/models/db" db_install "code.gitea.io/gitea/models/db/install" "code.gitea.io/gitea/models/migrations" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -147,8 +149,19 @@ func Install(ctx *context.Context) { // Server and other services settings form.OfflineMode = setting.OfflineMode - form.DisableGravatar = setting.DisableGravatar - form.EnableFederatedAvatar = setting.EnableFederatedAvatar + disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar) + if disableGravatarSetting != nil { + form.DisableGravatar = disableGravatarSetting.GetValueBool() + } else { + form.DisableGravatar = false + } + + enableFederatedAvatarSetting, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar) + if enableFederatedAvatarSetting != nil { + form.EnableFederatedAvatar = enableFederatedAvatarSetting.GetValueBool() + } else { + form.EnableFederatedAvatar = false + } form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp form.DisableRegistration = setting.Service.DisableRegistration @@ -439,7 +452,11 @@ func SubmitInstall(ctx *context.Context) { 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("picture").Key("DISABLE_GRAVATAR").SetValue(fmt.Sprint(form.DisableGravatar)) + // if you are reinstalling, this maybe not right because of missing version + if err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, strconv.FormatBool(form.DisableGravatar)); err != nil { + ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) + return + } cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(fmt.Sprint(form.EnableFederatedAvatar)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp)) diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 17471ef8a..d0664eb78 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -8,18 +8,13 @@ package admin import ( "fmt" "net/http" - "net/url" - "os" "runtime" "strconv" - "strings" "time" activities_model "code.gitea.io/gitea/models/activities" "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/process" "code.gitea.io/gitea/modules/queue" @@ -27,18 +22,13 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/updatechecker" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/mailer" - - "gitea.com/go-chi/session" ) const ( tplDashboard base.TplName = "admin/dashboard" - tplConfig base.TplName = "admin/config" tplMonitor base.TplName = "admin/monitor" tplStacktrace base.TplName = "admin/stacktrace" tplQueue base.TplName = "admin/queue" @@ -165,165 +155,6 @@ func DashboardPost(ctx *context.Context) { } } -// 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 - - 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["ReverseProxyAuthFullName"] = setting.ReverseProxyAuthFullName - - 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 virtualed 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["DisableGravatar"] = setting.DisableGravatar - ctx.Data["EnableFederatedAvatar"] = setting.EnableFederatedAvatar - - 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) -} - // Monitor show admin monitor page func Monitor(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.monitor") diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go new file mode 100644 index 000000000..614d3d4f6 --- /dev/null +++ b/routers/web/admin/config.go @@ -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, + }) +} diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go index b50549b80..f5ec294cc 100644 --- a/routers/web/admin/notice.go +++ b/routers/web/admin/notice.go @@ -9,7 +9,7 @@ import ( "net/http" "strconv" - admin_model "code.gitea.io/gitea/models/admin" + 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/log" @@ -26,13 +26,13 @@ func Notices(ctx *context.Context) { ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdminNotices"] = true - total := admin_model.CountNotices() + total := system_model.CountNotices() page := ctx.FormInt("page") if page <= 1 { page = 1 } - notices, err := admin_model.Notices(page, setting.UI.Admin.NoticePagingNum) + notices, err := system_model.Notices(page, setting.UI.Admin.NoticePagingNum) if err != nil { ctx.ServerError("Notices", err) return @@ -57,7 +57,7 @@ func DeleteNotices(ctx *context.Context) { } } - if err := admin_model.DeleteNoticesByIDs(ids); err != nil { + if err := system_model.DeleteNoticesByIDs(ids); err != nil { ctx.Flash.Error("DeleteNoticesByIDs: " + err.Error()) ctx.Status(http.StatusInternalServerError) } else { @@ -68,7 +68,7 @@ func DeleteNotices(ctx *context.Context) { // EmptyNotices delete all the notices func EmptyNotices(ctx *context.Context) { - if err := admin_model.DeleteNotices(0, 0); err != nil { + if err := system_model.DeleteNotices(0, 0); err != nil { ctx.ServerError("DeleteNotices", err) return } diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index ae4177cf1..c9e8eb4a8 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -7,7 +7,7 @@ package repo import ( "fmt" - admin_model "code.gitea.io/gitea/models/admin" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -24,7 +24,7 @@ func SetEditorconfigIfExists(ctx *context.Context) { if err != nil && !git.IsErrNotExist(err) { description := fmt.Sprintf("Error while getting .editorconfig file: %v", err) - if err := admin_model.CreateRepositoryNotice(description); err != nil { + if err := system_model.CreateRepositoryNotice(description); err != nil { ctx.ServerError("ErrCreatingReporitoryNotice", err) } return diff --git a/routers/web/web.go b/routers/web/web.go index c01a2bce4..8859ec585 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -473,8 +473,13 @@ func RegisterRoutes(m *web.Route) { m.Group("/admin", func() { m.Get("", adminReq, admin.Dashboard) m.Post("", adminReq, bindIgnErr(forms.AdminDashboardForm{}), admin.DashboardPost) - m.Get("/config", admin.Config) - m.Post("/config/test_mail", admin.SendTestMail) + + m.Group("/config", func() { + m.Get("", admin.Config) + m.Post("", admin.ChangeConfig) + m.Post("/test_mail", admin.SendTestMail) + }) + m.Group("/monitor", func() { m.Get("", admin.Monitor) m.Get("/stacktrace", admin.GoroutineStacktrace) diff --git a/services/cron/tasks.go b/services/cron/tasks.go index c26e47e0c..6ff5964d1 100644 --- a/services/cron/tasks.go +++ b/services/cron/tasks.go @@ -10,8 +10,8 @@ import ( "reflect" "sync" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" @@ -114,7 +114,7 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) { t.LastDoer = doerName t.lock.Unlock() - if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "cancelled", doerName, message)); err != nil { + if err := system_model.CreateNotice(ctx, system_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "cancelled", doerName, message)); err != nil { log.Error("CreateNotice: %v", err) } return @@ -127,7 +127,7 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) { t.lock.Unlock() if config.DoNoticeOnSuccess() { - if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "finished", doerName)); err != nil { + if err := system_model.CreateNotice(ctx, system_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "finished", doerName)); err != nil { log.Error("CreateNotice: %v", err) } } diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index c3455ec32..04b9560be 100644 --- a/services/cron/tasks_extended.go +++ b/services/cron/tasks_extended.go @@ -9,9 +9,9 @@ import ( "time" activities_model "code.gitea.io/gitea/models/activities" - "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/updatechecker" @@ -166,7 +166,7 @@ func registerDeleteOldSystemNotices() { OlderThan: 365 * 24 * time.Hour, }, func(ctx context.Context, _ *user_model.User, config Config) error { olderThanConfig := config.(*OlderThanConfig) - return admin.DeleteOldSystemNotices(olderThanConfig.OlderThan) + return system.DeleteOldSystemNotices(olderThanConfig.OlderThan) }) } diff --git a/services/issue/issue.go b/services/issue/issue.go index bbd027879..69b87686c 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -8,12 +8,12 @@ import ( "fmt" activities_model "code.gitea.io/gitea/models/activities" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/notification" @@ -234,7 +234,7 @@ func deleteIssue(issue *issues_model.Issue) error { } for i := range issue.Attachments { - admin_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath()) + system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath()) } // delete all database data still assigned to this issue diff --git a/services/migrations/common.go b/services/migrations/common.go index 305ae89b2..052975c9e 100644 --- a/services/migrations/common.go +++ b/services/migrations/common.go @@ -8,7 +8,7 @@ import ( "fmt" "strings" - admin_model "code.gitea.io/gitea/models/admin" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" @@ -17,7 +17,7 @@ import ( // WarnAndNotice will log the provided message and send a repository notice func WarnAndNotice(fmtStr string, args ...interface{}) { log.Warn(fmtStr, args...) - if err := admin_model.CreateRepositoryNotice(fmt.Sprintf(fmtStr, args...)); err != nil { + if err := system_model.CreateRepositoryNotice(fmt.Sprintf(fmtStr, args...)); err != nil { log.Error("create repository notice failed: ", err) } } diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index 040f0aebb..dfb21b884 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -14,8 +14,8 @@ import ( "strings" "code.gitea.io/gitea/models" - admin_model "code.gitea.io/gitea/models/admin" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/hostmatcher" "code.gitea.io/gitea/modules/log" @@ -132,7 +132,7 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str if err1 := uploader.Rollback(); err1 != nil { log.Error("rollback failed: %v", err1) } - if err2 := admin_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil { + if err2 := system_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil { log.Error("create respotiry notice failed: ", err2) } return nil, err diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index f4c527bbd..a72e7f72e 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -10,9 +10,9 @@ import ( "strings" "time" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/lfs" @@ -188,7 +188,7 @@ func pruneBrokenReferences(ctx context.Context, log.Error("Failed to prune mirror repository %s%-v references:\nStdout: %s\nStderr: %s\nErr: %v", wiki, m.Repo, stdoutMessage, stderrMessage, pruneErr) desc := fmt.Sprintf("Failed to prune mirror repository %s'%s' references: %s", wiki, repoPath, stderrMessage) - if err := admin_model.CreateRepositoryNotice(desc); err != nil { + if err := system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } // this if will only be reached on a successful prune so try to get the mirror again @@ -267,7 +267,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo if err != nil { log.Error("SyncMirrors [repo: %-v]: failed to update mirror repository:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return nil, false @@ -356,7 +356,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo if err != nil { log.Error("SyncMirrors [repo: %-v Wiki]: failed to update mirror repository wiki:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return nil, false @@ -568,7 +568,7 @@ func checkAndUpdateEmptyRepository(m *repo_model.Mirror, gitRepo *git.Repository if !git.IsErrUnsupportedVersion(err) { log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err) desc := fmt.Sprintf("Failed to uupdate default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return false @@ -579,7 +579,7 @@ func checkAndUpdateEmptyRepository(m *repo_model.Mirror, gitRepo *git.Repository if err := repo_model.UpdateRepositoryCols(db.DefaultContext, m.Repo, "default_branch", "is_empty"); err != nil { log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err) desc := fmt.Sprintf("Failed to uupdate default branch of repository '%s': %v", m.Repo.RepoPath(), err) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return false diff --git a/services/repository/check.go b/services/repository/check.go index 17bdf2fac..2417db6a2 100644 --- a/services/repository/check.go +++ b/services/repository/check.go @@ -11,9 +11,9 @@ import ( "time" "code.gitea.io/gitea/models" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -42,7 +42,7 @@ func GitFsck(ctx context.Context, timeout time.Duration, args []string) error { repoPath := repo.RepoPath() if err := git.Fsck(ctx, repoPath, timeout, args...); err != nil { log.Warn("Failed to health check repository (%v): %v", repo, err) - if err = admin_model.CreateRepositoryNotice("Failed to health check repository (%s): %v", repo.FullName(), err); err != nil { + if err = system_model.CreateRepositoryNotice("Failed to health check repository (%s): %v", repo.FullName(), err); err != nil { log.Error("CreateRepositoryNotice: %v", err) } } @@ -83,7 +83,7 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) erro if err != nil { log.Error("Repository garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) desc := fmt.Sprintf("Repository garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %v", repo.FullName(), err) @@ -93,7 +93,7 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) erro if err := repo_module.UpdateRepoSize(ctx, repo); err != nil { log.Error("Updating size as part of garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) desc := fmt.Sprintf("Updating size as part of garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return fmt.Errorf("Updating size as part of garbage collection failed in repo: %s: Error: %v", repo.FullName(), err) @@ -135,7 +135,7 @@ func gatherMissingRepoRecords(ctx context.Context) ([]*repo_model.Repository, er if strings.HasPrefix(err.Error(), "Aborted gathering missing repo") { return nil, err } - if err2 := admin_model.CreateRepositoryNotice("gatherMissingRepoRecords: %v", err); err2 != nil { + if err2 := system_model.CreateRepositoryNotice("gatherMissingRepoRecords: %v", err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err2) } return nil, err @@ -163,7 +163,7 @@ func DeleteMissingRepositories(ctx context.Context, doer *user_model.User) error log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { log.Error("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err) - if err2 := admin_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { + if err2 := system_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err) } } @@ -191,7 +191,7 @@ func ReinitMissingRepositories(ctx context.Context) error { log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID) if err := git.InitRepository(ctx, repo.RepoPath(), true); err != nil { log.Error("Unable (re)initialize repository %d at %s. Error: %v", repo.ID, repo.RepoPath(), err) - if err2 := admin_model.CreateRepositoryNotice("InitRepository [%d]: %v", repo.ID, err); err2 != nil { + if err2 := system_model.CreateRepositoryNotice("InitRepository [%d]: %v", repo.ID, err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err2) } } diff --git a/services/repository/repository.go b/services/repository/repository.go index d53035836..47687d9a7 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -9,12 +9,12 @@ import ( "fmt" "code.gitea.io/gitea/models" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -83,8 +83,8 @@ func PushCreateRepo(authUser, owner *user_model.User, repoName string) (*repo_mo // Init start repository service func Init() error { repo_module.LoadRepoConfig() - admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repository uploads", setting.Repository.Upload.TempPath) - admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath()) + system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repository uploads", setting.Repository.Upload.TempPath) + system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath()) return initPushQueue() } diff --git a/services/user/user.go b/services/user/user.go index 1edd9294c..dab9ac61a 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -13,12 +13,12 @@ import ( "time" "code.gitea.io/gitea/models" - admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/eventsource" @@ -186,7 +186,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { path := user_model.UserPath(u.Name) if err := util.RemoveAll(path); err != nil { err = fmt.Errorf("Failed to RemoveAll %s: %v", path, err) - _ = admin_model.CreateNotice(ctx, admin_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) + _ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) return err } @@ -194,7 +194,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { avatarPath := u.CustomAvatarRelativePath() if err := storage.Avatars.Delete(avatarPath); err != nil { err = fmt.Errorf("Failed to remove %s: %v", avatarPath, err) - _ = admin_model.CreateNotice(ctx, admin_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) + _ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) return err } } diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index d2374ade5..8faada4d5 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -12,8 +12,8 @@ import ( "os" "strings" - admin_model "code.gitea.io/gitea/models/admin" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -384,6 +384,6 @@ func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error { return err } - admin_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath()) + system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath()) return nil } diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 0d9432b39..982cfb280 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -301,10 +301,18 @@
{{.locale.Tr "admin.config.disable_gravatar"}}
-
{{if .DisableGravatar}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
+
+ +
+
{{.locale.Tr "admin.config.enable_federated_avatar"}}
-
{{if .EnableFederatedAvatar}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
+
+ +
+
diff --git a/web_src/js/features/admin-common.js b/web_src/js/features/admin/common.js similarity index 100% rename from web_src/js/features/admin-common.js rename to web_src/js/features/admin/common.js diff --git a/web_src/js/features/admin/config.js b/web_src/js/features/admin/config.js new file mode 100644 index 000000000..f5d8fae8f --- /dev/null +++ b/web_src/js/features/admin/config.js @@ -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; + }); +} diff --git a/web_src/js/features/admin-emails.js b/web_src/js/features/admin/emails.js similarity index 100% rename from web_src/js/features/admin-emails.js rename to web_src/js/features/admin/emails.js diff --git a/web_src/js/features/admin-users.js b/web_src/js/features/admin/users.js similarity index 100% rename from web_src/js/features/admin-users.js rename to web_src/js/features/admin/users.js diff --git a/web_src/js/index.js b/web_src/js/index.js index b13ad0e13..a829deaf1 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -16,7 +16,8 @@ import initRepoMigration from './features/repo-migration.js'; import initRepoProject from './features/repo-projects.js'; import initServiceWorker from './features/serviceworker.js'; import initTableSort from './features/tablesort.js'; -import {initAdminUserListSearchForm} from './features/admin-users.js'; +import {initAdminUserListSearchForm} from './features/admin/users.js'; +import {initAdminConfigs} from './features/admin/config.js'; import {initMarkupAnchors} from './markup/anchors.js'; import {initNotificationCount, initNotificationsTable} from './features/notification.js'; import {initRepoIssueContentHistory} from './features/repo-issue-content.js'; @@ -60,8 +61,8 @@ import { initGlobalTooltips, } from './features/common-global.js'; import {initRepoTopicBar} from './features/repo-home.js'; -import {initAdminEmails} from './features/admin-emails.js'; -import {initAdminCommon} from './features/admin-common.js'; +import {initAdminEmails} from './features/admin/emails.js'; +import {initAdminCommon} from './features/admin/common.js'; import {initRepoTemplateSearch} from './features/repo-template.js'; import {initRepoCodeView} from './features/repo-code.js'; import {initSshKeyFormParser} from './features/sshkey-helper.js'; @@ -139,6 +140,7 @@ $(document).ready(() => { initAdminCommon(); initAdminEmails(); initAdminUserListSearchForm(); + initAdminConfigs(); initDashboardRepoList(); diff --git a/web_src/less/_base.less b/web_src/less/_base.less index 2abebe5c7..bfc6e0cf9 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -131,6 +131,7 @@ --color-timeline: #ececec; --color-input-text: #212121; --color-input-background: #ffffff; + --color-input-toggle-background: #dedede; --color-input-border: #dedede; --color-input-border-hover: #cecece; --color-navbar: #f8f8f8; diff --git a/web_src/less/_form.less b/web_src/less/_form.less index 08e1f324b..3d2ec9fb8 100644 --- a/web_src/less/_form.less +++ b/web_src/less/_form.less @@ -113,7 +113,7 @@ textarea:focus, } .ui.toggle.checkbox label::before { - background: var(--color-input-background); + background: var(--color-input-toggle-background); } .ui.toggle.checkbox label, diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index 12dba7926..fe8316215 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -106,6 +106,7 @@ --color-timeline: #4c525e; --color-input-text: #d5dbe6; --color-input-background: #232933; + --color-input-toggle-background: #454a57; --color-input-border: #454a57; --color-input-border-hover: #505667; --color-navbar: #2a2e3a;