Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/markup/sanitizer_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
"data-markdown-generated-content", "data-attr-class",
}
generalSafeElements := []string{
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "center", "i", "strong", "em", "a", "pre", "code", "img", "tt",
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
"details", "caption", "figure", "figcaption",
Expand Down
26 changes: 17 additions & 9 deletions modules/setting/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
)

type PictureStruct struct {
DisableGravatar *config.Value[bool]
EnableFederatedAvatar *config.Value[bool]
DisableGravatar *config.Option[bool]
EnableFederatedAvatar *config.Option[bool]
}

type OpenWithEditorApp struct {
Expand All @@ -23,6 +23,9 @@ type OpenWithEditorApp struct {

type OpenWithEditorAppsType []OpenWithEditorApp

// ToTextareaString is only used in templates, for help prompt only
// TODO: OPEN-WITH-EDITOR-APP-JSON: Because there is no "rich UI", a plain text editor is used to manage the list of apps
// Maybe we can use some better formats like Yaml in the future, then a simple textarea can manage the config clearly
func (t OpenWithEditorAppsType) ToTextareaString() string {
var ret strings.Builder
for _, app := range t {
Expand All @@ -31,7 +34,7 @@ func (t OpenWithEditorAppsType) ToTextareaString() string {
return ret.String()
}

func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
func openWithEditorAppsDefaultValue() OpenWithEditorAppsType {
return OpenWithEditorAppsType{
{
DisplayName: "VS Code",
Expand All @@ -49,13 +52,14 @@ func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
}

type RepositoryStruct struct {
OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
GitGuideRemoteName *config.Value[string]
OpenWithEditorApps *config.Option[OpenWithEditorAppsType]
GitGuideRemoteName *config.Option[string]
}

type ConfigStruct struct {
Picture *PictureStruct
Repository *RepositoryStruct
Instance *InstanceStruct
}

var (
Expand All @@ -67,12 +71,16 @@ func initDefaultConfig() {
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
defaultConfig = &ConfigStruct{
Picture: &PictureStruct{
DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
EnableFederatedAvatar: config.NewOption[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
},
Repository: &RepositoryStruct{
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
OpenWithEditorApps: config.NewOption[OpenWithEditorAppsType]("repository.open-with.editor-apps").WithEmptyAsDefault().WithDefaultFunc(openWithEditorAppsDefaultValue),
GitGuideRemoteName: config.NewOption[string]("repository.git-guide-remote-name").WithEmptyAsDefault().WithDefaultSimple("origin"),
},
Instance: &InstanceStruct{
WebBanner: config.NewOption[WebBannerType]("instance.web_banner"),
MaintenanceMode: config.NewOption[MaintenanceModeType]("instance.maintenance_mode"),
},
}
}
Expand Down
149 changes: 116 additions & 33 deletions modules/setting/config/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package config

import (
"context"
"reflect"
"sync"

"code.gitea.io/gitea/modules/json"
Expand All @@ -16,18 +17,31 @@ type CfgSecKey struct {
Sec, Key string
}

type Value[T any] struct {
// OptionInterface is used to overcome Golang's generic interface limitation
type OptionInterface interface {
GetDefaultValue() any
}

type Option[T any] struct {
mu sync.RWMutex

cfgSecKey CfgSecKey
dynKey string

def, value T
value T
defSimple T
defFunc func() T
emptyAsDef bool
has bool
revision int
}

func (value *Value[T]) parse(key, valStr string) (v T) {
v = value.def
func (opt *Option[T]) GetDefaultValue() any {
return opt.DefaultValue()
}

func (opt *Option[T]) parse(key, valStr string) (v T) {
v = opt.DefaultValue()
if valStr != "" {
if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
Expand All @@ -36,63 +50,132 @@ func (value *Value[T]) parse(key, valStr string) (v T) {
return v
}

func (value *Value[T]) Value(ctx context.Context) (v T) {
func (opt *Option[T]) HasValue(ctx context.Context) bool {
_, _, has := opt.ValueRevision(ctx)
return has
}

func (opt *Option[T]) Value(ctx context.Context) (v T) {
v, _, _ = opt.ValueRevision(ctx)
return v
}

func isZeroOrEmpty(v any) bool {
if v == nil {
return true // interface itself is nil
}
r := reflect.ValueOf(v)
if r.IsZero() {
return true
}

if r.Kind() == reflect.Slice || r.Kind() == reflect.Map {
if r.IsNil() {
return true
}
return r.Len() == 0
}
return false
}

func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) {
dg := GetDynGetter()
if dg == nil {
// this is an edge case: the database is not initialized but the system setting is going to be used
// it should panic to avoid inconsistent config values (from config / system setting) and fix the code
panic("no config dyn value getter")
}

rev := dg.GetRevision(ctx)
rev = dg.GetRevision(ctx)

// if the revision in the database doesn't change, use the last value
value.mu.RLock()
if rev == value.revision {
v = value.value
value.mu.RUnlock()
return v
opt.mu.RLock()
if rev == opt.revision {
v = opt.value
has = opt.has
opt.mu.RUnlock()
return v, rev, has
}
value.mu.RUnlock()
opt.mu.RUnlock()

// try to parse the config and cache it
var valStr *string
if dynVal, has := dg.GetValue(ctx, value.dynKey); has {
if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue {
valStr = &dynVal
} else if cfgVal, has := GetCfgSecKeyGetter().GetValue(value.cfgSecKey.Sec, value.cfgSecKey.Key); has {
} else if cfgVal, has := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); has {
valStr = &cfgVal
}
if valStr == nil {
v = value.def
v = opt.DefaultValue()
has = false
} else {
v = value.parse(value.dynKey, *valStr)
v = opt.parse(opt.dynKey, *valStr)
if opt.emptyAsDef && isZeroOrEmpty(v) {
v = opt.DefaultValue()
} else {
has = true
}
}

value.mu.Lock()
value.value = v
value.revision = rev
value.mu.Unlock()
return v
opt.mu.Lock()
opt.value = v
opt.revision = rev
opt.has = has
opt.mu.Unlock()
return v, rev, has
}

func (opt *Option[T]) DynKey() string {
return opt.dynKey
}

// WithDefaultFunc sets the default value with a function
// The "def" value might be changed during runtime (e.g.: Unmarshal with default), so it shouldn't use the same pointer or slice
func (opt *Option[T]) WithDefaultFunc(f func() T) *Option[T] {
opt.defFunc = f
return opt
}

func (value *Value[T]) DynKey() string {
return value.dynKey
func (opt *Option[T]) WithDefaultSimple(def T) *Option[T] {
v := any(def)
switch v.(type) {
case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
default:
// TODO: use reflect to support convertable basic types like `type State string`
r := reflect.ValueOf(v)
if r.Kind() != reflect.Struct {
panic("invalid type for default value, use WithDefaultFunc instead")
}
}
opt.defSimple = def
return opt
}

func (value *Value[T]) WithDefault(def T) *Value[T] {
value.def = def
return value
func (opt *Option[T]) WithEmptyAsDefault() *Option[T] {
opt.emptyAsDef = true
return opt
}

func (value *Value[T]) DefaultValue() T {
return value.def
func (opt *Option[T]) DefaultValue() T {
if opt.defFunc != nil {
return opt.defFunc()
}
return opt.defSimple
}

func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
value.cfgSecKey = cfgSecKey
return value
func (opt *Option[T]) WithFileConfig(cfgSecKey CfgSecKey) *Option[T] {
opt.cfgSecKey = cfgSecKey
return opt
}

var allConfigOptions = map[string]OptionInterface{}

func NewOption[T any](dynKey string) *Option[T] {
v := &Option[T]{dynKey: dynKey}
allConfigOptions[dynKey] = v
return v
}

func ValueJSON[T any](dynKey string) *Value[T] {
return &Value[T]{dynKey: dynKey}
func GetConfigOption(dynKey string) OptionInterface {
return allConfigOptions[dynKey]
}
58 changes: 58 additions & 0 deletions modules/setting/config_option_instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
"time"

"code.gitea.io/gitea/modules/setting/config"
)

// WebBannerType fields are directly used in templates,
// do remember to update the template if you change the fields
type WebBannerType struct {
DisplayEnabled bool
ContentMessage string
StartTimeUnix int64
EndTimeUnix int64
}

func (b WebBannerType) ShouldDisplay() bool {
if !b.DisplayEnabled || b.ContentMessage == "" {
return false
}
now := time.Now().Unix()
if b.StartTimeUnix > 0 && now < b.StartTimeUnix {
return false
}
if b.EndTimeUnix > 0 && now > b.EndTimeUnix {
return false
}
return true
}

type MaintenanceModeType struct {
AdminWebAccessOnly bool
StartTimeUnix int64
EndTimeUnix int64
}

func (m MaintenanceModeType) IsActive() bool {
if !m.AdminWebAccessOnly {
return false
}
now := time.Now().Unix()
if m.StartTimeUnix > 0 && now < m.StartTimeUnix {
return false
}
if m.EndTimeUnix > 0 && now > m.EndTimeUnix {
return false
}
return true
}

type InstanceStruct struct {
WebBanner *config.Option[WebBannerType]
MaintenanceMode *config.Option[MaintenanceModeType]
}
6 changes: 5 additions & 1 deletion modules/web/middleware/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import (
"code.gitea.io/gitea/modules/util"
)

const cookieRedirectTo = "redirect_to"
const (
CookieWebBannerDismissed = "gitea_disbnr"
CookieTheme = "gitea_theme"
cookieRedirectTo = "redirect_to"
)

func GetRedirectToCookie(req *http.Request) string {
return GetSiteCookie(req, cookieRedirectTo)
Expand Down
8 changes: 8 additions & 0 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"save": "Save",
"add": "Add",
"add_all": "Add All",
"dismiss": "Dismiss",
"remove": "Remove",
"remove_all": "Remove All",
"remove_label_str": "Remove item \"%s\"",
Expand Down Expand Up @@ -3278,6 +3279,13 @@
"admin.config.cache_test_failed": "Failed to probe the cache: %v.",
"admin.config.cache_test_slow": "Cache test successful, but response is slow: %s.",
"admin.config.cache_test_succeeded": "Cache test successful, got a response in %s.",
"admin.config.common.start_time": "Start time",
"admin.config.common.end_time": "End time",
"admin.config.common.skip_time_check": "Leave time empty (clear the field) to skip time check",
"admin.config.instance_maintenance": "Instance Maintenance",
"admin.config.instance_maintenance_mode.admin_web_access_only": "Only allow admin to access the web UI",
"admin.config.instance_web_banner.enabled": "Show banner",
"admin.config.instance_web_banner.message_placeholder": "Banner message (supports markdown)",
"admin.config.session_config": "Session Configuration",
"admin.config.session_provider": "Session Provider",
"admin.config.provider_config": "Provider Config",
Expand Down
5 changes: 2 additions & 3 deletions routers/common/errpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
Expand All @@ -36,9 +37,7 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in
w.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
}

tmplCtx := context.NewTemplateContext(req.Context(), req)
tmplCtx["Locale"] = middleware.Locale(w, req)

tmplCtx := context.NewTemplateContextForWeb(reqctx.FromContext(req.Context()), req, middleware.Locale(w, req))
w.WriteHeader(respCode)

outBuf := &bytes.Buffer{}
Expand Down
Loading