github.com/mattermost/mattermost-server/server/v8@v8.0.0-20230610055354-a6d1d38b273d/config/utils.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package config 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "reflect" 11 "strings" 12 13 "github.com/mattermost/mattermost-server/server/public/model" 14 "github.com/mattermost/mattermost-server/server/public/shared/i18n" 15 "github.com/mattermost/mattermost-server/server/public/shared/mlog" 16 "github.com/mattermost/mattermost-server/server/v8/channels/utils" 17 ) 18 19 // marshalConfig converts the given configuration into JSON bytes for persistence. 20 func marshalConfig(cfg *model.Config) ([]byte, error) { 21 return json.MarshalIndent(cfg, "", " ") 22 } 23 24 // desanitize replaces fake settings with their actual values. 25 func desanitize(actual, target *model.Config) { 26 if target.LdapSettings.BindPassword != nil && *target.LdapSettings.BindPassword == model.FakeSetting { 27 *target.LdapSettings.BindPassword = *actual.LdapSettings.BindPassword 28 } 29 30 if *target.FileSettings.PublicLinkSalt == model.FakeSetting { 31 *target.FileSettings.PublicLinkSalt = *actual.FileSettings.PublicLinkSalt 32 } 33 if *target.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting { 34 target.FileSettings.AmazonS3SecretAccessKey = actual.FileSettings.AmazonS3SecretAccessKey 35 } 36 37 if *target.EmailSettings.SMTPPassword == model.FakeSetting { 38 target.EmailSettings.SMTPPassword = actual.EmailSettings.SMTPPassword 39 } 40 41 if *target.GitLabSettings.Secret == model.FakeSetting { 42 target.GitLabSettings.Secret = actual.GitLabSettings.Secret 43 } 44 45 if target.GoogleSettings.Secret != nil && *target.GoogleSettings.Secret == model.FakeSetting { 46 target.GoogleSettings.Secret = actual.GoogleSettings.Secret 47 } 48 49 if target.Office365Settings.Secret != nil && *target.Office365Settings.Secret == model.FakeSetting { 50 target.Office365Settings.Secret = actual.Office365Settings.Secret 51 } 52 53 if target.OpenIdSettings.Secret != nil && *target.OpenIdSettings.Secret == model.FakeSetting { 54 target.OpenIdSettings.Secret = actual.OpenIdSettings.Secret 55 } 56 57 if *target.SqlSettings.DataSource == model.FakeSetting { 58 *target.SqlSettings.DataSource = *actual.SqlSettings.DataSource 59 } 60 if *target.SqlSettings.AtRestEncryptKey == model.FakeSetting { 61 target.SqlSettings.AtRestEncryptKey = actual.SqlSettings.AtRestEncryptKey 62 } 63 64 if *target.ElasticsearchSettings.Password == model.FakeSetting { 65 *target.ElasticsearchSettings.Password = *actual.ElasticsearchSettings.Password 66 } 67 68 if len(target.SqlSettings.DataSourceReplicas) == len(actual.SqlSettings.DataSourceReplicas) { 69 for i, value := range target.SqlSettings.DataSourceReplicas { 70 if value == model.FakeSetting { 71 target.SqlSettings.DataSourceReplicas[i] = actual.SqlSettings.DataSourceReplicas[i] 72 } 73 } 74 } 75 76 if len(target.SqlSettings.DataSourceSearchReplicas) == len(actual.SqlSettings.DataSourceSearchReplicas) { 77 for i, value := range target.SqlSettings.DataSourceSearchReplicas { 78 if value == model.FakeSetting { 79 target.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i] 80 } 81 } 82 } 83 84 if *target.MessageExportSettings.GlobalRelaySettings.SMTPPassword == model.FakeSetting { 85 *target.MessageExportSettings.GlobalRelaySettings.SMTPPassword = *actual.MessageExportSettings.GlobalRelaySettings.SMTPPassword 86 } 87 88 if target.ServiceSettings.GfycatAPISecret != nil && *target.ServiceSettings.GfycatAPISecret == model.FakeSetting { 89 *target.ServiceSettings.GfycatAPISecret = *actual.ServiceSettings.GfycatAPISecret 90 } 91 92 if *target.ServiceSettings.SplitKey == model.FakeSetting { 93 *target.ServiceSettings.SplitKey = *actual.ServiceSettings.SplitKey 94 } 95 } 96 97 // fixConfig patches invalid or missing data in the configuration. 98 func fixConfig(cfg *model.Config) { 99 // Ensure SiteURL has no trailing slash. 100 if strings.HasSuffix(*cfg.ServiceSettings.SiteURL, "/") { 101 *cfg.ServiceSettings.SiteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/") 102 } 103 104 // Ensure the directory for a local file store has a trailing slash. 105 if *cfg.FileSettings.DriverName == model.ImageDriverLocal { 106 if *cfg.FileSettings.Directory != "" && !strings.HasSuffix(*cfg.FileSettings.Directory, "/") { 107 *cfg.FileSettings.Directory += "/" 108 } 109 } 110 111 FixInvalidLocales(cfg) 112 } 113 114 // FixInvalidLocales checks and corrects the given config for invalid locale-related settings. 115 // 116 // Ideally, this function would be completely internal, but it's currently exposed to allow the cli 117 // to test the config change before allowing the save. 118 func FixInvalidLocales(cfg *model.Config) bool { 119 var changed bool 120 121 locales := i18n.GetSupportedLocales() 122 if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok { 123 *cfg.LocalizationSettings.DefaultServerLocale = model.DefaultLocale 124 mlog.Warn("DefaultServerLocale must be one of the supported locales. Setting DefaultServerLocale to en as default value.") 125 changed = true 126 } 127 128 if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok { 129 *cfg.LocalizationSettings.DefaultClientLocale = model.DefaultLocale 130 mlog.Warn("DefaultClientLocale must be one of the supported locales. Setting DefaultClientLocale to en as default value.") 131 changed = true 132 } 133 134 if *cfg.LocalizationSettings.AvailableLocales != "" { 135 isDefaultClientLocaleInAvailableLocales := false 136 for _, word := range strings.Split(*cfg.LocalizationSettings.AvailableLocales, ",") { 137 if _, ok := locales[word]; !ok { 138 *cfg.LocalizationSettings.AvailableLocales = "" 139 isDefaultClientLocaleInAvailableLocales = true 140 mlog.Warn("AvailableLocales must include DefaultClientLocale. Setting AvailableLocales to all locales as default value.") 141 changed = true 142 break 143 } 144 145 if word == *cfg.LocalizationSettings.DefaultClientLocale { 146 isDefaultClientLocaleInAvailableLocales = true 147 } 148 } 149 150 availableLocales := *cfg.LocalizationSettings.AvailableLocales 151 152 if !isDefaultClientLocaleInAvailableLocales { 153 availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale 154 mlog.Warn("Adding DefaultClientLocale to AvailableLocales.") 155 changed = true 156 } 157 158 *cfg.LocalizationSettings.AvailableLocales = strings.Join(utils.RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",") 159 } 160 161 return changed 162 } 163 164 // Merge merges two configs together. The receiver's values are overwritten with the patch's 165 // values except when the patch's values are nil. 166 func Merge(cfg *model.Config, patch *model.Config, mergeConfig *utils.MergeConfig) (*model.Config, error) { 167 ret, err := utils.Merge(cfg, patch, mergeConfig) 168 if err != nil { 169 return nil, err 170 } 171 172 retCfg := ret.(model.Config) 173 return &retCfg, nil 174 } 175 176 func IsDatabaseDSN(dsn string) bool { 177 return strings.HasPrefix(dsn, "mysql://") || 178 strings.HasPrefix(dsn, "postgres://") || 179 strings.HasPrefix(dsn, "postgresql://") 180 } 181 182 func isJSONMap(data []byte) bool { 183 var m map[string]any 184 err := json.Unmarshal(data, &m) 185 return err == nil 186 } 187 188 func GetValueByPath(path []string, obj any) (any, bool) { 189 r := reflect.ValueOf(obj) 190 var val reflect.Value 191 if r.Kind() == reflect.Map { 192 val = r.MapIndex(reflect.ValueOf(path[0])) 193 if val.IsValid() { 194 val = val.Elem() 195 } 196 } else { 197 val = r.FieldByName(path[0]) 198 } 199 200 if !val.IsValid() { 201 return nil, false 202 } 203 204 switch { 205 case len(path) == 1: 206 return val.Interface(), true 207 case val.Kind() == reflect.Struct: 208 return GetValueByPath(path[1:], val.Interface()) 209 case val.Kind() == reflect.Map: 210 remainingPath := strings.Join(path[1:], ".") 211 mapIter := val.MapRange() 212 for mapIter.Next() { 213 key := mapIter.Key().String() 214 if strings.HasPrefix(remainingPath, key) { 215 i := strings.Count(key, ".") + 2 // number of dots + a dot on each side 216 mapVal := mapIter.Value() 217 // if no sub field path specified, return the object 218 if len(path[i:]) == 0 { 219 return mapVal.Interface(), true 220 } 221 data := mapVal.Interface() 222 if mapVal.Kind() == reflect.Ptr { 223 data = mapVal.Elem().Interface() // if value is a pointer, dereference it 224 } 225 // pass subpath 226 return GetValueByPath(path[i:], data) 227 } 228 } 229 } 230 return nil, false 231 } 232 233 func equal(oldCfg, newCfg *model.Config) (bool, error) { 234 oldCfgBytes, err := json.Marshal(oldCfg) 235 if err != nil { 236 return false, fmt.Errorf("failed to marshal old config: %w", err) 237 } 238 newCfgBytes, err := json.Marshal(newCfg) 239 if err != nil { 240 return false, fmt.Errorf("failed to marshal new config: %w", err) 241 } 242 return !bytes.Equal(oldCfgBytes, newCfgBytes), nil 243 }