code.gitea.io/gitea@v1.22.3/modules/setting/config_provider.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package setting
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/util"
    17  
    18  	"gopkg.in/ini.v1" //nolint:depguard
    19  )
    20  
    21  type ConfigKey interface {
    22  	Name() string
    23  	Value() string
    24  	SetValue(v string)
    25  
    26  	In(defaultVal string, candidates []string) string
    27  	String() string
    28  	Strings(delim string) []string
    29  
    30  	MustString(defaultVal string) string
    31  	MustBool(defaultVal ...bool) bool
    32  	MustInt(defaultVal ...int) int
    33  	MustInt64(defaultVal ...int64) int64
    34  	MustDuration(defaultVal ...time.Duration) time.Duration
    35  }
    36  
    37  type ConfigSection interface {
    38  	Name() string
    39  	MapTo(any) error
    40  	HasKey(key string) bool
    41  	NewKey(name, value string) (ConfigKey, error)
    42  	Key(key string) ConfigKey
    43  	Keys() []ConfigKey
    44  	ChildSections() []ConfigSection
    45  }
    46  
    47  // ConfigProvider represents a config provider
    48  type ConfigProvider interface {
    49  	Section(section string) ConfigSection
    50  	Sections() []ConfigSection
    51  	NewSection(name string) (ConfigSection, error)
    52  	GetSection(name string) (ConfigSection, error)
    53  	Save() error
    54  	SaveTo(filename string) error
    55  
    56  	DisableSaving()
    57  	PrepareSaving() (ConfigProvider, error)
    58  	IsLoadedFromEmpty() bool
    59  }
    60  
    61  type iniConfigProvider struct {
    62  	file string
    63  	ini  *ini.File
    64  
    65  	disableSaving   bool // disable the "Save" method because the config options could be polluted
    66  	loadedFromEmpty bool // whether the file has not existed previously
    67  }
    68  
    69  type iniConfigSection struct {
    70  	sec *ini.Section
    71  }
    72  
    73  var (
    74  	_ ConfigProvider = (*iniConfigProvider)(nil)
    75  	_ ConfigSection  = (*iniConfigSection)(nil)
    76  	_ ConfigKey      = (*ini.Key)(nil)
    77  )
    78  
    79  // ConfigSectionKey only searches the keys in the given section, but it is O(n).
    80  // ini package has a special behavior:  with "[sec] a=1" and an empty "[sec.sub]",
    81  // then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections.
    82  // It returns nil if the key doesn't exist.
    83  func ConfigSectionKey(sec ConfigSection, key string) ConfigKey {
    84  	if sec == nil {
    85  		return nil
    86  	}
    87  	for _, k := range sec.Keys() {
    88  		if k.Name() == key {
    89  			return k
    90  		}
    91  	}
    92  	return nil
    93  }
    94  
    95  func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string {
    96  	k := ConfigSectionKey(sec, key)
    97  	if k != nil && k.String() != "" {
    98  		return k.String()
    99  	}
   100  	if len(def) > 0 {
   101  		return def[0]
   102  	}
   103  	return ""
   104  }
   105  
   106  func ConfigSectionKeyBool(sec ConfigSection, key string, def ...bool) bool {
   107  	k := ConfigSectionKey(sec, key)
   108  	if k != nil && k.String() != "" {
   109  		b, _ := strconv.ParseBool(k.String())
   110  		return b
   111  	}
   112  	if len(def) > 0 {
   113  		return def[0]
   114  	}
   115  	return false
   116  }
   117  
   118  // ConfigInheritedKey works like ini.Section.Key(), but it always returns a new key instance, it is O(n) because NewKey is O(n)
   119  // and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values.
   120  // Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys.
   121  // It never returns nil.
   122  func ConfigInheritedKey(sec ConfigSection, key string) ConfigKey {
   123  	k := sec.Key(key)
   124  	if k != nil && k.String() != "" {
   125  		newKey, _ := sec.NewKey(k.Name(), k.String())
   126  		return newKey
   127  	}
   128  	newKey, _ := sec.NewKey(key, "")
   129  	return newKey
   130  }
   131  
   132  func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) string {
   133  	k := sec.Key(key)
   134  	if k != nil && k.String() != "" {
   135  		return k.String()
   136  	}
   137  	if len(def) > 0 {
   138  		return def[0]
   139  	}
   140  	return ""
   141  }
   142  
   143  func (s *iniConfigSection) Name() string {
   144  	return s.sec.Name()
   145  }
   146  
   147  func (s *iniConfigSection) MapTo(v any) error {
   148  	return s.sec.MapTo(v)
   149  }
   150  
   151  func (s *iniConfigSection) HasKey(key string) bool {
   152  	return s.sec.HasKey(key)
   153  }
   154  
   155  func (s *iniConfigSection) NewKey(name, value string) (ConfigKey, error) {
   156  	return s.sec.NewKey(name, value)
   157  }
   158  
   159  func (s *iniConfigSection) Key(key string) ConfigKey {
   160  	return s.sec.Key(key)
   161  }
   162  
   163  func (s *iniConfigSection) Keys() (keys []ConfigKey) {
   164  	for _, k := range s.sec.Keys() {
   165  		keys = append(keys, k)
   166  	}
   167  	return keys
   168  }
   169  
   170  func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
   171  	for _, s := range s.sec.ChildSections() {
   172  		sections = append(sections, &iniConfigSection{s})
   173  	}
   174  	return sections
   175  }
   176  
   177  func configProviderLoadOptions() ini.LoadOptions {
   178  	return ini.LoadOptions{
   179  		KeyValueDelimiterOnWrite: " = ",
   180  		IgnoreContinuation:       true,
   181  	}
   182  }
   183  
   184  // NewConfigProviderFromData this function is mainly for testing purpose
   185  func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
   186  	cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent))
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	cfg.NameMapper = ini.SnackCase
   191  	return &iniConfigProvider{
   192  		ini:             cfg,
   193  		loadedFromEmpty: true,
   194  	}, nil
   195  }
   196  
   197  // NewConfigProviderFromFile load configuration from file.
   198  // NOTE: do not print any log except error.
   199  func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
   200  	cfg := ini.Empty(configProviderLoadOptions())
   201  	loadedFromEmpty := true
   202  
   203  	if file != "" {
   204  		isFile, err := util.IsFile(file)
   205  		if err != nil {
   206  			return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err)
   207  		}
   208  		if isFile {
   209  			if err = cfg.Append(file); err != nil {
   210  				return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
   211  			}
   212  			loadedFromEmpty = false
   213  		}
   214  	}
   215  
   216  	cfg.NameMapper = ini.SnackCase
   217  	return &iniConfigProvider{
   218  		file:            file,
   219  		ini:             cfg,
   220  		loadedFromEmpty: loadedFromEmpty,
   221  	}, nil
   222  }
   223  
   224  func (p *iniConfigProvider) Section(section string) ConfigSection {
   225  	return &iniConfigSection{sec: p.ini.Section(section)}
   226  }
   227  
   228  func (p *iniConfigProvider) Sections() (sections []ConfigSection) {
   229  	for _, s := range p.ini.Sections() {
   230  		sections = append(sections, &iniConfigSection{s})
   231  	}
   232  	return sections
   233  }
   234  
   235  func (p *iniConfigProvider) NewSection(name string) (ConfigSection, error) {
   236  	sec, err := p.ini.NewSection(name)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	return &iniConfigSection{sec: sec}, nil
   241  }
   242  
   243  func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
   244  	sec, err := p.ini.GetSection(name)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  	return &iniConfigSection{sec: sec}, nil
   249  }
   250  
   251  var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save")
   252  
   253  // Save saves the content into file
   254  func (p *iniConfigProvider) Save() error {
   255  	if p.disableSaving {
   256  		return errDisableSaving
   257  	}
   258  	filename := p.file
   259  	if filename == "" {
   260  		return fmt.Errorf("config file path must not be empty")
   261  	}
   262  	if p.loadedFromEmpty {
   263  		if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
   264  			return fmt.Errorf("failed to create %q: %v", filename, err)
   265  		}
   266  	}
   267  	if err := p.ini.SaveTo(filename); err != nil {
   268  		return fmt.Errorf("failed to save %q: %v", filename, err)
   269  	}
   270  
   271  	// Change permissions to be more restrictive
   272  	fi, err := os.Stat(filename)
   273  	if err != nil {
   274  		return fmt.Errorf("failed to determine current conf file permissions: %v", err)
   275  	}
   276  
   277  	if fi.Mode().Perm() > 0o600 {
   278  		if err = os.Chmod(filename, 0o600); err != nil {
   279  			log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.")
   280  		}
   281  	}
   282  	return nil
   283  }
   284  
   285  func (p *iniConfigProvider) SaveTo(filename string) error {
   286  	if p.disableSaving {
   287  		return errDisableSaving
   288  	}
   289  	return p.ini.SaveTo(filename)
   290  }
   291  
   292  // DisableSaving disables the saving function, use PrepareSaving to get clear config options.
   293  func (p *iniConfigProvider) DisableSaving() {
   294  	p.disableSaving = true
   295  }
   296  
   297  // PrepareSaving loads the ini from file again to get clear config options.
   298  // Otherwise, the "MustXxx" calls would have polluted the current config provider,
   299  // it makes the "Save" outputs a lot of garbage options
   300  // After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped.
   301  func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) {
   302  	if p.file == "" {
   303  		return nil, errors.New("no config file to save")
   304  	}
   305  	return NewConfigProviderFromFile(p.file)
   306  }
   307  
   308  func (p *iniConfigProvider) IsLoadedFromEmpty() bool {
   309  	return p.loadedFromEmpty
   310  }
   311  
   312  func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
   313  	if err := rootCfg.Section(sectionName).MapTo(setting); err != nil {
   314  		log.Fatal("Failed to map %s settings: %v", sectionName, err)
   315  	}
   316  }
   317  
   318  // StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc
   319  var StartupProblems []string
   320  
   321  func LogStartupProblem(skip int, level log.Level, format string, args ...any) {
   322  	msg := fmt.Sprintf(format, args...)
   323  	log.Log(skip+1, level, "%s", msg)
   324  	StartupProblems = append(StartupProblems, msg)
   325  }
   326  
   327  func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
   328  	if rootCfg.Section(oldSection).HasKey(oldKey) {
   329  		LogStartupProblem(1, log.ERROR, "Deprecated config option `[%s].%s` is present, please use `[%s].%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
   330  	}
   331  }
   332  
   333  // deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
   334  func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
   335  	if rootCfg.Section(oldSection).HasKey(oldKey) {
   336  		LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
   337  	}
   338  }
   339  
   340  // NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content
   341  func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) {
   342  	iniFile, err := ini.LoadSources(ini.LoadOptions{
   343  		IgnoreInlineComment:         true,
   344  		UnescapeValueCommentSymbols: true,
   345  		IgnoreContinuation:          true,
   346  	}, source, others...)
   347  	if err != nil {
   348  		return nil, fmt.Errorf("unable to load locale ini: %w", err)
   349  	}
   350  	iniFile.BlockMode = false
   351  	return &iniConfigProvider{
   352  		ini:             iniFile,
   353  		loadedFromEmpty: true,
   354  	}, nil
   355  }
   356  
   357  func init() {
   358  	ini.PrettyFormat = false
   359  }