hunk.code_window: 		if err != nil {
   			mlog.Err(err)
   		} else {
   			a.Srv.clientConfig["CustomTermsOfServiceId"] = termsOfService.Id
   		}
   	}
  
hunk.labels: keep keep keep add keep keep keep
hunk.after_edit: 			a.Srv.limitedClientConfig["CustomTermsOfServiceId"] = termsOfService.Id
  
hunk.file_path: app/config.go
hunk.type: add
file: // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
  // See License.txt for license information.
  
  package utils
  
  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"io"
  	"io/ioutil"
  	"os"
  	"path/filepath"
  	"reflect"
  	"strconv"
  	"strings"
  
  	"github.com/fsnotify/fsnotify"
  	"github.com/mattermost/viper"
  	"github.com/pkg/errors"
  
  	"net/http"
  
  	"github.com/mattermost/mattermost-server/einterfaces"
  	"github.com/mattermost/mattermost-server/mlog"
  	"github.com/mattermost/mattermost-server/model"
  	"github.com/mattermost/mattermost-server/utils/jsonutils"
  )
  
  const (
  	LOG_ROTATE_SIZE = 10000
  	LOG_FILENAME    = "mattermost.log"
  )
  
  var (
  	commonBaseSearchPaths = []string{
  		".",
  		"..",
  		"../..",
  		"../../..",
  	}
  
  	termsOfServiceEnabledAndEmpty = model.NewAppError(
  		"Config.IsValid",
  		"model.config.is_valid.support.custom_terms_of_service_text.app_error",
  		nil,
  		"",
  		http.StatusBadRequest,
  	)
  )
  
  func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bool) string {
  	if filepath.IsAbs(path) {
  		if _, err := os.Stat(path); err == nil {
  			return path
  		}
  
  		return ""
  	}
  
  	searchPaths := []string{}
  	searchPaths = append(searchPaths, baseSearchPaths...)
  
  	// Additionally attempt to search relative to the location of the running binary.
  	var binaryDir string
  	if exe, err := os.Executable(); err == nil {
  		if exe, err = filepath.EvalSymlinks(exe); err == nil {
  			if exe, err = filepath.Abs(exe); err == nil {
  				binaryDir = filepath.Dir(exe)
  			}
  		}
  	}
  	if binaryDir != "" {
  		for _, baseSearchPath := range baseSearchPaths {
  			searchPaths = append(
  				searchPaths,
  				filepath.Join(binaryDir, baseSearchPath),
  			)
  		}
  	}
  
  	for _, parent := range searchPaths {
  		found, err := filepath.Abs(filepath.Join(parent, path))
  		if err != nil {
  			continue
  		} else if fileInfo, err := os.Stat(found); err == nil {
  			if filter != nil {
  				if filter(fileInfo) {
  					return found
  				}
  			} else {
  				return found
  			}
  		}
  	}
  
  	return ""
  }
  
  // FindConfigFile attempts to find an existing configuration file. fileName can be an absolute or
  // relative path or name such as "/opt/mattermost/config.json" or simply "config.json". An empty
  // string is returned if no configuration is found.
  func FindConfigFile(fileName string) (path string) {
  	found := FindFile(filepath.Join("config", fileName))
  	if found == "" {
  		found = FindPath(fileName, []string{"."}, nil)
  	}
  
  	return found
  }
  
  // FindFile looks for the given file in nearby ancestors relative to the current working
  // directory as well as the directory of the executable.
  func FindFile(path string) string {
  	return FindPath(path, commonBaseSearchPaths, func(fileInfo os.FileInfo) bool {
  		return !fileInfo.IsDir()
  	})
  }
  
  // FindDir looks for the given directory in nearby ancestors relative to the current working
  // directory as well as the directory of the executable, falling back to `./` if not found.
  func FindDir(dir string) (string, bool) {
  	found := FindPath(dir, commonBaseSearchPaths, func(fileInfo os.FileInfo) bool {
  		return fileInfo.IsDir()
  	})
  	if found == "" {
  		return "./", false
  	}
  
  	return found, true
  }
  
  func MloggerConfigFromLoggerConfig(s *model.LogSettings) *mlog.LoggerConfiguration {
  	return &mlog.LoggerConfiguration{
  		EnableConsole: s.EnableConsole,
  		ConsoleJson:   *s.ConsoleJson,
  		ConsoleLevel:  strings.ToLower(s.ConsoleLevel),
  		EnableFile:    s.EnableFile,
  		FileJson:      *s.FileJson,
  		FileLevel:     strings.ToLower(s.FileLevel),
  		FileLocation:  GetLogFileLocation(s.FileLocation),
  	}
  }
  
  // DON'T USE THIS Modify the level on the app logger
  func DisableDebugLogForTest() {
  	mlog.GloballyDisableDebugLogForTest()
  }
  
  // DON'T USE THIS Modify the level on the app logger
  func EnableDebugLogForTest() {
  	mlog.GloballyEnableDebugLogForTest()
  }
  
  func GetLogFileLocation(fileLocation string) string {
  	if fileLocation == "" {
  		fileLocation, _ = FindDir("logs")
  	}
  
  	return filepath.Join(fileLocation, LOG_FILENAME)
  }
  
  func SaveConfig(fileName string, config *model.Config) *model.AppError {
  	b, err := json.MarshalIndent(config, "", "    ")
  	if err != nil {
  		return model.NewAppError("SaveConfig", "utils.config.save_config.saving.app_error",
  			map[string]interface{}{"Filename": fileName}, err.Error(), http.StatusBadRequest)
  	}
  
  	err = ioutil.WriteFile(fileName, b, 0644)
  	if err != nil {
  		return model.NewAppError("SaveConfig", "utils.config.save_config.saving.app_error",
  			map[string]interface{}{"Filename": fileName}, err.Error(), http.StatusInternalServerError)
  	}
  
  	return nil
  }
  
  type ConfigWatcher struct {
  	watcher *fsnotify.Watcher
  	close   chan struct{}
  	closed  chan struct{}
  }
  
  func NewConfigWatcher(cfgFileName string, f func()) (*ConfigWatcher, error) {
  	watcher, err := fsnotify.NewWatcher()
  	if err != nil {
  		return nil, errors.Wrapf(err, "failed to create config watcher for file: "+cfgFileName)
  	}
  
  	configFile := filepath.Clean(cfgFileName)
  	configDir, _ := filepath.Split(configFile)
  	watcher.Add(configDir)
  
  	ret := &ConfigWatcher{
  		watcher: watcher,
  		close:   make(chan struct{}),
  		closed:  make(chan struct{}),
  	}
  
  	go func() {
  		defer close(ret.closed)
  		defer watcher.Close()
  
  		for {
  			select {
  			case event := <-watcher.Events:
  				// we only care about the config file
  				if filepath.Clean(event.Name) == configFile {
  					if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
  						mlog.Info(fmt.Sprintf("Config file watcher detected a change reloading %v", cfgFileName))
  
  						if _, _, configReadErr := ReadConfigFile(cfgFileName, true); configReadErr == nil {
  							f()
  						} else {
  							mlog.Error(fmt.Sprintf("Failed to read while watching config file at %v with err=%v", cfgFileName, configReadErr.Error()))
  						}
  					}
  				}
  			case err := <-watcher.Errors:
  				mlog.Error(fmt.Sprintf("Failed while watching config file at %v with err=%v", cfgFileName, err.Error()))
  			case <-ret.close:
  				return
  			}
  		}
  	}()
  
  	return ret, nil
  }
  
  func (w *ConfigWatcher) Close() {
  	close(w.close)
  	<-w.closed
  }
  
  // ReadConfig reads and parses the given configuration.
  func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
  	// Pre-flight check the syntax of the configuration file to improve error messaging.
  	configData, err := ioutil.ReadAll(r)
  	if err != nil {
  		return nil, nil, err
  	} else {
  		var rawConfig interface{}
  		if err := json.Unmarshal(configData, &rawConfig); err != nil {
  			return nil, nil, jsonutils.HumanizeJsonError(err, configData)
  		}
  	}
  
  	v := newViper(allowEnvironmentOverrides)
  	if err := v.ReadConfig(bytes.NewReader(configData)); err != nil {
  		return nil, nil, err
  	}
  
  	var config model.Config
  	unmarshalErr := v.Unmarshal(&config)
  	// https://github.com/spf13/viper/issues/324
  	// https://github.com/spf13/viper/issues/348
  	if unmarshalErr == nil {
  		config.PluginSettings.Plugins = make(map[string]map[string]interface{})
  		unmarshalErr = v.UnmarshalKey("pluginsettings.plugins", &config.PluginSettings.Plugins)
  	}
  	if unmarshalErr == nil {
  		config.PluginSettings.PluginStates = make(map[string]*model.PluginState)
  		unmarshalErr = v.UnmarshalKey("pluginsettings.pluginstates", &config.PluginSettings.PluginStates)
  	}
  
  	envConfig := v.EnvSettings()
  
  	var envErr error
  	if envConfig, envErr = fixEnvSettingsCase(envConfig); envErr != nil {
  		return nil, nil, envErr
  	}
  
  	return &config, envConfig, unmarshalErr
  }
  
  func newViper(allowEnvironmentOverrides bool) *viper.Viper {
  	v := viper.New()
  
  	v.SetConfigType("json")
  
  	if allowEnvironmentOverrides {
  		v.SetEnvPrefix("mm")
  		v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
  		v.AutomaticEnv()
  	}
  
  	// Set zeroed defaults for all the config settings so that Viper knows what environment variables
  	// it needs to be looking for. The correct defaults will later be applied using Config.SetDefaults.
  	defaults := getDefaultsFromStruct(model.Config{})
  
  	for key, value := range defaults {
  		if key == "PluginSettings.Plugins" || key == "PluginSettings.PluginStates" {
  			continue
  		}
  
  		v.SetDefault(key, value)
  	}
  
  	return v
  }
  
  func getDefaultsFromStruct(s interface{}) map[string]interface{} {
  	return flattenStructToMap(structToMap(reflect.TypeOf(s)))
  }
  
  // Converts a struct type into a nested map with keys matching the struct's fields and values
  // matching the zeroed value of the corresponding field.
  func structToMap(t reflect.Type) (out map[string]interface{}) {
  	defer func() {
  		if r := recover(); r != nil {
  			mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r))
  		}
  	}()
  
  	if t.Kind() != reflect.Struct {
  		// Should never hit this, but this will prevent a panic if that does happen somehow
  		return nil
  	}
  
  	out = map[string]interface{}{}
  
  	for i := 0; i < t.NumField(); i++ {
  		field := t.Field(i)
  
  		var value interface{}
  
  		switch field.Type.Kind() {
  		case reflect.Struct:
  			value = structToMap(field.Type)
  		case reflect.Ptr:
  			indirectType := field.Type.Elem()
  
  			if indirectType.Kind() == reflect.Struct {
  				// Follow pointers to structs since we need to define defaults for their fields
  				value = structToMap(indirectType)
  			} else {
  				value = nil
  			}
  		default:
  			value = reflect.Zero(field.Type).Interface()
  		}
  
  		out[field.Name] = value
  	}
  
  	return
  }
  
  // Flattens a nested map so that the result is a single map with keys corresponding to the
  // path through the original map. For example,
  // {
  //     "a": {
  //         "b": 1
  //     },
  //     "c": "sea"
  // }
  // would flatten to
  // {
  //     "a.b": 1,
  //     "c": "sea"
  // }
  func flattenStructToMap(in map[string]interface{}) map[string]interface{} {
  	out := make(map[string]interface{})
  
  	for key, value := range in {
  		if valueAsMap, ok := value.(map[string]interface{}); ok {
  			sub := flattenStructToMap(valueAsMap)
  
  			for subKey, subValue := range sub {
  				out[key+"."+subKey] = subValue
  			}
  		} else {
  			out[key] = value
  		}
  	}
  
  	return out
  }
  
  // Fixes the case of the environment variables sent back from Viper since Viper stores
  // everything as lower case.
  func fixEnvSettingsCase(in map[string]interface{}) (out map[string]interface{}, err error) {
  	defer func() {
  		if r := recover(); r != nil {
  			mlog.Error(fmt.Sprintf("Panicked in fixEnvSettingsCase. This should never happen. %v", r))
  			out = in
  		}
  	}()
  
  	var fixCase func(map[string]interface{}, reflect.Type) map[string]interface{}
  	fixCase = func(in map[string]interface{}, t reflect.Type) map[string]interface{} {
  		if t.Kind() != reflect.Struct {
  			// Should never hit this, but this will prevent a panic if that does happen somehow
  			return nil
  		}
  
  		out := make(map[string]interface{}, len(in))
  
  		for i := 0; i < t.NumField(); i++ {
  			field := t.Field(i)
  
  			key := field.Name
  			if value, ok := in[strings.ToLower(key)]; ok {
  				if valueAsMap, ok := value.(map[string]interface{}); ok {
  					out[key] = fixCase(valueAsMap, field.Type)
  				} else {
  					out[key] = value
  				}
  			}
  		}
  
  		return out
  	}
  
  	out = fixCase(in, reflect.TypeOf(model.Config{}))
  
  	return
  }
  
  // ReadConfigFile reads and parses the configuration at the given file path.
  func ReadConfigFile(path string, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
  	f, err := os.Open(path)
  	if err != nil {
  		return nil, nil, err
  	}
  	defer f.Close()
  	return ReadConfig(f, allowEnvironmentOverrides)
  }
  
  // EnsureConfigFile will attempt to locate a config file with the given name. If it does not exist,
  // it will attempt to locate a default config file, and copy it to a file named fileName in the same
  // directory. In either case, the config file path is returned.
  func EnsureConfigFile(fileName string) (string, error) {
  	if configFile := FindConfigFile(fileName); configFile != "" {
  		return configFile, nil
  	}
  	if defaultPath := FindConfigFile("default.json"); defaultPath != "" {
  		destPath := filepath.Join(filepath.Dir(defaultPath), fileName)
  		src, err := os.Open(defaultPath)
  		if err != nil {
  			return "", err
  		}
  		defer src.Close()
  		dest, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
  		if err != nil {
  			return "", err
  		}
  		defer dest.Close()
  		if _, err := io.Copy(dest, src); err == nil {
  			return destPath, nil
  		}
  	}
  	return "", fmt.Errorf("no config file found")
  }
  
  // LoadConfig will try to search around for the corresponding config file.  It will search
  // /tmp/fileName then attempt ./config/fileName, then ../config/fileName and last it will look at
  // fileName.
  func LoadConfig(fileName string) (*model.Config, string, map[string]interface{}, *model.AppError) {
  	var configPath string
  
  	if fileName != filepath.Base(fileName) {
  		configPath = fileName
  	} else {
  		if path, err := EnsureConfigFile(fileName); err != nil {
  			appErr := model.NewAppError("LoadConfig", "utils.config.load_config.opening.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
  			return nil, "", nil, appErr
  		} else {
  			configPath = path
  		}
  	}
  
  	config, envConfig, err := ReadConfigFile(configPath, true)
  	if err != nil {
  		appErr := model.NewAppError("LoadConfig", "utils.config.load_config.decoding.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
  		return nil, "", nil, appErr
  	}
  
  	needSave := len(config.SqlSettings.AtRestEncryptKey) == 0 || len(*config.FileSettings.PublicLinkSalt) == 0 ||
  		len(config.EmailSettings.InviteSalt) == 0
  
  	config.SetDefaults()
  
  	// Don't treat it as an error right now if custom terms of service are enabled but text is empty.
  	// This is because terms of service text will be fetched from database at a later state, but
  	// the flag indicating it is enabled is fetched from config file right away.
  	if err := config.IsValid(); err != nil && err.Id != termsOfServiceEnabledAndEmpty.Id {
  		return nil, "", nil, err
  	}
  
  	if needSave {
  		if err := SaveConfig(configPath, config); err != nil {
  			mlog.Warn(err.Error())
  		}
  	}
  
  	if err := ValidateLocales(config); err != nil {
  		if err := SaveConfig(configPath, config); err != nil {
  			mlog.Warn(err.Error())
  		}
  	}
  
  	if *config.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
  		dir := config.FileSettings.Directory
  		if len(dir) > 0 && dir[len(dir)-1:] != "/" {
  			config.FileSettings.Directory += "/"
  		}
  	}
  
  	return config, configPath, envConfig, nil
  }
  
  func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string {
  	props := GenerateLimitedClientConfig(c, diagnosticId, license)
  
  	props["SiteURL"] = strings.TrimRight(*c.ServiceSettings.SiteURL, "/")
  	props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/")
  	props["EnableUserDeactivation"] = strconv.FormatBool(*c.TeamSettings.EnableUserDeactivation)
  	props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage
  	props["EnableXToLeaveChannelsFromLHS"] = strconv.FormatBool(*c.TeamSettings.EnableXToLeaveChannelsFromLHS)
  	props["TeammateNameDisplay"] = *c.TeamSettings.TeammateNameDisplay
  	props["ExperimentalPrimaryTeam"] = *c.TeamSettings.ExperimentalPrimaryTeam
  	props["ExperimentalViewArchivedChannels"] = strconv.FormatBool(*c.TeamSettings.ExperimentalViewArchivedChannels)
  
  	props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider)
  	props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
  	props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
  	props["EnableOutgoingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableOutgoingWebhooks)
  	props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands)
  	props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
  	props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)
  	props["EnableUserAccessTokens"] = strconv.FormatBool(*c.ServiceSettings.EnableUserAccessTokens)
  	props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews)
  	props["EnableTesting"] = strconv.FormatBool(c.ServiceSettings.EnableTesting)
  	props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)
  	props["PostEditTimeLimit"] = fmt.Sprintf("%v", *c.ServiceSettings.PostEditTimeLimit)
  	props["CloseUnusedDirectMessages"] = strconv.FormatBool(*c.ServiceSettings.CloseUnusedDirectMessages)
  	props["EnablePreviewFeatures"] = strconv.FormatBool(*c.ServiceSettings.EnablePreviewFeatures)
  	props["EnableTutorial"] = strconv.FormatBool(*c.ServiceSettings.EnableTutorial)
  	props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
  	props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
  	props["ExperimentalEnablePostMetadata"] = strconv.FormatBool(*c.ExperimentalSettings.EnablePostMetadata)
  
  	if *c.ServiceSettings.ExperimentalChannelOrganization || *c.ServiceSettings.ExperimentalGroupUnreadChannels != model.GROUP_UNREAD_CHANNELS_DISABLED {
  		props["ExperimentalChannelOrganization"] = strconv.FormatBool(true)
  	} else {
  		props["ExperimentalChannelOrganization"] = strconv.FormatBool(false)
  	}
  
  	props["ExperimentalEnableAutomaticReplies"] = strconv.FormatBool(*c.TeamSettings.ExperimentalEnableAutomaticReplies)
  	props["ExperimentalTimezone"] = strconv.FormatBool(*c.DisplaySettings.ExperimentalTimezone)
  
  	props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
  	props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications)
  	props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification)
  	props["EnableEmailBatching"] = strconv.FormatBool(*c.EmailSettings.EnableEmailBatching)
  	props["EnablePreviewModeBanner"] = strconv.FormatBool(*c.EmailSettings.EnablePreviewModeBanner)
  	props["EmailNotificationContentsType"] = *c.EmailSettings.EmailNotificationContentsType
  
  	props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress)
  
  	props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments)
  	props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink)
  
  	props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort)
  	props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort)
  
  	props["AvailableLocales"] = *c.LocalizationSettings.AvailableLocales
  	props["SQLDriverName"] = *c.SqlSettings.DriverName
  
  	props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker)
  	props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker)
  	props["GfycatApiKey"] = *c.ServiceSettings.GfycatApiKey
  	props["GfycatApiSecret"] = *c.ServiceSettings.GfycatApiSecret
  	props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)
  
  	props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10)
  	props["EnableConfirmNotificationsToChannel"] = strconv.FormatBool(*c.TeamSettings.EnableConfirmNotificationsToChannel)
  	props["TimeBetweenUserTypingUpdatesMilliseconds"] = strconv.FormatInt(*c.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, 10)
  	props["EnableUserTypingMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableUserTypingMessages)
  	props["EnableChannelViewedMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableChannelViewedMessages)
  
  	props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable)
  
  	props["RunJobs"] = strconv.FormatBool(*c.JobSettings.RunJobs)
  
  	props["EnableEmailInvitations"] = strconv.FormatBool(*c.ServiceSettings.EnableEmailInvitations)
  
  	// Set default values for all options that require a license.
  	props["ExperimentalHideTownSquareinLHS"] = "false"
  	props["ExperimentalTownSquareIsReadOnly"] = "false"
  	props["ExperimentalEnableAuthenticationTransfer"] = "true"
  	props["LdapNicknameAttributeSet"] = "false"
  	props["LdapFirstNameAttributeSet"] = "false"
  	props["LdapLastNameAttributeSet"] = "false"
  	props["EnforceMultifactorAuthentication"] = "false"
  	props["EnableCompliance"] = "false"
  	props["EnableMobileFileDownload"] = "true"
  	props["EnableMobileFileUpload"] = "true"
  	props["SamlFirstNameAttributeSet"] = "false"
  	props["SamlLastNameAttributeSet"] = "false"
  	props["SamlNicknameAttributeSet"] = "false"
  	props["EnableCluster"] = "false"
  	props["EnableMetrics"] = "false"
  	props["PasswordMinimumLength"] = "0"
  	props["PasswordRequireLowercase"] = "false"
  	props["PasswordRequireUppercase"] = "false"
  	props["PasswordRequireNumber"] = "false"
  	props["PasswordRequireSymbol"] = "false"
  	props["EnableBanner"] = "false"
  	props["BannerText"] = ""
  	props["BannerColor"] = ""
  	props["BannerTextColor"] = ""
  	props["AllowBannerDismissal"] = "false"
  	props["EnableThemeSelection"] = "true"
  	props["DefaultTheme"] = ""
  	props["AllowCustomThemes"] = "true"
  	props["AllowedThemes"] = ""
  	props["DataRetentionEnableMessageDeletion"] = "false"
  	props["DataRetentionMessageRetentionDays"] = "0"
  	props["DataRetentionEnableFileDeletion"] = "false"
  	props["DataRetentionFileRetentionDays"] = "0"
  	props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength)
  	props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase)
  	props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
  	props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
  	props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
  	props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomUrlSchemes, ",")
  
  	if license != nil {
  		props["ExperimentalHideTownSquareinLHS"] = strconv.FormatBool(*c.TeamSettings.ExperimentalHideTownSquareinLHS)
  		props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly)
  		props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
  
  		if *license.Features.LDAP {
  			props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "")
  			props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "")
  			props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "")
  		}
  
  		if *license.Features.MFA {
  			props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication)
  		}
  
  		if *license.Features.Compliance {
  			props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable)
  			props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload)
  			props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload)
  		}
  
  		if *license.Features.SAML {
  			props["SamlFirstNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.FirstNameAttribute != "")
  			props["SamlLastNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.LastNameAttribute != "")
  			props["SamlNicknameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.NicknameAttribute != "")
  
  			// do this under the correct licensed feature
  			props["ExperimentalClientSideCertEnable"] = strconv.FormatBool(*c.ExperimentalSettings.ClientSideCertEnable)
  			props["ExperimentalClientSideCertCheck"] = *c.ExperimentalSettings.ClientSideCertCheck
  		}
  
  		if *license.Features.Cluster {
  			props["EnableCluster"] = strconv.FormatBool(*c.ClusterSettings.Enable)
  		}
  
  		if *license.Features.Cluster {
  			props["EnableMetrics"] = strconv.FormatBool(*c.MetricsSettings.Enable)
  		}
  
  		if *license.Features.Announcement {
  			props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner)
  			props["BannerText"] = *c.AnnouncementSettings.BannerText
  			props["BannerColor"] = *c.AnnouncementSettings.BannerColor
  			props["BannerTextColor"] = *c.AnnouncementSettings.BannerTextColor
  			props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal)
  		}
  
  		if *license.Features.ThemeManagement {
  			props["EnableThemeSelection"] = strconv.FormatBool(*c.ThemeSettings.EnableThemeSelection)
  			props["DefaultTheme"] = *c.ThemeSettings.DefaultTheme
  			props["AllowCustomThemes"] = strconv.FormatBool(*c.ThemeSettings.AllowCustomThemes)
  			props["AllowedThemes"] = strings.Join(c.ThemeSettings.AllowedThemes, ",")
  		}
  
  		if *license.Features.DataRetention {
  			props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion)
  			props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10)
  			props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion)
  			props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10)
  		}
  
  		if *license.Features.CustomTermsOfService {
  			props["EnableCustomTermsOfService"] = strconv.FormatBool(*c.SupportSettings.CustomTermsOfServiceEnabled)
  			props["CustomTermsOfServiceReAcceptancePeriod"] = strconv.FormatInt(int64(*c.SupportSettings.CustomTermsOfServiceReAcceptancePeriod), 10)
  		}
  	}
  
  	return props
  }
  
  func GenerateLimitedClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string {
  	props := make(map[string]string)
  
  	props["Version"] = model.CurrentVersion
  	props["BuildNumber"] = model.BuildNumber
  	props["BuildDate"] = model.BuildDate
  	props["BuildHash"] = model.BuildHash
  	props["BuildHashEnterprise"] = model.BuildHashEnterprise
  	props["BuildEnterpriseReady"] = model.BuildEnterpriseReady
  
  	props["SiteName"] = c.TeamSettings.SiteName
  	props["EnableUserCreation"] = strconv.FormatBool(*c.TeamSettings.EnableUserCreation)
  	props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer)
  
  	props["AndroidLatestVersion"] = c.ClientRequirements.AndroidLatestVersion
  	props["AndroidMinVersion"] = c.ClientRequirements.AndroidMinVersion
  	props["DesktopLatestVersion"] = c.ClientRequirements.DesktopLatestVersion
  	props["DesktopMinVersion"] = c.ClientRequirements.DesktopMinVersion
  	props["IosLatestVersion"] = c.ClientRequirements.IosLatestVersion
  	props["IosMinVersion"] = c.ClientRequirements.IosMinVersion
  
  	props["EnableDiagnostics"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
  
  	props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
  	props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
  	props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
  
  	props["EmailLoginButtonColor"] = *c.EmailSettings.LoginButtonColor
  	props["EmailLoginButtonBorderColor"] = *c.EmailSettings.LoginButtonBorderColor
  	props["EmailLoginButtonTextColor"] = *c.EmailSettings.LoginButtonTextColor
  
  	props["EnableSignUpWithGitLab"] = strconv.FormatBool(c.GitLabSettings.Enable)
  
  	props["TermsOfServiceLink"] = *c.SupportSettings.TermsOfServiceLink
  	props["PrivacyPolicyLink"] = *c.SupportSettings.PrivacyPolicyLink
  	props["AboutLink"] = *c.SupportSettings.AboutLink
  	props["HelpLink"] = *c.SupportSettings.HelpLink
  	props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink
  	props["SupportEmail"] = *c.SupportSettings.SupportEmail
  
  	props["DefaultClientLocale"] = *c.LocalizationSettings.DefaultClientLocale
  
  	props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji)
  	props["AppDownloadLink"] = *c.NativeAppSettings.AppDownloadLink
  	props["AndroidAppDownloadLink"] = *c.NativeAppSettings.AndroidAppDownloadLink
  	props["IosAppDownloadLink"] = *c.NativeAppSettings.IosAppDownloadLink
  
  	props["DiagnosticId"] = diagnosticId
  	props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
  
  	hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != ""
  	props["HasImageProxy"] = strconv.FormatBool(hasImageProxy)
  
  	// Set default values for all options that require a license.
  	props["EnableCustomBrand"] = "false"
  	props["CustomBrandText"] = ""
  	props["CustomDescriptionText"] = ""
  	props["EnableLdap"] = "false"
  	props["LdapLoginFieldName"] = ""
  	props["LdapLoginButtonColor"] = ""
  	props["LdapLoginButtonBorderColor"] = ""
  	props["LdapLoginButtonTextColor"] = ""
  	props["EnableMultifactorAuthentication"] = "false"
  	props["EnableSaml"] = "false"
  	props["SamlLoginButtonText"] = ""
  	props["SamlLoginButtonColor"] = ""
  	props["SamlLoginButtonBorderColor"] = ""
  	props["SamlLoginButtonTextColor"] = ""
  	props["EnableSignUpWithGoogle"] = "false"
  	props["EnableSignUpWithOffice365"] = "false"
  	props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
  	props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
  	props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText
  	props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
  
  	if license != nil {
  		if *license.Features.LDAP {
  			props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable)
  			props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName
  			props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor
  			props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor
  			props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor
  		}
  
  		if *license.Features.SAML {
  			props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable)
  			props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText
  			props["SamlLoginButtonColor"] = *c.SamlSettings.LoginButtonColor
  			props["SamlLoginButtonBorderColor"] = *c.SamlSettings.LoginButtonBorderColor
  			props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor
  		}
  
  		if *license.Features.GoogleOAuth {
  			props["EnableSignUpWithGoogle"] = strconv.FormatBool(c.GoogleSettings.Enable)
  		}
  
  		if *license.Features.Office365OAuth {
  			props["EnableSignUpWithOffice365"] = strconv.FormatBool(c.Office365Settings.Enable)
  		}
  	}
  
  	return props
  }
  
  func ValidateLdapFilter(cfg *model.Config, ldap einterfaces.LdapInterface) *model.AppError {
  	if *cfg.LdapSettings.Enable && ldap != nil && *cfg.LdapSettings.UserFilter != "" {
  		if err := ldap.ValidateFilter(*cfg.LdapSettings.UserFilter); err != nil {
  			return err
  		}
  	}
  	return nil
  }
  
  func ValidateLocales(cfg *model.Config) *model.AppError {
  	var err *model.AppError
  	locales := GetSupportedLocales()
  	if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok {
  		*cfg.LocalizationSettings.DefaultServerLocale = model.DEFAULT_LOCALE
  		err = model.NewAppError("ValidateLocales", "utils.config.supported_server_locale.app_error", nil, "", http.StatusBadRequest)
  	}
  
  	if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok {
  		*cfg.LocalizationSettings.DefaultClientLocale = model.DEFAULT_LOCALE
  		err = model.NewAppError("ValidateLocales", "utils.config.supported_client_locale.app_error", nil, "", http.StatusBadRequest)
  	}
  
  	if len(*cfg.LocalizationSettings.AvailableLocales) > 0 {
  		isDefaultClientLocaleInAvailableLocales := false
  		for _, word := range strings.Split(*cfg.LocalizationSettings.AvailableLocales, ",") {
  			if _, ok := locales[word]; !ok {
  				*cfg.LocalizationSettings.AvailableLocales = ""
  				isDefaultClientLocaleInAvailableLocales = true
  				err = model.NewAppError("ValidateLocales", "utils.config.supported_available_locales.app_error", nil, "", http.StatusBadRequest)
  				break
  			}
  
  			if word == *cfg.LocalizationSettings.DefaultClientLocale {
  				isDefaultClientLocaleInAvailableLocales = true
  			}
  		}
  
  		availableLocales := *cfg.LocalizationSettings.AvailableLocales
  
  		if !isDefaultClientLocaleInAvailableLocales {
  			availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale
  			err = model.NewAppError("ValidateLocales", "utils.config.add_client_locale.app_error", nil, "", http.StatusBadRequest)
  		}
  
  		*cfg.LocalizationSettings.AvailableLocales = strings.Join(RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",")
  	}
  
  	return err
  }
  