github.com/gigforks/mattermost-server@v4.9.1-0.20180619094218-800d97fa55d0+incompatible/utils/config.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package utils 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14 "reflect" 15 "strconv" 16 "strings" 17 18 "github.com/fsnotify/fsnotify" 19 "github.com/pkg/errors" 20 "github.com/spf13/viper" 21 22 "net/http" 23 24 "github.com/mattermost/mattermost-server/einterfaces" 25 "github.com/mattermost/mattermost-server/mlog" 26 "github.com/mattermost/mattermost-server/model" 27 "github.com/mattermost/mattermost-server/utils/jsonutils" 28 ) 29 30 const ( 31 LOG_ROTATE_SIZE = 10000 32 LOG_FILENAME = "mattermost.log" 33 ) 34 35 // FindConfigFile attempts to find an existing configuration file. fileName can be an absolute or 36 // relative path or name such as "/opt/mattermost/config.json" or simply "config.json". An empty 37 // string is returned if no configuration is found. 38 func FindConfigFile(fileName string) (path string) { 39 if filepath.IsAbs(fileName) { 40 if _, err := os.Stat(fileName); err == nil { 41 return fileName 42 } 43 } else { 44 for _, dir := range []string{"./config", "../config", "../../config", "."} { 45 path, _ := filepath.Abs(filepath.Join(dir, fileName)) 46 if _, err := os.Stat(path); err == nil { 47 return path 48 } 49 } 50 } 51 return "" 52 } 53 54 // FindDir looks for the given directory in nearby ancestors, falling back to `./` if not found. 55 func FindDir(dir string) (string, bool) { 56 for _, parent := range []string{".", "..", "../.."} { 57 foundDir, err := filepath.Abs(filepath.Join(parent, dir)) 58 if err != nil { 59 continue 60 } else if _, err := os.Stat(foundDir); err == nil { 61 return foundDir, true 62 } 63 } 64 return "./", false 65 } 66 67 func MloggerConfigFromLoggerConfig(s *model.LogSettings) *mlog.LoggerConfiguration { 68 return &mlog.LoggerConfiguration{ 69 EnableConsole: s.EnableConsole, 70 ConsoleJson: *s.ConsoleJson, 71 ConsoleLevel: strings.ToLower(s.ConsoleLevel), 72 EnableFile: s.EnableFile, 73 FileJson: *s.FileJson, 74 FileLevel: strings.ToLower(s.FileLevel), 75 FileLocation: GetLogFileLocation(s.FileLocation), 76 } 77 } 78 79 // DON'T USE THIS Modify the level on the app logger 80 func DisableDebugLogForTest() { 81 mlog.GloballyDisableDebugLogForTest() 82 } 83 84 // DON'T USE THIS Modify the level on the app logger 85 func EnableDebugLogForTest() { 86 mlog.GloballyEnableDebugLogForTest() 87 } 88 89 func GetLogFileLocation(fileLocation string) string { 90 if fileLocation == "" { 91 fileLocation, _ = FindDir("logs") 92 } 93 94 return filepath.Join(fileLocation, LOG_FILENAME) 95 } 96 97 func SaveConfig(fileName string, config *model.Config) *model.AppError { 98 b, err := json.MarshalIndent(config, "", " ") 99 if err != nil { 100 return model.NewAppError("SaveConfig", "utils.config.save_config.saving.app_error", 101 map[string]interface{}{"Filename": fileName}, err.Error(), http.StatusBadRequest) 102 } 103 104 err = ioutil.WriteFile(fileName, b, 0644) 105 if err != nil { 106 return model.NewAppError("SaveConfig", "utils.config.save_config.saving.app_error", 107 map[string]interface{}{"Filename": fileName}, err.Error(), http.StatusInternalServerError) 108 } 109 110 return nil 111 } 112 113 type ConfigWatcher struct { 114 watcher *fsnotify.Watcher 115 close chan struct{} 116 closed chan struct{} 117 } 118 119 func NewConfigWatcher(cfgFileName string, f func()) (*ConfigWatcher, error) { 120 watcher, err := fsnotify.NewWatcher() 121 if err != nil { 122 return nil, errors.Wrapf(err, "failed to create config watcher for file: "+cfgFileName) 123 } 124 125 configFile := filepath.Clean(cfgFileName) 126 configDir, _ := filepath.Split(configFile) 127 watcher.Add(configDir) 128 129 ret := &ConfigWatcher{ 130 watcher: watcher, 131 close: make(chan struct{}), 132 closed: make(chan struct{}), 133 } 134 135 go func() { 136 defer close(ret.closed) 137 defer watcher.Close() 138 139 for { 140 select { 141 case event := <-watcher.Events: 142 // we only care about the config file 143 if filepath.Clean(event.Name) == configFile { 144 if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create { 145 mlog.Info(fmt.Sprintf("Config file watcher detected a change reloading %v", cfgFileName)) 146 147 if _, _, configReadErr := ReadConfigFile(cfgFileName, true); configReadErr == nil { 148 f() 149 } else { 150 mlog.Error(fmt.Sprintf("Failed to read while watching config file at %v with err=%v", cfgFileName, configReadErr.Error())) 151 } 152 } 153 } 154 case err := <-watcher.Errors: 155 mlog.Error(fmt.Sprintf("Failed while watching config file at %v with err=%v", cfgFileName, err.Error())) 156 case <-ret.close: 157 return 158 } 159 } 160 }() 161 162 return ret, nil 163 } 164 165 func (w *ConfigWatcher) Close() { 166 close(w.close) 167 <-w.closed 168 } 169 170 // ReadConfig reads and parses the given configuration. 171 func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) { 172 // Pre-flight check the syntax of the configuration file to improve error messaging. 173 configData, err := ioutil.ReadAll(r) 174 if err != nil { 175 return nil, nil, err 176 } else { 177 var rawConfig interface{} 178 if err := json.Unmarshal(configData, &rawConfig); err != nil { 179 return nil, nil, jsonutils.HumanizeJsonError(err, configData) 180 } 181 } 182 183 v := newViper(allowEnvironmentOverrides) 184 if err := v.ReadConfig(bytes.NewReader(configData)); err != nil { 185 return nil, nil, err 186 } 187 188 var config model.Config 189 unmarshalErr := v.Unmarshal(&config) 190 if unmarshalErr == nil { 191 // https://github.com/spf13/viper/issues/324 192 // https://github.com/spf13/viper/issues/348 193 config.PluginSettings = model.PluginSettings{} 194 unmarshalErr = v.UnmarshalKey("pluginsettings", &config.PluginSettings) 195 } 196 197 envConfig := v.EnvSettings() 198 199 var envErr error 200 if envConfig, envErr = fixEnvSettingsCase(envConfig); envErr != nil { 201 return nil, nil, envErr 202 } 203 204 return &config, envConfig, unmarshalErr 205 } 206 207 func newViper(allowEnvironmentOverrides bool) *viper.Viper { 208 v := viper.New() 209 210 v.SetConfigType("json") 211 212 if allowEnvironmentOverrides { 213 v.SetEnvPrefix("mm") 214 v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 215 v.AutomaticEnv() 216 } 217 218 // Set zeroed defaults for all the config settings so that Viper knows what environment variables 219 // it needs to be looking for. The correct defaults will later be applied using Config.SetDefaults. 220 defaults := getDefaultsFromStruct(model.Config{}) 221 222 for key, value := range defaults { 223 v.SetDefault(key, value) 224 } 225 226 return v 227 } 228 229 func getDefaultsFromStruct(s interface{}) map[string]interface{} { 230 return flattenStructToMap(structToMap(reflect.TypeOf(s))) 231 } 232 233 // Converts a struct type into a nested map with keys matching the struct's fields and values 234 // matching the zeroed value of the corresponding field. 235 func structToMap(t reflect.Type) (out map[string]interface{}) { 236 defer func() { 237 if r := recover(); r != nil { 238 mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r)) 239 } 240 }() 241 242 if t.Kind() != reflect.Struct { 243 // Should never hit this, but this will prevent a panic if that does happen somehow 244 return nil 245 } 246 247 out = map[string]interface{}{} 248 249 for i := 0; i < t.NumField(); i++ { 250 field := t.Field(i) 251 252 var value interface{} 253 254 switch field.Type.Kind() { 255 case reflect.Struct: 256 value = structToMap(field.Type) 257 case reflect.Ptr: 258 indirectType := field.Type.Elem() 259 260 if indirectType.Kind() == reflect.Struct { 261 // Follow pointers to structs since we need to define defaults for their fields 262 value = structToMap(indirectType) 263 } else { 264 value = nil 265 } 266 default: 267 value = reflect.Zero(field.Type).Interface() 268 } 269 270 out[field.Name] = value 271 } 272 273 return 274 } 275 276 // Flattens a nested map so that the result is a single map with keys corresponding to the 277 // path through the original map. For example, 278 // { 279 // "a": { 280 // "b": 1 281 // }, 282 // "c": "sea" 283 // } 284 // would flatten to 285 // { 286 // "a.b": 1, 287 // "c": "sea" 288 // } 289 func flattenStructToMap(in map[string]interface{}) map[string]interface{} { 290 out := make(map[string]interface{}) 291 292 for key, value := range in { 293 if valueAsMap, ok := value.(map[string]interface{}); ok { 294 sub := flattenStructToMap(valueAsMap) 295 296 for subKey, subValue := range sub { 297 out[key+"."+subKey] = subValue 298 } 299 } else { 300 out[key] = value 301 } 302 } 303 304 return out 305 } 306 307 // Fixes the case of the environment variables sent back from Viper since Viper stores 308 // everything as lower case. 309 func fixEnvSettingsCase(in map[string]interface{}) (out map[string]interface{}, err error) { 310 defer func() { 311 if r := recover(); r != nil { 312 mlog.Error(fmt.Sprintf("Panicked in fixEnvSettingsCase. This should never happen. %v", r)) 313 out = in 314 } 315 }() 316 317 var fixCase func(map[string]interface{}, reflect.Type) map[string]interface{} 318 fixCase = func(in map[string]interface{}, t reflect.Type) map[string]interface{} { 319 if t.Kind() != reflect.Struct { 320 // Should never hit this, but this will prevent a panic if that does happen somehow 321 return nil 322 } 323 324 out := make(map[string]interface{}, len(in)) 325 326 for i := 0; i < t.NumField(); i++ { 327 field := t.Field(i) 328 329 key := field.Name 330 if value, ok := in[strings.ToLower(key)]; ok { 331 if valueAsMap, ok := value.(map[string]interface{}); ok { 332 out[key] = fixCase(valueAsMap, field.Type) 333 } else { 334 out[key] = value 335 } 336 } 337 } 338 339 return out 340 } 341 342 out = fixCase(in, reflect.TypeOf(model.Config{})) 343 344 return 345 } 346 347 // ReadConfigFile reads and parses the configuration at the given file path. 348 func ReadConfigFile(path string, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) { 349 f, err := os.Open(path) 350 if err != nil { 351 return nil, nil, err 352 } 353 defer f.Close() 354 return ReadConfig(f, allowEnvironmentOverrides) 355 } 356 357 // EnsureConfigFile will attempt to locate a config file with the given name. If it does not exist, 358 // it will attempt to locate a default config file, and copy it to a file named fileName in the same 359 // directory. In either case, the config file path is returned. 360 func EnsureConfigFile(fileName string) (string, error) { 361 if configFile := FindConfigFile(fileName); configFile != "" { 362 return configFile, nil 363 } 364 if defaultPath := FindConfigFile("default.json"); defaultPath != "" { 365 destPath := filepath.Join(filepath.Dir(defaultPath), fileName) 366 src, err := os.Open(defaultPath) 367 if err != nil { 368 return "", err 369 } 370 defer src.Close() 371 dest, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 372 if err != nil { 373 return "", err 374 } 375 defer dest.Close() 376 if _, err := io.Copy(dest, src); err == nil { 377 return destPath, nil 378 } 379 } 380 return "", fmt.Errorf("no config file found") 381 } 382 383 // LoadConfig will try to search around for the corresponding config file. It will search 384 // /tmp/fileName then attempt ./config/fileName, then ../config/fileName and last it will look at 385 // fileName. 386 func LoadConfig(fileName string) (*model.Config, string, map[string]interface{}, *model.AppError) { 387 var configPath string 388 389 if fileName != filepath.Base(fileName) { 390 configPath = fileName 391 } else { 392 if path, err := EnsureConfigFile(fileName); err != nil { 393 appErr := model.NewAppError("LoadConfig", "utils.config.load_config.opening.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0) 394 return nil, "", nil, appErr 395 } else { 396 configPath = path 397 } 398 } 399 400 config, envConfig, err := ReadConfigFile(configPath, true) 401 if err != nil { 402 appErr := model.NewAppError("LoadConfig", "utils.config.load_config.decoding.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0) 403 return nil, "", nil, appErr 404 } 405 406 needSave := len(config.SqlSettings.AtRestEncryptKey) == 0 || len(*config.FileSettings.PublicLinkSalt) == 0 || 407 len(config.EmailSettings.InviteSalt) == 0 408 409 config.SetDefaults() 410 411 if err := config.IsValid(); err != nil { 412 return nil, "", nil, err 413 } 414 415 if needSave { 416 if err := SaveConfig(configPath, config); err != nil { 417 mlog.Warn(err.Error()) 418 } 419 } 420 421 if err := ValidateLocales(config); err != nil { 422 if err := SaveConfig(configPath, config); err != nil { 423 mlog.Warn(err.Error()) 424 } 425 } 426 427 if *config.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { 428 dir := config.FileSettings.Directory 429 if len(dir) > 0 && dir[len(dir)-1:] != "/" { 430 config.FileSettings.Directory += "/" 431 } 432 } 433 434 return config, configPath, envConfig, nil 435 } 436 437 func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string { 438 props := make(map[string]string) 439 440 props["Version"] = model.CurrentVersion 441 props["BuildNumber"] = model.BuildNumber 442 props["BuildDate"] = model.BuildDate 443 props["BuildHash"] = model.BuildHash 444 props["BuildHashEnterprise"] = model.BuildHashEnterprise 445 props["BuildEnterpriseReady"] = model.BuildEnterpriseReady 446 447 props["SiteURL"] = strings.TrimRight(*c.ServiceSettings.SiteURL, "/") 448 props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/") 449 props["SiteName"] = c.TeamSettings.SiteName 450 props["EnableTeamCreation"] = strconv.FormatBool(*c.TeamSettings.EnableTeamCreation) 451 props["EnableAPIv3"] = strconv.FormatBool(*c.ServiceSettings.EnableAPIv3) 452 props["EnableUserCreation"] = strconv.FormatBool(c.TeamSettings.EnableUserCreation) 453 props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer) 454 props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage 455 props["RestrictTeamInvite"] = *c.TeamSettings.RestrictTeamInvite 456 props["RestrictPublicChannelCreation"] = *c.TeamSettings.RestrictPublicChannelCreation 457 props["RestrictPrivateChannelCreation"] = *c.TeamSettings.RestrictPrivateChannelCreation 458 props["RestrictPublicChannelManagement"] = *c.TeamSettings.RestrictPublicChannelManagement 459 props["RestrictPrivateChannelManagement"] = *c.TeamSettings.RestrictPrivateChannelManagement 460 props["RestrictPublicChannelDeletion"] = *c.TeamSettings.RestrictPublicChannelDeletion 461 props["RestrictPrivateChannelDeletion"] = *c.TeamSettings.RestrictPrivateChannelDeletion 462 props["RestrictPrivateChannelManageMembers"] = *c.TeamSettings.RestrictPrivateChannelManageMembers 463 props["EnableXToLeaveChannelsFromLHS"] = strconv.FormatBool(*c.TeamSettings.EnableXToLeaveChannelsFromLHS) 464 props["TeammateNameDisplay"] = *c.TeamSettings.TeammateNameDisplay 465 props["ExperimentalPrimaryTeam"] = *c.TeamSettings.ExperimentalPrimaryTeam 466 467 props["AndroidLatestVersion"] = c.ClientRequirements.AndroidLatestVersion 468 props["AndroidMinVersion"] = c.ClientRequirements.AndroidMinVersion 469 props["DesktopLatestVersion"] = c.ClientRequirements.DesktopLatestVersion 470 props["DesktopMinVersion"] = c.ClientRequirements.DesktopMinVersion 471 props["IosLatestVersion"] = c.ClientRequirements.IosLatestVersion 472 props["IosMinVersion"] = c.ClientRequirements.IosMinVersion 473 474 props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) 475 props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey 476 props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks) 477 props["EnableOutgoingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableOutgoingWebhooks) 478 props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands) 479 props["EnableOnlyAdminIntegrations"] = strconv.FormatBool(*c.ServiceSettings.EnableOnlyAdminIntegrations) 480 props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride) 481 props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride) 482 props["EnableUserAccessTokens"] = strconv.FormatBool(*c.ServiceSettings.EnableUserAccessTokens) 483 props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews) 484 props["EnableTesting"] = strconv.FormatBool(c.ServiceSettings.EnableTesting) 485 props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper) 486 props["EnableDiagnostics"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics) 487 props["RestrictPostDelete"] = *c.ServiceSettings.RestrictPostDelete 488 props["AllowEditPost"] = *c.ServiceSettings.AllowEditPost 489 props["PostEditTimeLimit"] = fmt.Sprintf("%v", *c.ServiceSettings.PostEditTimeLimit) 490 props["CloseUnusedDirectMessages"] = strconv.FormatBool(*c.ServiceSettings.CloseUnusedDirectMessages) 491 props["EnablePreviewFeatures"] = strconv.FormatBool(*c.ServiceSettings.EnablePreviewFeatures) 492 props["EnableTutorial"] = strconv.FormatBool(*c.ServiceSettings.EnableTutorial) 493 props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages) 494 props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels 495 props["ExperimentalEnableAutomaticReplies"] = strconv.FormatBool(*c.TeamSettings.ExperimentalEnableAutomaticReplies) 496 props["ExperimentalTimezone"] = strconv.FormatBool(*c.DisplaySettings.ExperimentalTimezone) 497 498 props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications) 499 props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications) 500 props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail) 501 props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail) 502 props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername) 503 props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification) 504 props["EnableEmailBatching"] = strconv.FormatBool(*c.EmailSettings.EnableEmailBatching) 505 props["EmailNotificationContentsType"] = *c.EmailSettings.EmailNotificationContentsType 506 507 props["EmailLoginButtonColor"] = *c.EmailSettings.LoginButtonColor 508 props["EmailLoginButtonBorderColor"] = *c.EmailSettings.LoginButtonBorderColor 509 props["EmailLoginButtonTextColor"] = *c.EmailSettings.LoginButtonTextColor 510 511 props["EnableSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Enable) 512 props["EnableSignUpWithIyo"] = strconv.FormatBool(c.IyoSettings.Enable) 513 514 props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress) 515 516 props["TermsOfServiceLink"] = *c.SupportSettings.TermsOfServiceLink 517 props["PrivacyPolicyLink"] = *c.SupportSettings.PrivacyPolicyLink 518 props["AboutLink"] = *c.SupportSettings.AboutLink 519 props["HelpLink"] = *c.SupportSettings.HelpLink 520 props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink 521 props["SupportEmail"] = *c.SupportSettings.SupportEmail 522 523 props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments) 524 props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink) 525 526 props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort) 527 props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort) 528 529 props["DefaultClientLocale"] = *c.LocalizationSettings.DefaultClientLocale 530 props["AvailableLocales"] = *c.LocalizationSettings.AvailableLocales 531 props["SQLDriverName"] = *c.SqlSettings.DriverName 532 533 props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji) 534 props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker) 535 props["RestrictCustomEmojiCreation"] = *c.ServiceSettings.RestrictCustomEmojiCreation 536 props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10) 537 props["AppDownloadLink"] = *c.NativeAppSettings.AppDownloadLink 538 props["AndroidAppDownloadLink"] = *c.NativeAppSettings.AndroidAppDownloadLink 539 props["IosAppDownloadLink"] = *c.NativeAppSettings.IosAppDownloadLink 540 541 props["EnableWebrtc"] = strconv.FormatBool(*c.WebrtcSettings.Enable) 542 543 props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10) 544 props["EnableConfirmNotificationsToChannel"] = strconv.FormatBool(*c.TeamSettings.EnableConfirmNotificationsToChannel) 545 props["TimeBetweenUserTypingUpdatesMilliseconds"] = strconv.FormatInt(*c.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, 10) 546 props["EnableUserTypingMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableUserTypingMessages) 547 props["EnableChannelViewedMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableChannelViewedMessages) 548 549 props["DiagnosticId"] = diagnosticId 550 props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics) 551 552 props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable) 553 554 hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != "" 555 props["HasImageProxy"] = strconv.FormatBool(hasImageProxy) 556 557 // Set default values for all options that require a license. 558 props["ExperimentalTownSquareIsReadOnly"] = "false" 559 props["ExperimentalEnableAuthenticationTransfer"] = "true" 560 props["EnableCustomBrand"] = "false" 561 props["CustomBrandText"] = "" 562 props["CustomDescriptionText"] = "" 563 props["EnableLdap"] = "false" 564 props["LdapLoginFieldName"] = "" 565 props["LdapNicknameAttributeSet"] = "false" 566 props["LdapFirstNameAttributeSet"] = "false" 567 props["LdapLastNameAttributeSet"] = "false" 568 props["LdapLoginButtonColor"] = "" 569 props["LdapLoginButtonBorderColor"] = "" 570 props["LdapLoginButtonTextColor"] = "" 571 props["EnableMultifactorAuthentication"] = "false" 572 props["EnforceMultifactorAuthentication"] = "false" 573 props["EnableCompliance"] = "false" 574 props["EnableMobileFileDownload"] = "true" 575 props["EnableMobileFileUpload"] = "true" 576 props["EnableSaml"] = "false" 577 props["SamlLoginButtonText"] = "" 578 props["SamlFirstNameAttributeSet"] = "false" 579 props["SamlLastNameAttributeSet"] = "false" 580 props["SamlNicknameAttributeSet"] = "false" 581 props["SamlLoginButtonColor"] = "" 582 props["SamlLoginButtonBorderColor"] = "" 583 props["SamlLoginButtonTextColor"] = "" 584 props["EnableCluster"] = "false" 585 props["EnableMetrics"] = "false" 586 props["EnableSignUpWithGoogle"] = "false" 587 props["EnableSignUpWithOffice365"] = "false" 588 props["PasswordMinimumLength"] = "0" 589 props["PasswordRequireLowercase"] = "false" 590 props["PasswordRequireUppercase"] = "false" 591 props["PasswordRequireNumber"] = "false" 592 props["PasswordRequireSymbol"] = "false" 593 props["EnableBanner"] = "false" 594 props["BannerText"] = "" 595 props["BannerColor"] = "" 596 props["BannerTextColor"] = "" 597 props["AllowBannerDismissal"] = "false" 598 props["EnableThemeSelection"] = "true" 599 props["DefaultTheme"] = "" 600 props["AllowCustomThemes"] = "true" 601 props["AllowedThemes"] = "" 602 props["DataRetentionEnableMessageDeletion"] = "false" 603 props["DataRetentionMessageRetentionDays"] = "0" 604 props["DataRetentionEnableFileDeletion"] = "false" 605 props["DataRetentionFileRetentionDays"] = "0" 606 607 if license != nil { 608 props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly) 609 props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer) 610 611 if *license.Features.CustomBrand { 612 props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand) 613 props["CustomBrandText"] = *c.TeamSettings.CustomBrandText 614 props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText 615 } 616 617 if *license.Features.LDAP { 618 props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable) 619 props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName 620 props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "") 621 props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "") 622 props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "") 623 props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor 624 props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor 625 props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor 626 } 627 628 if *license.Features.MFA { 629 props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication) 630 props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication) 631 } 632 633 if *license.Features.Compliance { 634 props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable) 635 props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload) 636 props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload) 637 } 638 639 if *license.Features.SAML { 640 props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable) 641 props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText 642 props["SamlFirstNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.FirstNameAttribute != "") 643 props["SamlLastNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.LastNameAttribute != "") 644 props["SamlNicknameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.NicknameAttribute != "") 645 props["SamlLoginButtonColor"] = *c.SamlSettings.LoginButtonColor 646 props["SamlLoginButtonBorderColor"] = *c.SamlSettings.LoginButtonBorderColor 647 props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor 648 } 649 650 if *license.Features.Cluster { 651 props["EnableCluster"] = strconv.FormatBool(*c.ClusterSettings.Enable) 652 } 653 654 if *license.Features.Cluster { 655 props["EnableMetrics"] = strconv.FormatBool(*c.MetricsSettings.Enable) 656 } 657 658 if *license.Features.GoogleOAuth { 659 props["EnableSignUpWithGoogle"] = strconv.FormatBool(c.GoogleSettings.Enable) 660 } 661 662 if *license.Features.Office365OAuth { 663 props["EnableSignUpWithOffice365"] = strconv.FormatBool(c.Office365Settings.Enable) 664 } 665 666 if *license.Features.PasswordRequirements { 667 props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength) 668 props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase) 669 props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase) 670 props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number) 671 props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol) 672 } 673 674 if *license.Features.Announcement { 675 props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner) 676 props["BannerText"] = *c.AnnouncementSettings.BannerText 677 props["BannerColor"] = *c.AnnouncementSettings.BannerColor 678 props["BannerTextColor"] = *c.AnnouncementSettings.BannerTextColor 679 props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal) 680 } 681 682 if *license.Features.ThemeManagement { 683 props["EnableThemeSelection"] = strconv.FormatBool(*c.ThemeSettings.EnableThemeSelection) 684 props["DefaultTheme"] = *c.ThemeSettings.DefaultTheme 685 props["AllowCustomThemes"] = strconv.FormatBool(*c.ThemeSettings.AllowCustomThemes) 686 props["AllowedThemes"] = strings.Join(c.ThemeSettings.AllowedThemes, ",") 687 } 688 689 if *license.Features.DataRetention { 690 props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion) 691 props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10) 692 props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion) 693 props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10) 694 } 695 } 696 697 return props 698 } 699 700 func ValidateLdapFilter(cfg *model.Config, ldap einterfaces.LdapInterface) *model.AppError { 701 if *cfg.LdapSettings.Enable && ldap != nil && *cfg.LdapSettings.UserFilter != "" { 702 if err := ldap.ValidateFilter(*cfg.LdapSettings.UserFilter); err != nil { 703 return err 704 } 705 } 706 return nil 707 } 708 709 func ValidateLocales(cfg *model.Config) *model.AppError { 710 var err *model.AppError 711 locales := GetSupportedLocales() 712 if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok { 713 *cfg.LocalizationSettings.DefaultServerLocale = model.DEFAULT_LOCALE 714 err = model.NewAppError("ValidateLocales", "utils.config.supported_server_locale.app_error", nil, "", http.StatusBadRequest) 715 } 716 717 if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok { 718 *cfg.LocalizationSettings.DefaultClientLocale = model.DEFAULT_LOCALE 719 err = model.NewAppError("ValidateLocales", "utils.config.supported_client_locale.app_error", nil, "", http.StatusBadRequest) 720 } 721 722 if len(*cfg.LocalizationSettings.AvailableLocales) > 0 { 723 isDefaultClientLocaleInAvailableLocales := false 724 for _, word := range strings.Split(*cfg.LocalizationSettings.AvailableLocales, ",") { 725 if _, ok := locales[word]; !ok { 726 *cfg.LocalizationSettings.AvailableLocales = "" 727 isDefaultClientLocaleInAvailableLocales = true 728 err = model.NewAppError("ValidateLocales", "utils.config.supported_available_locales.app_error", nil, "", http.StatusBadRequest) 729 break 730 } 731 732 if word == *cfg.LocalizationSettings.DefaultClientLocale { 733 isDefaultClientLocaleInAvailableLocales = true 734 } 735 } 736 737 availableLocales := *cfg.LocalizationSettings.AvailableLocales 738 739 if !isDefaultClientLocaleInAvailableLocales { 740 availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale 741 err = model.NewAppError("ValidateLocales", "utils.config.add_client_locale.app_error", nil, "", http.StatusBadRequest) 742 } 743 744 *cfg.LocalizationSettings.AvailableLocales = strings.Join(RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",") 745 } 746 747 return err 748 }