// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package config import ( "bytes" "encoding/json" "fmt" "reflect" "strings" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/utils" ) // marshalConfig converts the given configuration into JSON bytes for persistence. func marshalConfig(cfg *model.Config) ([]byte, error) { return json.MarshalIndent(cfg, "", " ") } // Desanitize replaces fake settings with their actual values. Safe to call on // partial configs: every pointer dereference is nil-guarded so callers do not // need to run SetDefaults() first. func Desanitize(actual, target *model.Config) { if target.LdapSettings.BindPassword != nil && *target.LdapSettings.BindPassword == model.FakeSetting && actual.LdapSettings.BindPassword != nil { *target.LdapSettings.BindPassword = *actual.LdapSettings.BindPassword } if target.FileSettings.PublicLinkSalt != nil && *target.FileSettings.PublicLinkSalt == model.FakeSetting && actual.FileSettings.PublicLinkSalt != nil { *target.FileSettings.PublicLinkSalt = *actual.FileSettings.PublicLinkSalt } if target.FileSettings.AmazonS3SecretAccessKey != nil && *target.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting && actual.FileSettings.AmazonS3SecretAccessKey != nil { target.FileSettings.AmazonS3SecretAccessKey = actual.FileSettings.AmazonS3SecretAccessKey } if target.FileSettings.ExportAmazonS3SecretAccessKey != nil && *target.FileSettings.ExportAmazonS3SecretAccessKey == model.FakeSetting && actual.FileSettings.ExportAmazonS3SecretAccessKey != nil { target.FileSettings.ExportAmazonS3SecretAccessKey = actual.FileSettings.ExportAmazonS3SecretAccessKey } if target.FileSettings.AzureAccessKey != nil && *target.FileSettings.AzureAccessKey == model.FakeSetting && actual.FileSettings.AzureAccessKey != nil { target.FileSettings.AzureAccessKey = actual.FileSettings.AzureAccessKey } if target.FileSettings.ExportAzureAccessKey != nil && *target.FileSettings.ExportAzureAccessKey == model.FakeSetting && actual.FileSettings.ExportAzureAccessKey != nil { target.FileSettings.ExportAzureAccessKey = actual.FileSettings.ExportAzureAccessKey } if target.EmailSettings.SMTPPassword != nil && *target.EmailSettings.SMTPPassword == model.FakeSetting { target.EmailSettings.SMTPPassword = actual.EmailSettings.SMTPPassword } if target.GitLabSettings.Secret != nil && *target.GitLabSettings.Secret == model.FakeSetting { target.GitLabSettings.Secret = actual.GitLabSettings.Secret } if target.GoogleSettings.Secret != nil && *target.GoogleSettings.Secret == model.FakeSetting { target.GoogleSettings.Secret = actual.GoogleSettings.Secret } if target.Office365Settings.Secret != nil && *target.Office365Settings.Secret == model.FakeSetting { target.Office365Settings.Secret = actual.Office365Settings.Secret } if target.OpenIdSettings.Secret != nil && *target.OpenIdSettings.Secret == model.FakeSetting { target.OpenIdSettings.Secret = actual.OpenIdSettings.Secret } if target.SqlSettings.DataSource != nil && *target.SqlSettings.DataSource == model.FakeSetting && actual.SqlSettings.DataSource != nil { *target.SqlSettings.DataSource = *actual.SqlSettings.DataSource } if target.SqlSettings.AtRestEncryptKey != nil && *target.SqlSettings.AtRestEncryptKey == model.FakeSetting { target.SqlSettings.AtRestEncryptKey = actual.SqlSettings.AtRestEncryptKey } if target.ElasticsearchSettings.Password != nil && *target.ElasticsearchSettings.Password == model.FakeSetting && actual.ElasticsearchSettings.Password != nil { *target.ElasticsearchSettings.Password = *actual.ElasticsearchSettings.Password } if len(target.SqlSettings.DataSourceReplicas) == len(actual.SqlSettings.DataSourceReplicas) { for i, value := range target.SqlSettings.DataSourceReplicas { if value == model.FakeSetting { target.SqlSettings.DataSourceReplicas[i] = actual.SqlSettings.DataSourceReplicas[i] } } } if len(target.SqlSettings.DataSourceSearchReplicas) == len(actual.SqlSettings.DataSourceSearchReplicas) { for i, value := range target.SqlSettings.DataSourceSearchReplicas { if value == model.FakeSetting { target.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i] } } } if target.MessageExportSettings.GlobalRelaySettings != nil && target.MessageExportSettings.GlobalRelaySettings.SMTPPassword != nil && *target.MessageExportSettings.GlobalRelaySettings.SMTPPassword == model.FakeSetting && actual.MessageExportSettings.GlobalRelaySettings != nil && actual.MessageExportSettings.GlobalRelaySettings.SMTPPassword != nil { *target.MessageExportSettings.GlobalRelaySettings.SMTPPassword = *actual.MessageExportSettings.GlobalRelaySettings.SMTPPassword } if target.ServiceSettings.SplitKey != nil && *target.ServiceSettings.SplitKey == model.FakeSetting && actual.ServiceSettings.SplitKey != nil { *target.ServiceSettings.SplitKey = *actual.ServiceSettings.SplitKey } if target.ServiceSettings.GoogleDeveloperKey != nil && *target.ServiceSettings.GoogleDeveloperKey == model.FakeSetting { target.ServiceSettings.GoogleDeveloperKey = actual.ServiceSettings.GoogleDeveloperKey } if target.ServiceSettings.GiphySdkKey != nil && *target.ServiceSettings.GiphySdkKey == model.FakeSetting { target.ServiceSettings.GiphySdkKey = actual.ServiceSettings.GiphySdkKey } if target.CacheSettings.RedisPassword != nil && *target.CacheSettings.RedisPassword == model.FakeSetting { target.CacheSettings.RedisPassword = actual.CacheSettings.RedisPassword } if target.AutoTranslationSettings.LibreTranslate != nil && target.AutoTranslationSettings.LibreTranslate.APIKey != nil && *target.AutoTranslationSettings.LibreTranslate.APIKey == model.FakeSetting { target.AutoTranslationSettings.LibreTranslate.APIKey = actual.AutoTranslationSettings.LibreTranslate.APIKey } for id, settings := range target.PluginSettings.Plugins { for k, v := range settings { if v == model.FakeSetting { settings[k] = actual.PluginSettings.Plugins[id][k] } } } } // fixConfig patches invalid or missing data in the configuration. func fixConfig(cfg *model.Config) { // Ensure SiteURL has no trailing slash. if strings.HasSuffix(*cfg.ServiceSettings.SiteURL, "/") { *cfg.ServiceSettings.SiteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/") } // Ensure the directory for a local file store has a trailing slash. if *cfg.FileSettings.DriverName == model.ImageDriverLocal { if *cfg.FileSettings.Directory != "" && !strings.HasSuffix(*cfg.FileSettings.Directory, "/") { *cfg.FileSettings.Directory += "/" } } fixInvalidLocales(cfg) } // fixInvalidLocales checks and corrects the given config for invalid locale-related settings. func fixInvalidLocales(cfg *model.Config) bool { var changed bool locales := i18n.GetSupportedLocales() if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok { mlog.Warn("DefaultServerLocale must be one of the supported locales. Setting DefaultServerLocale to en as default value.", mlog.String("locale", *cfg.LocalizationSettings.DefaultServerLocale)) *cfg.LocalizationSettings.DefaultServerLocale = model.DefaultLocale changed = true } if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok { mlog.Warn("DefaultClientLocale must be one of the supported locales. Setting DefaultClientLocale to en as default value.", mlog.String("locale", *cfg.LocalizationSettings.DefaultClientLocale)) *cfg.LocalizationSettings.DefaultClientLocale = model.DefaultLocale changed = true } if *cfg.LocalizationSettings.AvailableLocales != "" { isDefaultClientLocaleInAvailableLocales := false for word := range strings.SplitSeq(*cfg.LocalizationSettings.AvailableLocales, ",") { if _, ok := locales[word]; !ok { *cfg.LocalizationSettings.AvailableLocales = "" isDefaultClientLocaleInAvailableLocales = true mlog.Warn("AvailableLocales must include DefaultClientLocale. Setting AvailableLocales to all locales as default value.") changed = true break } if word == *cfg.LocalizationSettings.DefaultClientLocale { isDefaultClientLocaleInAvailableLocales = true } } availableLocales := *cfg.LocalizationSettings.AvailableLocales if !isDefaultClientLocaleInAvailableLocales { availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale mlog.Warn("Adding DefaultClientLocale to AvailableLocales.") changed = true } *cfg.LocalizationSettings.AvailableLocales = strings.Join(utils.RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",") } return changed } // Merge merges two configs together. The receiver's values are overwritten with the patch's // values except when the patch's values are nil. func Merge(cfg *model.Config, patch *model.Config, mergeConfig *utils.MergeConfig) (*model.Config, error) { return utils.Merge(cfg, patch, mergeConfig) } func IsDatabaseDSN(dsn string) bool { return strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") } func isJSONMap(data []byte) bool { var m map[string]any err := json.Unmarshal(data, &m) return err == nil } func GetValueByPath(path []string, obj any) (any, bool) { r := reflect.ValueOf(obj) var val reflect.Value if r.Kind() == reflect.Map { val = r.MapIndex(reflect.ValueOf(path[0])) if val.IsValid() { val = val.Elem() } } else { val = r.FieldByName(path[0]) } if !val.IsValid() { return nil, false } switch { case len(path) == 1: return val.Interface(), true case val.Kind() == reflect.Struct: return GetValueByPath(path[1:], val.Interface()) case val.Kind() == reflect.Map: remainingPath := strings.Join(path[1:], ".") mapIter := val.MapRange() for mapIter.Next() { key := mapIter.Key().String() if strings.HasPrefix(remainingPath, key) { i := strings.Count(key, ".") + 2 // number of dots + a dot on each side mapVal := mapIter.Value() // if no sub field path specified, return the object if len(path[i:]) == 0 { return mapVal.Interface(), true } data := mapVal.Interface() if mapVal.Kind() == reflect.Pointer { data = mapVal.Elem().Interface() // if value is a pointer, dereference it } // pass subpath return GetValueByPath(path[i:], data) } } } return nil, false } func equal(oldCfg, newCfg *model.Config) (bool, error) { oldCfgBytes, err := json.Marshal(oldCfg) if err != nil { return false, fmt.Errorf("failed to marshal old config: %w", err) } newCfgBytes, err := json.Marshal(newCfg) if err != nil { return false, fmt.Errorf("failed to marshal new config: %w", err) } return !bytes.Equal(oldCfgBytes, newCfgBytes), nil }