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