feat: vendor gitea 1.16.2
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -25,10 +26,12 @@ var (
|
||||
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
|
||||
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
|
||||
SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
|
||||
WorkflowDirs []string `ini:"WORKFLOW_DIRS"`
|
||||
}{
|
||||
Enabled: true,
|
||||
DefaultActionsURL: defaultActionsURLGitHub,
|
||||
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
|
||||
WorkflowDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -119,5 +122,20 @@ func loadActionsFrom(rootCfg ConfigProvider) error {
|
||||
return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression)
|
||||
}
|
||||
|
||||
workflowDirs := make([]string, 0, len(Actions.WorkflowDirs))
|
||||
for _, dir := range Actions.WorkflowDirs {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
dir = strings.ReplaceAll(dir, `\`, `/`)
|
||||
dir = strings.TrimRight(dir, "/")
|
||||
workflowDirs = append(workflowDirs, dir)
|
||||
}
|
||||
if len(workflowDirs) == 0 {
|
||||
return errors.New("[actions] WORKFLOW_DIRS must contain at least one entry")
|
||||
}
|
||||
Actions.WorkflowDirs = workflowDirs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -97,6 +97,65 @@ STORAGE_TYPE = minio
|
||||
assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
|
||||
}
|
||||
|
||||
func Test_WorkflowDirs(t *testing.T) {
|
||||
oldActions := Actions
|
||||
defer func() {
|
||||
Actions = oldActions
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
iniStr string
|
||||
wantDirs []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
iniStr: `[actions]`,
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "single dir",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows",
|
||||
wantDirs: []string{".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "custom order",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows,.gitea/workflows",
|
||||
wantDirs: []string{".github/workflows", ".gitea/workflows"},
|
||||
},
|
||||
{
|
||||
name: "whitespace trimming",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows , .github/workflows ",
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "trailing slash normalization",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows/,.github/workflows/",
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "only commas and whitespace",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = , , ,",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(tt.iniStr)
|
||||
require.NoError(t, err)
|
||||
err = loadActionsFrom(cfg)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantDirs, Actions.WorkflowDirs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getDefaultActionsURLForActions(t *testing.T) {
|
||||
oldActions := Actions
|
||||
oldAppURL := AppURL
|
||||
|
||||
@@ -5,6 +5,7 @@ package setting
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// Admin settings
|
||||
@@ -15,12 +16,33 @@ var Admin struct {
|
||||
ExternalUserDisableFeatures container.Set[string]
|
||||
}
|
||||
|
||||
var validUserFeatures = container.SetOf(
|
||||
UserFeatureDeletion,
|
||||
UserFeatureManageSSHKeys,
|
||||
UserFeatureManageGPGKeys,
|
||||
UserFeatureManageMFA,
|
||||
UserFeatureManageCredentials,
|
||||
UserFeatureChangeUsername,
|
||||
UserFeatureChangeFullName,
|
||||
)
|
||||
|
||||
func loadAdminFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("admin")
|
||||
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
|
||||
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
|
||||
Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
|
||||
Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures)
|
||||
|
||||
for feature := range Admin.UserDisabledFeatures {
|
||||
if !validUserFeatures.Contains(feature) {
|
||||
log.Warn("USER_DISABLED_FEATURES contains unknown feature %q", feature)
|
||||
}
|
||||
}
|
||||
for feature := range Admin.ExternalUserDisableFeatures {
|
||||
if !validUserFeatures.Contains(feature) && !Admin.UserDisabledFeatures.Contains(feature) {
|
||||
log.Warn("EXTERNAL_USER_DISABLE_FEATURES contains unknown feature %q", feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -16,13 +16,9 @@ var Attachment AttachmentSettingType
|
||||
func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
|
||||
Attachment = AttachmentSettingType{
|
||||
AllowedTypes: ".avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip",
|
||||
|
||||
// FIXME: this size is used for both "issue attachment" and "release attachment"
|
||||
// The design is not right, these two should be different settings
|
||||
MaxSize: 2048,
|
||||
|
||||
MaxFiles: 5,
|
||||
Enabled: true,
|
||||
MaxSize: 100,
|
||||
MaxFiles: 5,
|
||||
Enabled: true,
|
||||
}
|
||||
sec, _ := rootCfg.GetSection("attachment")
|
||||
if sec == nil {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -11,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 {
|
||||
@@ -22,15 +23,18 @@ 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 {
|
||||
ret := ""
|
||||
var ret strings.Builder
|
||||
for _, app := range t {
|
||||
ret += app.DisplayName + " = " + app.OpenURL + "\n"
|
||||
ret.WriteString(app.DisplayName + " = " + app.OpenURL + "\n")
|
||||
}
|
||||
return ret
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
|
||||
func openWithEditorAppsDefaultValue() OpenWithEditorAppsType {
|
||||
return OpenWithEditorAppsType{
|
||||
{
|
||||
DisplayName: "VS Code",
|
||||
@@ -48,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 (
|
||||
@@ -66,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").WithDefaultSimple(true).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"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
@@ -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)
|
||||
@@ -36,7 +50,35 @@ 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
|
||||
@@ -44,55 +86,96 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
|
||||
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, hasCfgValue := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); hasCfgValue {
|
||||
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()
|
||||
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 (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 convertible 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 (opt *Option[T]) WithEmptyAsDefault() *Option[T] {
|
||||
opt.emptyAsDef = true
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *Option[T]) DefaultValue() T {
|
||||
if opt.defFunc != nil {
|
||||
return opt.defFunc()
|
||||
}
|
||||
return opt.defSimple
|
||||
}
|
||||
|
||||
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 (value *Value[T]) DynKey() string {
|
||||
return value.dynKey
|
||||
}
|
||||
|
||||
func (value *Value[T]) WithDefault(def T) *Value[T] {
|
||||
value.def = def
|
||||
return value
|
||||
}
|
||||
|
||||
func (value *Value[T]) DefaultValue() T {
|
||||
return value.def
|
||||
}
|
||||
|
||||
func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
|
||||
value.cfgSecKey = cfgSecKey
|
||||
return value
|
||||
}
|
||||
|
||||
func ValueJSON[T any](dynKey string) *Value[T] {
|
||||
return &Value[T]{dynKey: dynKey}
|
||||
func GetConfigOption(dynKey string) OptionInterface {
|
||||
return allConfigOptions[dynKey]
|
||||
}
|
||||
|
||||
@@ -51,10 +51,10 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
||||
for _, unescapeIdx := range escapeStringIndices {
|
||||
preceding := encoded[last:unescapeIdx[0]]
|
||||
if !inKey {
|
||||
if splitter := strings.Index(preceding, "__"); splitter > -1 {
|
||||
section += preceding[:splitter]
|
||||
if before, after, cutOk := strings.Cut(preceding, "__"); cutOk {
|
||||
section += before
|
||||
inKey = true
|
||||
key += preceding[splitter+2:]
|
||||
key += after
|
||||
} else {
|
||||
section += preceding
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
||||
decodedBytes := make([]byte, len(toDecode)/2)
|
||||
for i := 0; i < len(toDecode)/2; i++ {
|
||||
// Can ignore error here as we know these should be hexadecimal from the regexp
|
||||
byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 0)
|
||||
byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 8)
|
||||
decodedBytes[i] = byte(byteInt)
|
||||
}
|
||||
if inKey {
|
||||
@@ -77,9 +77,9 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
||||
}
|
||||
remaining := encoded[last:]
|
||||
if !inKey {
|
||||
if splitter := strings.Index(remaining, "__"); splitter > -1 {
|
||||
section += remaining[:splitter]
|
||||
key += remaining[splitter+2:]
|
||||
if before, after, cutOk := strings.Cut(remaining, "__"); cutOk {
|
||||
section += before
|
||||
key += after
|
||||
} else {
|
||||
section += remaining
|
||||
}
|
||||
@@ -111,21 +111,21 @@ func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, sect
|
||||
|
||||
func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
|
||||
for _, kv := range envs {
|
||||
idx := strings.IndexByte(kv, '=')
|
||||
if idx < 0 {
|
||||
before, after, ok := strings.Cut(kv, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse the environment variable to config section name and key name
|
||||
envKey := kv[:idx]
|
||||
envValue := kv[idx+1:]
|
||||
envKey := before
|
||||
envValue := after
|
||||
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// use environment value as config value, or read the file content as value if the key indicates a file
|
||||
keyValue := envValue
|
||||
keyValue := envValue //nolint:staticcheck // false positive
|
||||
if useFileValue {
|
||||
fileContent, err := os.ReadFile(envValue)
|
||||
if err != nil {
|
||||
@@ -167,24 +167,13 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
|
||||
return changed
|
||||
}
|
||||
|
||||
// InitGiteaEnvVars initializes the environment variables for gitea
|
||||
func InitGiteaEnvVars() {
|
||||
func UnsetUnnecessaryEnvVars() {
|
||||
// Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want,
|
||||
// but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users,
|
||||
// so at the moment we could still keep "unsetting the unnecessary environments"
|
||||
// but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users.
|
||||
// So at the moment we just keep "unsetting the unnecessary environment variables".
|
||||
|
||||
// HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig".
|
||||
// But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist,
|
||||
// then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set.
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
|
||||
func InitGiteaEnvVarsForTesting() {
|
||||
InitGiteaEnvVars()
|
||||
_ = os.Unsetenv("GIT_AUTHOR_NAME")
|
||||
_ = os.Unsetenv("GIT_AUTHOR_EMAIL")
|
||||
_ = os.Unsetenv("GIT_AUTHOR_DATE")
|
||||
_ = os.Unsetenv("GIT_COMMITTER_NAME")
|
||||
_ = os.Unsetenv("GIT_COMMITTER_EMAIL")
|
||||
_ = os.Unsetenv("GIT_COMMITTER_DATE")
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -41,6 +41,7 @@ type ConfigSection interface {
|
||||
HasKey(key string) bool
|
||||
NewKey(name, value string) (ConfigKey, error)
|
||||
Key(key string) ConfigKey
|
||||
DeleteKey(key string)
|
||||
Keys() []ConfigKey
|
||||
ChildSections() []ConfigSection
|
||||
}
|
||||
@@ -51,6 +52,7 @@ type ConfigProvider interface {
|
||||
Sections() []ConfigSection
|
||||
NewSection(name string) (ConfigSection, error)
|
||||
GetSection(name string) (ConfigSection, error)
|
||||
DeleteSection(name string)
|
||||
Save() error
|
||||
SaveTo(filename string) error
|
||||
|
||||
@@ -168,6 +170,10 @@ func (s *iniConfigSection) Keys() (keys []ConfigKey) {
|
||||
return keys
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) DeleteKey(key string) {
|
||||
s.sec.DeleteKey(key)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
|
||||
for _, s := range s.sec.ChildSections() {
|
||||
sections = append(sections, &iniConfigSection{s})
|
||||
@@ -249,6 +255,10 @@ func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
|
||||
return &iniConfigSection{sec: sec}, nil
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) DeleteSection(name string) {
|
||||
p.ini.DeleteSection(name)
|
||||
}
|
||||
|
||||
var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save")
|
||||
|
||||
// Save saves the content into file
|
||||
@@ -338,23 +348,6 @@ func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
|
||||
}
|
||||
}
|
||||
|
||||
// NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content
|
||||
func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) {
|
||||
iniFile, err := ini.LoadSources(ini.LoadOptions{
|
||||
IgnoreInlineComment: true,
|
||||
UnescapeValueCommentSymbols: true,
|
||||
IgnoreContinuation: true,
|
||||
}, source, others...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load locale ini: %w", err)
|
||||
}
|
||||
iniFile.BlockMode = false
|
||||
return &iniConfigProvider{
|
||||
ini: iniFile,
|
||||
loadedFromEmpty: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ini.PrettyFormat = false
|
||||
}
|
||||
|
||||
@@ -113,24 +113,6 @@ func TestNewConfigProviderFromFile(t *testing.T) {
|
||||
assert.Equal(t, "[foo]\nk1 = a\n\n[bar]\nk1 = b\n", string(bs))
|
||||
}
|
||||
|
||||
func TestNewConfigProviderForLocale(t *testing.T) {
|
||||
// load locale from file
|
||||
localeFile := t.TempDir() + "/locale.ini"
|
||||
_ = os.WriteFile(localeFile, []byte(`k1=a`), 0o644)
|
||||
cfg, err := NewConfigProviderForLocale(localeFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a", cfg.Section("").Key("k1").String())
|
||||
|
||||
// load locale from bytes
|
||||
cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", cfg.Section("").Key("k1").String())
|
||||
cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"), []byte("k2=xxx"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", cfg.Section("").Key("k1").String())
|
||||
assert.Equal(t, "xxx", cfg.Section("").Key("k2").String())
|
||||
}
|
||||
|
||||
func TestDisableSaving(t *testing.T) {
|
||||
testFile := t.TempDir() + "/test.ini"
|
||||
_ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644)
|
||||
|
||||
@@ -15,13 +15,11 @@ var CORSConfig = struct {
|
||||
MaxAge time.Duration
|
||||
AllowCredentials bool
|
||||
Headers []string
|
||||
XFrameOptions string
|
||||
}{
|
||||
AllowDomain: []string{"*"},
|
||||
Methods: []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
Headers: []string{"Content-Type", "User-Agent"},
|
||||
MaxAge: 10 * time.Minute,
|
||||
XFrameOptions: "SAMEORIGIN",
|
||||
AllowDomain: []string{"*"},
|
||||
Methods: []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
Headers: []string{"Content-Type", "User-Agent"},
|
||||
MaxAge: 10 * time.Minute,
|
||||
}
|
||||
|
||||
func loadCorsFrom(rootCfg ConfigProvider) {
|
||||
|
||||
@@ -5,6 +5,7 @@ package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,54 +18,47 @@ var Git = struct {
|
||||
HomePath string
|
||||
DisableDiffHighlight bool
|
||||
|
||||
MaxGitDiffLines int
|
||||
MaxGitDiffLineCharacters int
|
||||
MaxGitDiffFiles int
|
||||
CommitsRangeSize int // CommitsRangeSize the default commits range size
|
||||
BranchesRangeSize int // BranchesRangeSize the default branches range size
|
||||
VerbosePush bool
|
||||
VerbosePushDelay time.Duration
|
||||
GCArgs []string `ini:"GC_ARGS" delim:" "`
|
||||
EnableAutoGitWireProtocol bool
|
||||
PullRequestPushMessage bool
|
||||
LargeObjectThreshold int64
|
||||
DisableCoreProtectNTFS bool
|
||||
DisablePartialClone bool
|
||||
Timeout struct {
|
||||
Default int
|
||||
MaxGitDiffLines int
|
||||
MaxGitDiffLineCharacters int
|
||||
MaxGitDiffFiles int
|
||||
CommitsRangeSize int // CommitsRangeSize the default commits range size
|
||||
BranchesRangeSize int // BranchesRangeSize the default branches range size
|
||||
VerbosePush bool
|
||||
VerbosePushDelay time.Duration
|
||||
GCArgs []string `ini:"GC_ARGS" delim:" "`
|
||||
EnableAutoGitWireProtocol bool
|
||||
PullRequestPushMessage bool
|
||||
LargeObjectThreshold int64
|
||||
DisableCoreProtectNTFS bool
|
||||
DisablePartialClone bool
|
||||
DiffRenameSimilarityThreshold string
|
||||
Timeout struct {
|
||||
Migrate int
|
||||
Mirror int
|
||||
Clone int
|
||||
Pull int
|
||||
GC int `ini:"GC"`
|
||||
} `ini:"git.timeout"`
|
||||
}{
|
||||
DisableDiffHighlight: false,
|
||||
MaxGitDiffLines: 1000,
|
||||
MaxGitDiffLineCharacters: 5000,
|
||||
MaxGitDiffFiles: 100,
|
||||
CommitsRangeSize: 50,
|
||||
BranchesRangeSize: 20,
|
||||
VerbosePush: true,
|
||||
VerbosePushDelay: 5 * time.Second,
|
||||
GCArgs: []string{},
|
||||
EnableAutoGitWireProtocol: true,
|
||||
PullRequestPushMessage: true,
|
||||
LargeObjectThreshold: 1024 * 1024,
|
||||
DisablePartialClone: false,
|
||||
DisableDiffHighlight: false,
|
||||
MaxGitDiffLines: 1000,
|
||||
MaxGitDiffLineCharacters: 5000,
|
||||
MaxGitDiffFiles: 100,
|
||||
CommitsRangeSize: 50,
|
||||
BranchesRangeSize: 20,
|
||||
VerbosePush: true,
|
||||
VerbosePushDelay: 5 * time.Second,
|
||||
GCArgs: []string{},
|
||||
EnableAutoGitWireProtocol: true,
|
||||
PullRequestPushMessage: true,
|
||||
LargeObjectThreshold: 1024 * 1024,
|
||||
DisablePartialClone: false,
|
||||
DiffRenameSimilarityThreshold: "50%",
|
||||
Timeout: struct {
|
||||
Default int
|
||||
Migrate int
|
||||
Mirror int
|
||||
Clone int
|
||||
Pull int
|
||||
GC int `ini:"GC"`
|
||||
}{
|
||||
Default: 360,
|
||||
Migrate: 600,
|
||||
Mirror: 300,
|
||||
Clone: 300,
|
||||
Pull: 300,
|
||||
GC: 60,
|
||||
},
|
||||
}
|
||||
@@ -117,4 +111,9 @@ func loadGitFrom(rootCfg ConfigProvider) {
|
||||
} else {
|
||||
Git.HomePath = filepath.Clean(Git.HomePath)
|
||||
}
|
||||
|
||||
// validate for a integer percentage between 0% and 100%
|
||||
if !regexp.MustCompile(`^([0-9]|[1-9][0-9]|100)%$`).MatchString(Git.DiffRenameSimilarityThreshold) {
|
||||
log.Fatal("Invalid git.DIFF_RENAME_SIMILARITY_THRESHOLD: %s", Git.DiffRenameSimilarityThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
const IncomingEmailTokenPlaceholder = "%{token}"
|
||||
|
||||
var IncomingEmail = struct {
|
||||
Enabled bool
|
||||
ReplyToAddress string
|
||||
TokenPlaceholder string `ini:"-"`
|
||||
Host string
|
||||
Port int
|
||||
UseTLS bool `ini:"USE_TLS"`
|
||||
@@ -28,7 +29,6 @@ var IncomingEmail = struct {
|
||||
}{
|
||||
Mailbox: "INBOX",
|
||||
DeleteHandledMessage: true,
|
||||
TokenPlaceholder: "%{token}",
|
||||
MaximumMessageSize: 10485760,
|
||||
}
|
||||
|
||||
@@ -54,19 +54,10 @@ func checkReplyToAddress() error {
|
||||
return errors.New("name must not be set")
|
||||
}
|
||||
|
||||
c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder)
|
||||
switch c {
|
||||
case 0:
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
||||
case 1:
|
||||
default:
|
||||
return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder)
|
||||
placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder)
|
||||
userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@")
|
||||
if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) {
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder)
|
||||
}
|
||||
|
||||
parts := strings.Split(IncomingEmail.ReplyToAddress, "@")
|
||||
if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) {
|
||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func checkGlobMatch(t *testing.T, globstr string, list []indexerMatchList) {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
assert.Equal(t, m.position, -1, "Test string `%s` doesn't match `%s` anywhere; expected @%d", m.value, globstr, m.position)
|
||||
assert.Equal(t, -1, m.position, "Test string `%s` doesn't match `%s` anywhere; expected @%d", m.value, globstr, m.position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +81,7 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
|
||||
jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
|
||||
}
|
||||
LFS.JWTSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64()
|
||||
|
||||
// Save secret
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
|
||||
@@ -6,17 +6,19 @@ package setting
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_loadMailerFrom(t *testing.T) {
|
||||
kases := map[string]*Mailer{
|
||||
"smtp.mydomain.com": {
|
||||
SMTPAddr: "smtp.mydomain.com",
|
||||
"smtp.mydomain.test": {
|
||||
SMTPAddr: "smtp.mydomain.test",
|
||||
SMTPPort: "465",
|
||||
},
|
||||
"smtp.mydomain.com:123": {
|
||||
SMTPAddr: "smtp.mydomain.com",
|
||||
"smtp.mydomain.test:123": {
|
||||
SMTPAddr: "smtp.mydomain.test",
|
||||
SMTPPort: "123",
|
||||
},
|
||||
":123": {
|
||||
@@ -39,3 +41,30 @@ func Test_loadMailerFrom(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSettingsForInstallMailServiceFlags(t *testing.T) {
|
||||
defer test.MockVariableValue(&Service)()
|
||||
defer test.MockVariableValue(&MailService)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[database]
|
||||
DB_TYPE = postgres
|
||||
|
||||
[mailer]
|
||||
ENABLED = true
|
||||
SMTP_ADDR = 127.0.0.1
|
||||
SMTP_PORT = 465
|
||||
FROM = noreply@example.com
|
||||
|
||||
[service]
|
||||
REGISTER_EMAIL_CONFIRM = true
|
||||
ENABLE_NOTIFY_MAIL = true
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadDBSetting(cfg)
|
||||
loadServiceFrom(cfg)
|
||||
loadMailsFrom(cfg)
|
||||
|
||||
assert.True(t, Service.RegisterEmailConfirm)
|
||||
assert.True(t, Service.EnableNotifyMail)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package setting
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -43,22 +44,20 @@ var Markdown = struct {
|
||||
RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"`
|
||||
|
||||
CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` // Actually it is a "markup" option because it is used in "post processor"
|
||||
FileExtensions []string
|
||||
FileNamePatterns []string `ini:"-"`
|
||||
|
||||
EnableMath bool
|
||||
MathCodeBlockDetection []string
|
||||
MathCodeBlockOptions MarkdownMathCodeBlockOptions `ini:"-"`
|
||||
}{
|
||||
FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
|
||||
EnableMath: true,
|
||||
EnableMath: true,
|
||||
}
|
||||
|
||||
// MarkupRenderer defines the external parser configured in ini
|
||||
type MarkupRenderer struct {
|
||||
Enabled bool
|
||||
MarkupName string
|
||||
Command string
|
||||
FileExtensions []string
|
||||
FilePatterns []string
|
||||
IsInputFile bool
|
||||
NeedPostProcess bool
|
||||
MarkupSanitizerRules []MarkupSanitizerRule
|
||||
@@ -77,6 +76,13 @@ type MarkupSanitizerRule struct {
|
||||
|
||||
func loadMarkupFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "markdown", &Markdown)
|
||||
|
||||
markdownFileExtensions := rootCfg.Section("markdown").Key("FILE_EXTENSIONS").Strings(",")
|
||||
if len(markdownFileExtensions) == 0 || len(markdownFileExtensions) == 1 && markdownFileExtensions[0] == "" {
|
||||
markdownFileExtensions = []string{".md", ".markdown", ".mdown", ".mkd", ".livemd"}
|
||||
}
|
||||
Markdown.FileNamePatterns = fileExtensionsToPatterns("markdown", markdownFileExtensions)
|
||||
|
||||
const none = "none"
|
||||
|
||||
const renderOptionShortIssuePattern = "short-issue-pattern"
|
||||
@@ -215,21 +221,30 @@ func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerR
|
||||
return rule, true
|
||||
}
|
||||
|
||||
func newMarkupRenderer(name string, sec ConfigSection) {
|
||||
extensionReg := regexp.MustCompile(`\.\w`)
|
||||
var extensionReg = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^(\.[-\w]+)+$`)
|
||||
})
|
||||
|
||||
extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
|
||||
exts := make([]string, 0, len(extensions))
|
||||
func fileExtensionsToPatterns(sectionName string, extensions []string) []string {
|
||||
patterns := make([]string, 0, len(extensions))
|
||||
for _, extension := range extensions {
|
||||
if !extensionReg.MatchString(extension) {
|
||||
log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
|
||||
if !extensionReg().MatchString(extension) {
|
||||
log.Warn("Config section %s file extension %s is invalid. Extension ignored", sectionName, extension)
|
||||
} else {
|
||||
exts = append(exts, extension)
|
||||
patterns = append(patterns, "*"+extension)
|
||||
}
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
if len(exts) == 0 {
|
||||
log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
|
||||
func newMarkupRenderer(name string, sec ConfigSection) {
|
||||
if !sec.Key("ENABLED").MustBool(false) {
|
||||
return
|
||||
}
|
||||
|
||||
fileNamePatterns := fileExtensionsToPatterns(name, sec.Key("FILE_EXTENSIONS").Strings(","))
|
||||
if len(fileNamePatterns) == 0 {
|
||||
log.Warn("Config section %s file extension is empty, markup render is ignored", name)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -255,18 +270,17 @@ func newMarkupRenderer(name string, sec ConfigSection) {
|
||||
}
|
||||
|
||||
// ATTENTION! at the moment, only a safe set like "allow-scripts" are allowed for sandbox mode.
|
||||
// "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token
|
||||
// "allow-same-origin" should NEVER be used, it leads to XSS attack: makes the JS in iframe can access parent window's config and send requests with user's credentials.
|
||||
renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString("allow-scripts allow-popups")
|
||||
if renderContentSandbox == "disabled" {
|
||||
renderContentSandbox = ""
|
||||
}
|
||||
|
||||
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
|
||||
Enabled: sec.Key("ENABLED").MustBool(false),
|
||||
MarkupName: name,
|
||||
FileExtensions: exts,
|
||||
Command: command,
|
||||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
||||
MarkupName: name,
|
||||
FilePatterns: fileNamePatterns,
|
||||
Command: command,
|
||||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
||||
|
||||
RenderContentMode: renderContentMode,
|
||||
RenderContentSandbox: renderContentSandbox,
|
||||
|
||||
@@ -133,16 +133,13 @@ func loadOAuth2From(rootCfg ConfigProvider) {
|
||||
|
||||
// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
|
||||
// Because this secret is also used as GeneralTokenSigningSecret (as a quick not-that-breaking fix for some legacy problems).
|
||||
// Including: CSRF token, account validation token, etc ...
|
||||
// Including: account validation token, etc ...
|
||||
// In main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
|
||||
jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
|
||||
if InstallLock {
|
||||
jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
log.Fatal("error generating JWT secret: %v", err)
|
||||
}
|
||||
jwtSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64()
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
if err != nil {
|
||||
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
|
||||
@@ -162,10 +159,7 @@ var generalSigningSecret atomic.Pointer[[]byte]
|
||||
func GetGeneralTokenSigningSecret() []byte {
|
||||
old := generalSigningSecret.Load()
|
||||
if old == nil || len(*old) == 0 {
|
||||
jwtSecret, _, err := generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
log.Fatal("Unable to generate general JWT secret: %v", err)
|
||||
}
|
||||
jwtSecret, _ := generate.NewJwtSecretWithBase64()
|
||||
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
@@ -16,30 +16,31 @@ var (
|
||||
Storage *Storage
|
||||
Enabled bool
|
||||
|
||||
LimitTotalOwnerCount int64
|
||||
LimitTotalOwnerSize int64
|
||||
LimitSizeAlpine int64
|
||||
LimitSizeArch int64
|
||||
LimitSizeCargo int64
|
||||
LimitSizeChef int64
|
||||
LimitSizeComposer int64
|
||||
LimitSizeConan int64
|
||||
LimitSizeConda int64
|
||||
LimitSizeContainer int64
|
||||
LimitSizeCran int64
|
||||
LimitSizeDebian int64
|
||||
LimitSizeGeneric int64
|
||||
LimitSizeGo int64
|
||||
LimitSizeHelm int64
|
||||
LimitSizeMaven int64
|
||||
LimitSizeNpm int64
|
||||
LimitSizeNuGet int64
|
||||
LimitSizePub int64
|
||||
LimitSizePyPI int64
|
||||
LimitSizeRpm int64
|
||||
LimitSizeRubyGems int64
|
||||
LimitSizeSwift int64
|
||||
LimitSizeVagrant int64
|
||||
LimitTotalOwnerCount int64
|
||||
LimitTotalOwnerSize int64
|
||||
LimitSizeAlpine int64
|
||||
LimitSizeArch int64
|
||||
LimitSizeCargo int64
|
||||
LimitSizeChef int64
|
||||
LimitSizeComposer int64
|
||||
LimitSizeConan int64
|
||||
LimitSizeConda int64
|
||||
LimitSizeContainer int64
|
||||
LimitSizeCran int64
|
||||
LimitSizeDebian int64
|
||||
LimitSizeGeneric int64
|
||||
LimitSizeGo int64
|
||||
LimitSizeHelm int64
|
||||
LimitSizeMaven int64
|
||||
LimitSizeNpm int64
|
||||
LimitSizeNuGet int64
|
||||
LimitSizePub int64
|
||||
LimitSizePyPI int64
|
||||
LimitSizeRpm int64
|
||||
LimitSizeRubyGems int64
|
||||
LimitSizeSwift int64
|
||||
LimitSizeTerraformState int64
|
||||
LimitSizeVagrant int64
|
||||
|
||||
DefaultRPMSignEnabled bool
|
||||
}{
|
||||
@@ -86,6 +87,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
|
||||
Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM")
|
||||
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
|
||||
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
|
||||
Packages.LimitSizeTerraformState = mustBytes(sec, "LIMIT_SIZE_TERRAFORM_STATE")
|
||||
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
|
||||
Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false)
|
||||
return nil
|
||||
|
||||
@@ -22,9 +22,7 @@ var (
|
||||
RenderedSizeFactor: 2,
|
||||
}
|
||||
|
||||
GravatarSource string
|
||||
DisableGravatar bool // Depreciated: migrated to database
|
||||
EnableFederatedAvatar bool // Depreciated: migrated to database
|
||||
GravatarSource string
|
||||
|
||||
RepoAvatar = struct {
|
||||
Storage *Storage
|
||||
@@ -65,29 +63,12 @@ func loadAvatarsFrom(rootCfg ConfigProvider) error {
|
||||
GravatarSource = source
|
||||
}
|
||||
|
||||
DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool(GetDefaultDisableGravatar())
|
||||
deprecatedSettingDB(rootCfg, "", "DISABLE_GRAVATAR")
|
||||
EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(GetDefaultEnableFederatedAvatar(DisableGravatar))
|
||||
deprecatedSettingDB(rootCfg, "", "ENABLE_FEDERATED_AVATAR")
|
||||
deprecatedSettingDB(rootCfg, "picture", "DISABLE_GRAVATAR")
|
||||
deprecatedSettingDB(rootCfg, "picture", "ENABLE_FEDERATED_AVATAR")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDefaultDisableGravatar() bool {
|
||||
return OfflineMode
|
||||
}
|
||||
|
||||
func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool {
|
||||
v := !InstallLock
|
||||
if OfflineMode {
|
||||
v = false
|
||||
}
|
||||
if disableGravatar {
|
||||
v = false
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func loadRepoAvatarFrom(rootCfg ConfigProvider) error {
|
||||
sec := rootCfg.Section("picture")
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ const (
|
||||
RepoCreatingPublic = "public"
|
||||
)
|
||||
|
||||
// enumerates the values for [repository.pull-request] DEFAULT_TITLE_SOURCE
|
||||
const (
|
||||
RepoPRTitleSourceFirstCommit = "first-commit"
|
||||
RepoPRTitleSourceAuto = "auto"
|
||||
)
|
||||
|
||||
// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
|
||||
const ItemsPerPage = 40
|
||||
|
||||
@@ -86,9 +92,10 @@ var (
|
||||
DefaultMergeMessageOfficialApproversOnly bool
|
||||
PopulateSquashCommentWithCommitMessages bool
|
||||
AddCoCommitterTrailers bool
|
||||
TestConflictingPatchesWithGitApply bool
|
||||
RetargetChildrenOnMerge bool
|
||||
DelayCheckForInactiveDays int
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultTitleSource string
|
||||
} `ini:"repository.pull-request"`
|
||||
|
||||
// Issue Setting
|
||||
@@ -100,6 +107,8 @@ var (
|
||||
Release struct {
|
||||
AllowedTypes string
|
||||
DefaultPagingNum int
|
||||
FileMaxSize int64
|
||||
MaxFiles int64
|
||||
} `ini:"repository.release"`
|
||||
|
||||
Signing struct {
|
||||
@@ -208,9 +217,10 @@ var (
|
||||
DefaultMergeMessageOfficialApproversOnly bool
|
||||
PopulateSquashCommentWithCommitMessages bool
|
||||
AddCoCommitterTrailers bool
|
||||
TestConflictingPatchesWithGitApply bool
|
||||
RetargetChildrenOnMerge bool
|
||||
DelayCheckForInactiveDays int
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultTitleSource string
|
||||
}{
|
||||
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
|
||||
// Same as GitHub. See
|
||||
@@ -227,6 +237,7 @@ var (
|
||||
AddCoCommitterTrailers: true,
|
||||
RetargetChildrenOnMerge: true,
|
||||
DelayCheckForInactiveDays: 7,
|
||||
DefaultTitleSource: RepoPRTitleSourceAuto,
|
||||
},
|
||||
|
||||
// Issue settings
|
||||
@@ -241,9 +252,13 @@ var (
|
||||
Release: struct {
|
||||
AllowedTypes string
|
||||
DefaultPagingNum int
|
||||
FileMaxSize int64
|
||||
MaxFiles int64
|
||||
}{
|
||||
AllowedTypes: "",
|
||||
DefaultPagingNum: 10,
|
||||
FileMaxSize: 2048,
|
||||
MaxFiles: 5,
|
||||
},
|
||||
|
||||
// Signing settings
|
||||
|
||||
@@ -14,6 +14,12 @@ import (
|
||||
)
|
||||
|
||||
// Security settings
|
||||
var Security = struct {
|
||||
// TODO: move more settings to this struct in future
|
||||
XFrameOptions string
|
||||
}{
|
||||
XFrameOptions: "SAMEORIGIN",
|
||||
}
|
||||
|
||||
var (
|
||||
InstallLock bool
|
||||
@@ -25,6 +31,7 @@ var (
|
||||
ReverseProxyAuthEmail string
|
||||
ReverseProxyAuthFullName string
|
||||
ReverseProxyLimit int
|
||||
ReverseProxyLogoutRedirect string
|
||||
ReverseProxyTrustedProxies []string
|
||||
MinPasswordLength int
|
||||
ImportLocalPaths bool
|
||||
@@ -36,8 +43,6 @@ var (
|
||||
PasswordCheckPwn bool
|
||||
SuccessfulTokensCacheSize int
|
||||
DisableQueryAuthToken bool
|
||||
CSRFCookieName = "_csrf"
|
||||
CSRFCookieHTTPOnly = true
|
||||
RecordUserSignupMetadata = false
|
||||
TwoFactorAuthEnforced = false
|
||||
)
|
||||
@@ -105,7 +110,6 @@ func generateSaveInternalToken(rootCfg ConfigProvider) {
|
||||
|
||||
func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("security")
|
||||
InstallLock = HasInstallLock(rootCfg)
|
||||
LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31)
|
||||
SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
|
||||
if SecretKey == "" {
|
||||
@@ -121,6 +125,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME")
|
||||
|
||||
ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1)
|
||||
ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").String()
|
||||
ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",")
|
||||
if len(ReverseProxyTrustedProxies) == 0 {
|
||||
ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"}
|
||||
@@ -139,10 +144,16 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString(""))
|
||||
}
|
||||
|
||||
CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true)
|
||||
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
|
||||
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
|
||||
|
||||
deprecatedSetting(rootCfg, "cors", "X_FRAME_OPTIONS", "security", "X_FRAME_OPTIONS", "v1.26.0")
|
||||
if sec.HasKey("X_FRAME_OPTIONS") {
|
||||
Security.XFrameOptions = sec.Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
|
||||
} else {
|
||||
Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
|
||||
}
|
||||
|
||||
twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String()
|
||||
switch twoFactorAuth {
|
||||
case "":
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// Scheme describes protocol types
|
||||
@@ -44,6 +43,7 @@ const (
|
||||
const (
|
||||
PublicURLAuto = "auto"
|
||||
PublicURLLegacy = "legacy"
|
||||
PublicURLNever = "never"
|
||||
)
|
||||
|
||||
// Server settings
|
||||
@@ -72,9 +72,6 @@ var (
|
||||
// It maps to ini:"LOCAL_ROOT_URL" in [server]
|
||||
LocalURL string
|
||||
|
||||
// AssetVersion holds an opaque value that is used for cache-busting assets
|
||||
AssetVersion string
|
||||
|
||||
// appTempPathInternal is the temporary path for the app, it is only an internal variable
|
||||
// DO NOT use it directly, always use AppDataTempDir
|
||||
appTempPathInternal string
|
||||
@@ -91,7 +88,6 @@ var (
|
||||
RedirectOtherPort bool
|
||||
RedirectorUseProxyProtocol bool
|
||||
PortToRedirect string
|
||||
OfflineMode bool
|
||||
CertFile string
|
||||
KeyFile string
|
||||
StaticRootPath string
|
||||
@@ -163,7 +159,7 @@ func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte {
|
||||
}
|
||||
|
||||
// MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash
|
||||
func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string {
|
||||
func MakeAbsoluteAssetURL(appURL *url.URL, staticURLPrefix string) string {
|
||||
parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/"))
|
||||
if err != nil {
|
||||
log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err)
|
||||
@@ -171,11 +167,12 @@ func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string {
|
||||
|
||||
if err == nil && parsedPrefix.Hostname() == "" {
|
||||
if staticURLPrefix == "" {
|
||||
return strings.TrimSuffix(appURL, "/")
|
||||
return strings.TrimSuffix(appURL.String(), "/")
|
||||
}
|
||||
|
||||
// StaticURLPrefix is just a path
|
||||
return util.URLJoin(appURL, strings.TrimSuffix(staticURLPrefix, "/"))
|
||||
appHostURL := &url.URL{Scheme: appURL.Scheme, Host: appURL.Host}
|
||||
return appHostURL.String() + "/" + strings.Trim(staticURLPrefix, "/")
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(staticURLPrefix, "/")
|
||||
@@ -286,8 +283,8 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
|
||||
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
|
||||
AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
|
||||
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLLegacy)
|
||||
if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy {
|
||||
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto)
|
||||
if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever {
|
||||
log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection)
|
||||
}
|
||||
|
||||
@@ -316,9 +313,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
Domain = urlHostname
|
||||
}
|
||||
|
||||
AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix)
|
||||
AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed)
|
||||
|
||||
AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix)
|
||||
manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL)
|
||||
ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes)
|
||||
|
||||
@@ -346,7 +341,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false)
|
||||
PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80")
|
||||
RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol)
|
||||
OfflineMode = sec.Key("OFFLINE_MODE").MustBool(true)
|
||||
if len(StaticRootPath) == 0 {
|
||||
StaticRootPath = AppWorkPath
|
||||
}
|
||||
@@ -370,6 +364,10 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: GOLANG-HTTP-TMPDIR: Some Golang packages (like "http") use os.TempDir() to create temporary files when uploading files.
|
||||
// So ideally we should set the TMPDIR environment variable to make them use our managed temp directory.
|
||||
// But there is no clear place to set it currently, for example: when running "install" page, the AppDataPath is not ready yet, then AppDataTempDir won't work
|
||||
|
||||
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
|
||||
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
|
||||
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// SessionConfig defines Session settings
|
||||
@@ -49,10 +50,8 @@ func loadSessionFrom(rootCfg ConfigProvider) {
|
||||
checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
|
||||
}
|
||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
||||
SessionConfig.CookiePath = AppSubURL
|
||||
if SessionConfig.CookiePath == "" {
|
||||
SessionConfig.CookiePath = "/"
|
||||
}
|
||||
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
|
||||
SessionConfig.CookiePath = util.IfZero(AppSubURL, "/")
|
||||
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
|
||||
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
|
||||
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// settings
|
||||
@@ -28,8 +29,10 @@ var (
|
||||
CfgProvider ConfigProvider
|
||||
IsWindows bool
|
||||
|
||||
// IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing
|
||||
// TODO: this is only a temporary solution, we should make the test code more reliable
|
||||
// IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
|
||||
// * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable)
|
||||
// * Panic in dev or testing mode to make the problem more obvious and easier to debug
|
||||
// * Mock some functions or options to make testing easier (eg: session store, time, URL detection, etc.)
|
||||
IsInTesting = false
|
||||
)
|
||||
|
||||
@@ -57,6 +60,10 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
|
||||
return currentUser, runUser == currentUser
|
||||
}
|
||||
|
||||
func IsInE2eTesting() bool {
|
||||
return os.Getenv("GITEA_TEST_E2E") == "true"
|
||||
}
|
||||
|
||||
// PrepareAppDataPath creates app data directory if necessary
|
||||
func PrepareAppDataPath() error {
|
||||
// FIXME: There are too many calls to MkdirAll in old code. It is incorrect.
|
||||
@@ -108,6 +115,9 @@ func LoadCommonSettings() {
|
||||
|
||||
// loadCommonSettingsFrom loads common configurations from a configuration provider.
|
||||
func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||
// a lot of logic depends on InstallLock value, so it must be loaded before any other settings
|
||||
InstallLock = HasInstallLock(cfg)
|
||||
|
||||
// WARNING: don't change the sequence except you know what you are doing.
|
||||
loadRunModeFrom(cfg)
|
||||
loadLogGlobalFrom(cfg)
|
||||
@@ -154,32 +164,38 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||
|
||||
func loadRunModeFrom(rootCfg ConfigProvider) {
|
||||
rootSec := rootCfg.Section("")
|
||||
mustNotRunAsRoot(rootSec)
|
||||
|
||||
runModeValue := os.Getenv("GITEA_RUN_MODE")
|
||||
runModeValue = util.IfZero(runModeValue, rootSec.Key("RUN_MODE").String())
|
||||
// non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value.
|
||||
IsProd = !strings.EqualFold(runModeValue, "dev") // TODO: can use case-sensitive comparing in the future
|
||||
RunMode = util.Iif(IsProd, "prod", "dev")
|
||||
|
||||
// there is a separate check: mustCurrentRunUserMatch (IsRunUserMatchCurrentUser)
|
||||
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
|
||||
}
|
||||
|
||||
func mustNotRunAsRoot(rootSec ConfigSection) {
|
||||
if os.Getuid() != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mustRunAsRoot := os.Getenv("SNAP") != "" && os.Getenv("SNAP_NAME") != "" // snap container runs the app as uid=0
|
||||
if mustRunAsRoot {
|
||||
return
|
||||
}
|
||||
|
||||
// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
|
||||
// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
|
||||
unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")
|
||||
unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value()
|
||||
RunMode = os.Getenv("GITEA_RUN_MODE")
|
||||
if RunMode == "" {
|
||||
RunMode = rootSec.Key("RUN_MODE").MustString("prod")
|
||||
}
|
||||
allowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") || // check gitea config
|
||||
optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() // check gitea env var
|
||||
|
||||
// non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value.
|
||||
RunMode = strings.ToLower(RunMode)
|
||||
if RunMode != "dev" {
|
||||
RunMode = "prod"
|
||||
}
|
||||
IsProd = RunMode != "dev"
|
||||
|
||||
// check if we run as root
|
||||
if os.Getuid() == 0 {
|
||||
if !unsafeAllowRunAsRoot {
|
||||
// Special thanks to VLC which inspired the wording of this messaging.
|
||||
log.Fatal("Gitea is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission")
|
||||
}
|
||||
log.Critical("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
if !allowRunAsRoot {
|
||||
// Special thanks to VLC which inspired the wording of this messaging.
|
||||
log.Fatal("Gitea is not supposed to be run as root. If you need to use privileged TCP ports please instead use `setcap` and the `cap_net_bind_service` permission.")
|
||||
}
|
||||
log.Warn("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
}
|
||||
|
||||
// HasInstallLock checks the install-lock in ConfigProvider directly, because sometimes the config file is not loaded into setting variables yet.
|
||||
@@ -192,7 +208,7 @@ func mustCurrentRunUserMatch(rootCfg ConfigProvider) {
|
||||
if HasInstallLock(rootCfg) {
|
||||
currentUser, match := IsRunUserMatchCurrentUser(RunUser)
|
||||
if !match {
|
||||
log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser)
|
||||
log.Fatal("Expect user '%s' (RUN_USER in app.ini) but current user is: %s", RunUser, currentUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,7 +239,7 @@ func LoadSettings() {
|
||||
func LoadSettingsForInstall() {
|
||||
loadDBSetting(CfgProvider)
|
||||
loadServiceFrom(CfgProvider)
|
||||
loadMailerFrom(CfgProvider)
|
||||
loadMailsFrom(CfgProvider)
|
||||
}
|
||||
|
||||
var configuredPaths = make(map[string]string)
|
||||
@@ -240,4 +256,5 @@ func PanicInDevOrTesting(msg string, a ...any) {
|
||||
if !IsProd || IsInTesting {
|
||||
panic(fmt.Sprintf(msg, a...))
|
||||
}
|
||||
log.Error(msg, a...)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
@@ -12,18 +13,26 @@ import (
|
||||
)
|
||||
|
||||
func TestMakeAbsoluteAssetURL(t *testing.T) {
|
||||
assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234", "https://localhost:2345"))
|
||||
assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234/", "https://localhost:2345"))
|
||||
assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234/", "https://localhost:2345/"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234", "/foo"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/", "/foo"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/", "/foo/"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo", "/foo"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/foo"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/foo/"))
|
||||
assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo", "/bar"))
|
||||
assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/bar"))
|
||||
assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/bar/"))
|
||||
appURL1, _ := url.Parse("https://localhost:1234")
|
||||
appURL2, _ := url.Parse("https://localhost:1234/")
|
||||
appURLSub1, _ := url.Parse("https://localhost:1234/foo")
|
||||
appURLSub2, _ := url.Parse("https://localhost:1234/foo/")
|
||||
|
||||
// static URL is an absolute URL, so should be used
|
||||
assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345"))
|
||||
assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345/"))
|
||||
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL2, "/foo"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo/"))
|
||||
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub2, "/foo"))
|
||||
assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo/"))
|
||||
|
||||
assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar"))
|
||||
assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub2, "/bar"))
|
||||
assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar/"))
|
||||
}
|
||||
|
||||
func TestMakeManifestData(t *testing.T) {
|
||||
|
||||
@@ -172,11 +172,11 @@ func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection,
|
||||
targetType := targetSec.Key("STORAGE_TYPE").String()
|
||||
if targetType == "" {
|
||||
if !IsValidStorageType(StorageType(typ)) {
|
||||
return nil, 0, fmt.Errorf("unknow storage type %q", typ)
|
||||
return nil, 0, fmt.Errorf("unknown storage type %q", typ)
|
||||
}
|
||||
targetSec.Key("STORAGE_TYPE").SetValue(typ)
|
||||
} else if !IsValidStorageType(StorageType(targetType)) {
|
||||
return nil, 0, fmt.Errorf("unknow storage type %q for section storage.%v", targetType, typ)
|
||||
return nil, 0, fmt.Errorf("unknown storage type %q for section storage.%v", targetType, typ)
|
||||
}
|
||||
|
||||
return targetSec, targetSecIsTyp, nil
|
||||
@@ -202,7 +202,7 @@ func getStorageTargetSection(rootCfg ConfigProvider, name, typ string, sec Confi
|
||||
}
|
||||
}
|
||||
|
||||
// check stoarge name thirdly
|
||||
// check storage name thirdly
|
||||
targetSec, _ := rootCfg.GetSection(storageSectionName + "." + name)
|
||||
if targetSec != nil {
|
||||
targetType := targetSec.Key("STORAGE_TYPE").String()
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
var giteaTestSourceRoot *string
|
||||
|
||||
func GetGiteaTestSourceRoot() string {
|
||||
return *giteaTestSourceRoot
|
||||
}
|
||||
|
||||
func SetupGiteaTestEnv() {
|
||||
if giteaTestSourceRoot != nil {
|
||||
return // already initialized
|
||||
}
|
||||
|
||||
IsInTesting = true
|
||||
giteaRoot := os.Getenv("GITEA_TEST_ROOT")
|
||||
if giteaRoot == "" {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename)))
|
||||
fixturesDir := filepath.Join(giteaRoot, "models", "fixtures")
|
||||
if _, err := os.Stat(fixturesDir); err != nil {
|
||||
panic("in gitea source code directory, fixtures directory not found: " + fixturesDir)
|
||||
}
|
||||
}
|
||||
|
||||
appWorkPathBuiltin = giteaRoot
|
||||
AppWorkPath = giteaRoot
|
||||
AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "")
|
||||
StaticRootPath = giteaRoot // need to load assets (options, public) from the source code directory for testing
|
||||
|
||||
// giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF"
|
||||
giteaConf := os.Getenv("GITEA_TEST_CONF")
|
||||
if giteaConf == "" {
|
||||
// By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger.
|
||||
// It's easier for developers to debug bugs step by step with a debugger.
|
||||
// Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes.
|
||||
giteaConf = "tests/sqlite.ini"
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_CONF not set - defaulting to %s\n", giteaConf)
|
||||
if !EnableSQLite3 {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
// CustomConf must be absolute path to make tests pass,
|
||||
CustomConf = filepath.Join(AppWorkPath, giteaConf)
|
||||
|
||||
// also unset unnecessary env vars for testing (only keep "GITEA_TEST_*" ones)
|
||||
UnsetUnnecessaryEnvVars()
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "GIT_") || (strings.HasPrefix(env, "GITEA_") && !strings.HasPrefix(env, "GITEA_TEST_")) {
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
_ = os.Unsetenv(k)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future
|
||||
_ = os.Setenv("GITEA_ROOT", giteaRoot)
|
||||
_ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts
|
||||
giteaTestSourceRoot = &giteaRoot
|
||||
}
|
||||
@@ -25,10 +25,10 @@ var UI = struct {
|
||||
ReactionMaxUserNum int
|
||||
MaxDisplayFileSize int64
|
||||
ShowUserEmail bool
|
||||
DefaultShowFullName bool
|
||||
DefaultTheme string
|
||||
Themes []string
|
||||
FileIconTheme string
|
||||
FolderIconTheme string
|
||||
Reactions []string
|
||||
ReactionsLookup container.Set[string] `ini:"-"`
|
||||
CustomEmojis []string
|
||||
@@ -42,6 +42,15 @@ var UI = struct {
|
||||
|
||||
AmbiguousUnicodeDetection bool
|
||||
|
||||
// TODO: DefaultShowFullName is introduced by https://github.com/go-gitea/gitea/pull/6710
|
||||
// But there are still many edge cases:
|
||||
// * Many places still use "username", not respecting this setting
|
||||
// * Many places use "Full Name" if it is not empty, cause inconsistent UI for users who have set their full name but some others don't
|
||||
// * Even if DefaultShowFullName=false, many places still need to show the full name
|
||||
// For most cases, either "username" or "username (Full Name)" should be used and are good enough.
|
||||
// Only in very few cases (e.g.: unimportant lists, narrow layout), "username" or "Full Name" can be used.
|
||||
DefaultShowFullName bool
|
||||
|
||||
Notification struct {
|
||||
MinTimeout time.Duration
|
||||
TimeoutStep time.Duration
|
||||
@@ -88,6 +97,7 @@ var UI = struct {
|
||||
MaxDisplayFileSize: 8388608,
|
||||
DefaultTheme: `gitea-auto`,
|
||||
FileIconTheme: `material`,
|
||||
FolderIconTheme: `basic`,
|
||||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
||||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
|
||||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
|
||||
|
||||
Reference in New Issue
Block a user