github.com/lologarithm/mattermost-server@v5.3.2-0.20181002060438-c82a84ed765b+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  }