summaryrefslogtreecommitdiffstats
path: root/modules/setting/config_provider.go
blob: 12cf36aa59d0e286987442acf83e54953157481f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/util"

	"gopkg.in/ini.v1" //nolint:depguard
)

type ConfigKey interface {
	Name() string
	Value() string
	SetValue(v string)

	In(defaultVal string, candidates []string) string
	String() string
	Strings(delim string) []string

	MustString(defaultVal string) string
	MustBool(defaultVal ...bool) bool
	MustInt(defaultVal ...int) int
	MustInt64(defaultVal ...int64) int64
	MustDuration(defaultVal ...time.Duration) time.Duration
}

type ConfigSection interface {
	Name() string
	MapTo(any) error
	HasKey(key string) bool
	NewKey(name, value string) (ConfigKey, error)
	Key(key string) ConfigKey
	Keys() []ConfigKey
	ChildSections() []ConfigSection
}

// ConfigProvider represents a config provider
type ConfigProvider interface {
	Section(section string) ConfigSection
	Sections() []ConfigSection
	NewSection(name string) (ConfigSection, error)
	GetSection(name string) (ConfigSection, error)
	Save() error
	SaveTo(filename string) error

	GetFile() string
	DisableSaving()
	PrepareSaving() (ConfigProvider, error)
	IsLoadedFromEmpty() bool
}

type iniConfigProvider struct {
	file string
	ini  *ini.File

	disableSaving   bool // disable the "Save" method because the config options could be polluted
	loadedFromEmpty bool // whether the file has not existed previously
}

type iniConfigSection struct {
	sec *ini.Section
}

var (
	_ ConfigProvider = (*iniConfigProvider)(nil)
	_ ConfigSection  = (*iniConfigSection)(nil)
	_ ConfigKey      = (*ini.Key)(nil)
)

// ConfigSectionKey only searches the keys in the given section, but it is O(n).
// ini package has a special behavior:  with "[sec] a=1" and an empty "[sec.sub]",
// then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections.
// It returns nil if the key doesn't exist.
func ConfigSectionKey(sec ConfigSection, key string) ConfigKey {
	if sec == nil {
		return nil
	}
	for _, k := range sec.Keys() {
		if k.Name() == key {
			return k
		}
	}
	return nil
}

func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string {
	k := ConfigSectionKey(sec, key)
	if k != nil && k.String() != "" {
		return k.String()
	}
	if len(def) > 0 {
		return def[0]
	}
	return ""
}

func ConfigSectionKeyBool(sec ConfigSection, key string, def ...bool) bool {
	k := ConfigSectionKey(sec, key)
	if k != nil && k.String() != "" {
		b, _ := strconv.ParseBool(k.String())
		return b
	}
	if len(def) > 0 {
		return def[0]
	}
	return false
}

// ConfigInheritedKey works like ini.Section.Key(), but it always returns a new key instance, it is O(n) because NewKey is O(n)
// and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values.
// Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys.
// It never returns nil.
func ConfigInheritedKey(sec ConfigSection, key string) ConfigKey {
	k := sec.Key(key)
	if k != nil && k.String() != "" {
		newKey, _ := sec.NewKey(k.Name(), k.String())
		return newKey
	}
	newKey, _ := sec.NewKey(key, "")
	return newKey
}

func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) string {
	k := sec.Key(key)
	if k != nil && k.String() != "" {
		return k.String()
	}
	if len(def) > 0 {
		return def[0]
	}
	return ""
}

func (s *iniConfigSection) Name() string {
	return s.sec.Name()
}

func (s *iniConfigSection) MapTo(v any) error {
	return s.sec.MapTo(v)
}

func (s *iniConfigSection) HasKey(key string) bool {
	return s.sec.HasKey(key)
}

func (s *iniConfigSection) NewKey(name, value string) (ConfigKey, error) {
	return s.sec.NewKey(name, value)
}

func (s *iniConfigSection) Key(key string) ConfigKey {
	return s.sec.Key(key)
}

func (s *iniConfigSection) Keys() (keys []ConfigKey) {
	for _, k := range s.sec.Keys() {
		keys = append(keys, k)
	}
	return keys
}

func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
	for _, s := range s.sec.ChildSections() {
		sections = append(sections, &iniConfigSection{s})
	}
	return sections
}

func configProviderLoadOptions() ini.LoadOptions {
	return ini.LoadOptions{
		KeyValueDelimiterOnWrite: " = ",
		IgnoreContinuation:       true,
	}
}

// NewConfigProviderFromData this function is mainly for testing purpose
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
	cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent))
	if err != nil {
		return nil, err
	}
	cfg.NameMapper = ini.SnackCase
	return &iniConfigProvider{
		ini:             cfg,
		loadedFromEmpty: true,
	}, nil
}

// NewConfigProviderFromFile load configuration from file.
// NOTE: do not print any log except error.
func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
	cfg := ini.Empty(configProviderLoadOptions())
	loadedFromEmpty := true

	if file != "" {
		isFile, err := util.IsFile(file)
		if err != nil {
			return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err)
		}
		if isFile {
			if err = cfg.Append(file); err != nil {
				return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
			}
			loadedFromEmpty = false
		}
	}

	cfg.NameMapper = ini.SnackCase
	return &iniConfigProvider{
		file:            file,
		ini:             cfg,
		loadedFromEmpty: loadedFromEmpty,
	}, nil
}

func (p *iniConfigProvider) Section(section string) ConfigSection {
	return &iniConfigSection{sec: p.ini.Section(section)}
}

func (p *iniConfigProvider) Sections() (sections []ConfigSection) {
	for _, s := range p.ini.Sections() {
		sections = append(sections, &iniConfigSection{s})
	}
	return sections
}

func (p *iniConfigProvider) NewSection(name string) (ConfigSection, error) {
	sec, err := p.ini.NewSection(name)
	if err != nil {
		return nil, err
	}
	return &iniConfigSection{sec: sec}, nil
}

func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
	sec, err := p.ini.GetSection(name)
	if err != nil {
		return nil, err
	}
	return &iniConfigSection{sec: sec}, nil
}

var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save")

func (p *iniConfigProvider) GetFile() string {
	return p.file
}

// Save saves the content into file
func (p *iniConfigProvider) Save() error {
	if p.disableSaving {
		return errDisableSaving
	}
	filename := p.file
	if filename == "" {
		return fmt.Errorf("config file path must not be empty")
	}
	if p.loadedFromEmpty {
		if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
			return fmt.Errorf("failed to create %q: %v", filename, err)
		}
	}
	if err := p.ini.SaveTo(filename); err != nil {
		return fmt.Errorf("failed to save %q: %v", filename, err)
	}

	// Change permissions to be more restrictive
	fi, err := os.Stat(filename)
	if err != nil {
		return fmt.Errorf("failed to determine current conf file permissions: %v", err)
	}

	if fi.Mode().Perm() > 0o600 {
		if err = os.Chmod(filename, 0o600); err != nil {
			log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.")
		}
	}
	return nil
}

func (p *iniConfigProvider) SaveTo(filename string) error {
	if p.disableSaving {
		return errDisableSaving
	}
	return p.ini.SaveTo(filename)
}

// DisableSaving disables the saving function, use PrepareSaving to get clear config options.
func (p *iniConfigProvider) DisableSaving() {
	p.disableSaving = true
}

// PrepareSaving loads the ini from file again to get clear config options.
// Otherwise, the "MustXxx" calls would have polluted the current config provider,
// it makes the "Save" outputs a lot of garbage options
// After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped.
func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) {
	if p.file == "" {
		return nil, errors.New("no config file to save")
	}
	return NewConfigProviderFromFile(p.file)
}

func (p *iniConfigProvider) IsLoadedFromEmpty() bool {
	return p.loadedFromEmpty
}

func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
	if err := rootCfg.Section(sectionName).MapTo(setting); err != nil {
		log.Fatal("Failed to map %s settings: %v", sectionName, err)
	}
}

// DeprecatedWarnings contains the warning message for various deprecations, including: setting option, file/folder, etc
var DeprecatedWarnings []string

func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
	if rootCfg.Section(oldSection).HasKey(oldKey) {
		msg := fmt.Sprintf("Deprecated config option `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
		log.Error("%v", msg)
		DeprecatedWarnings = append(DeprecatedWarnings, msg)
	}
}

// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
	if rootCfg.Section(oldSection).HasKey(oldKey) {
		log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey)
	}
}

// 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
}