github.com/keys-pub/mattermost-server@v4.10.10+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  
   513  	props["ShowEmailAddress"] = strconv.FormatBool(c.PrivacySettings.ShowEmailAddress)
   514  
   515  	props["TermsOfServiceLink"] = *c.SupportSettings.TermsOfServiceLink
   516  	props["PrivacyPolicyLink"] = *c.SupportSettings.PrivacyPolicyLink
   517  	props["AboutLink"] = *c.SupportSettings.AboutLink
   518  	props["HelpLink"] = *c.SupportSettings.HelpLink
   519  	props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink
   520  	props["SupportEmail"] = *c.SupportSettings.SupportEmail
   521  
   522  	props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments)
   523  	props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink)
   524  
   525  	props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort)
   526  	props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort)
   527  
   528  	props["DefaultClientLocale"] = *c.LocalizationSettings.DefaultClientLocale
   529  	props["AvailableLocales"] = *c.LocalizationSettings.AvailableLocales
   530  	props["SQLDriverName"] = *c.SqlSettings.DriverName
   531  
   532  	props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji)
   533  	props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker)
   534  	props["RestrictCustomEmojiCreation"] = *c.ServiceSettings.RestrictCustomEmojiCreation
   535  	props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)
   536  	props["AppDownloadLink"] = *c.NativeAppSettings.AppDownloadLink
   537  	props["AndroidAppDownloadLink"] = *c.NativeAppSettings.AndroidAppDownloadLink
   538  	props["IosAppDownloadLink"] = *c.NativeAppSettings.IosAppDownloadLink
   539  
   540  	props["EnableWebrtc"] = strconv.FormatBool(*c.WebrtcSettings.Enable)
   541  
   542  	props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10)
   543  	props["EnableConfirmNotificationsToChannel"] = strconv.FormatBool(*c.TeamSettings.EnableConfirmNotificationsToChannel)
   544  	props["TimeBetweenUserTypingUpdatesMilliseconds"] = strconv.FormatInt(*c.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, 10)
   545  	props["EnableUserTypingMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableUserTypingMessages)
   546  	props["EnableChannelViewedMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableChannelViewedMessages)
   547  
   548  	props["DiagnosticId"] = diagnosticId
   549  	props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
   550  
   551  	props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable)
   552  
   553  	hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != ""
   554  	props["HasImageProxy"] = strconv.FormatBool(hasImageProxy)
   555  
   556  	// Set default values for all options that require a license.
   557  	props["ExperimentalTownSquareIsReadOnly"] = "false"
   558  	props["ExperimentalEnableAuthenticationTransfer"] = "true"
   559  	props["EnableCustomBrand"] = "false"
   560  	props["CustomBrandText"] = ""
   561  	props["CustomDescriptionText"] = ""
   562  	props["EnableLdap"] = "false"
   563  	props["LdapLoginFieldName"] = ""
   564  	props["LdapNicknameAttributeSet"] = "false"
   565  	props["LdapFirstNameAttributeSet"] = "false"
   566  	props["LdapLastNameAttributeSet"] = "false"
   567  	props["LdapLoginButtonColor"] = ""
   568  	props["LdapLoginButtonBorderColor"] = ""
   569  	props["LdapLoginButtonTextColor"] = ""
   570  	props["EnableMultifactorAuthentication"] = "false"
   571  	props["EnforceMultifactorAuthentication"] = "false"
   572  	props["EnableCompliance"] = "false"
   573  	props["EnableMobileFileDownload"] = "true"
   574  	props["EnableMobileFileUpload"] = "true"
   575  	props["EnableSaml"] = "false"
   576  	props["SamlLoginButtonText"] = ""
   577  	props["SamlFirstNameAttributeSet"] = "false"
   578  	props["SamlLastNameAttributeSet"] = "false"
   579  	props["SamlNicknameAttributeSet"] = "false"
   580  	props["SamlLoginButtonColor"] = ""
   581  	props["SamlLoginButtonBorderColor"] = ""
   582  	props["SamlLoginButtonTextColor"] = ""
   583  	props["EnableCluster"] = "false"
   584  	props["EnableMetrics"] = "false"
   585  	props["EnableSignUpWithGoogle"] = "false"
   586  	props["EnableSignUpWithOffice365"] = "false"
   587  	props["PasswordMinimumLength"] = "0"
   588  	props["PasswordRequireLowercase"] = "false"
   589  	props["PasswordRequireUppercase"] = "false"
   590  	props["PasswordRequireNumber"] = "false"
   591  	props["PasswordRequireSymbol"] = "false"
   592  	props["EnableBanner"] = "false"
   593  	props["BannerText"] = ""
   594  	props["BannerColor"] = ""
   595  	props["BannerTextColor"] = ""
   596  	props["AllowBannerDismissal"] = "false"
   597  	props["EnableThemeSelection"] = "true"
   598  	props["DefaultTheme"] = ""
   599  	props["AllowCustomThemes"] = "true"
   600  	props["AllowedThemes"] = ""
   601  	props["DataRetentionEnableMessageDeletion"] = "false"
   602  	props["DataRetentionMessageRetentionDays"] = "0"
   603  	props["DataRetentionEnableFileDeletion"] = "false"
   604  	props["DataRetentionFileRetentionDays"] = "0"
   605  
   606  	if license != nil {
   607  		props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly)
   608  		props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
   609  
   610  		if *license.Features.CustomBrand {
   611  			props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
   612  			props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
   613  			props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText
   614  		}
   615  
   616  		if *license.Features.LDAP {
   617  			props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable)
   618  			props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName
   619  			props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "")
   620  			props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "")
   621  			props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "")
   622  			props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor
   623  			props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor
   624  			props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor
   625  		}
   626  
   627  		if *license.Features.MFA {
   628  			props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
   629  			props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication)
   630  		}
   631  
   632  		if *license.Features.Compliance {
   633  			props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable)
   634  			props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload)
   635  			props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload)
   636  		}
   637  
   638  		if *license.Features.SAML {
   639  			props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable)
   640  			props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText
   641  			props["SamlFirstNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.FirstNameAttribute != "")
   642  			props["SamlLastNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.LastNameAttribute != "")
   643  			props["SamlNicknameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.NicknameAttribute != "")
   644  			props["SamlLoginButtonColor"] = *c.SamlSettings.LoginButtonColor
   645  			props["SamlLoginButtonBorderColor"] = *c.SamlSettings.LoginButtonBorderColor
   646  			props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor
   647  		}
   648  
   649  		if *license.Features.Cluster {
   650  			props["EnableCluster"] = strconv.FormatBool(*c.ClusterSettings.Enable)
   651  		}
   652  
   653  		if *license.Features.Cluster {
   654  			props["EnableMetrics"] = strconv.FormatBool(*c.MetricsSettings.Enable)
   655  		}
   656  
   657  		if *license.Features.GoogleOAuth {
   658  			props["EnableSignUpWithGoogle"] = strconv.FormatBool(c.GoogleSettings.Enable)
   659  		}
   660  
   661  		if *license.Features.Office365OAuth {
   662  			props["EnableSignUpWithOffice365"] = strconv.FormatBool(c.Office365Settings.Enable)
   663  		}
   664  
   665  		if *license.Features.PasswordRequirements {
   666  			props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength)
   667  			props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase)
   668  			props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
   669  			props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
   670  			props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
   671  		}
   672  
   673  		if *license.Features.Announcement {
   674  			props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner)
   675  			props["BannerText"] = *c.AnnouncementSettings.BannerText
   676  			props["BannerColor"] = *c.AnnouncementSettings.BannerColor
   677  			props["BannerTextColor"] = *c.AnnouncementSettings.BannerTextColor
   678  			props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal)
   679  		}
   680  
   681  		if *license.Features.ThemeManagement {
   682  			props["EnableThemeSelection"] = strconv.FormatBool(*c.ThemeSettings.EnableThemeSelection)
   683  			props["DefaultTheme"] = *c.ThemeSettings.DefaultTheme
   684  			props["AllowCustomThemes"] = strconv.FormatBool(*c.ThemeSettings.AllowCustomThemes)
   685  			props["AllowedThemes"] = strings.Join(c.ThemeSettings.AllowedThemes, ",")
   686  		}
   687  
   688  		if *license.Features.DataRetention {
   689  			props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion)
   690  			props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10)
   691  			props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion)
   692  			props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10)
   693  		}
   694  	}
   695  
   696  	return props
   697  }
   698  
   699  func ValidateLdapFilter(cfg *model.Config, ldap einterfaces.LdapInterface) *model.AppError {
   700  	if *cfg.LdapSettings.Enable && ldap != nil && *cfg.LdapSettings.UserFilter != "" {
   701  		if err := ldap.ValidateFilter(*cfg.LdapSettings.UserFilter); err != nil {
   702  			return err
   703  		}
   704  	}
   705  	return nil
   706  }
   707  
   708  func ValidateLocales(cfg *model.Config) *model.AppError {
   709  	var err *model.AppError
   710  	locales := GetSupportedLocales()
   711  	if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok {
   712  		*cfg.LocalizationSettings.DefaultServerLocale = model.DEFAULT_LOCALE
   713  		err = model.NewAppError("ValidateLocales", "utils.config.supported_server_locale.app_error", nil, "", http.StatusBadRequest)
   714  	}
   715  
   716  	if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok {
   717  		*cfg.LocalizationSettings.DefaultClientLocale = model.DEFAULT_LOCALE
   718  		err = model.NewAppError("ValidateLocales", "utils.config.supported_client_locale.app_error", nil, "", http.StatusBadRequest)
   719  	}
   720  
   721  	if len(*cfg.LocalizationSettings.AvailableLocales) > 0 {
   722  		isDefaultClientLocaleInAvailableLocales := false
   723  		for _, word := range strings.Split(*cfg.LocalizationSettings.AvailableLocales, ",") {
   724  			if _, ok := locales[word]; !ok {
   725  				*cfg.LocalizationSettings.AvailableLocales = ""
   726  				isDefaultClientLocaleInAvailableLocales = true
   727  				err = model.NewAppError("ValidateLocales", "utils.config.supported_available_locales.app_error", nil, "", http.StatusBadRequest)
   728  				break
   729  			}
   730  
   731  			if word == *cfg.LocalizationSettings.DefaultClientLocale {
   732  				isDefaultClientLocaleInAvailableLocales = true
   733  			}
   734  		}
   735  
   736  		availableLocales := *cfg.LocalizationSettings.AvailableLocales
   737  
   738  		if !isDefaultClientLocaleInAvailableLocales {
   739  			availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale
   740  			err = model.NewAppError("ValidateLocales", "utils.config.add_client_locale.app_error", nil, "", http.StatusBadRequest)
   741  		}
   742  
   743  		*cfg.LocalizationSettings.AvailableLocales = strings.Join(RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",")
   744  	}
   745  
   746  	return err
   747  }