mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
There are numerous issues here, including some non-determinism in the viper library (fixable) and some annoying behaviour regarding periods in keys, often used by plugin ids (fix unknown). Let's defer the handling of same until we can get our config loading library to do what we need it to do vs. having to hack around viper all the time.
857 lines
32 KiB
Go
857 lines
32 KiB
Go
// 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{
|
|
".",
|
|
"..",
|
|
"../..",
|
|
"../../..",
|
|
}
|
|
)
|
|
|
|
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{}
|
|
for _, baseSearchPath := range baseSearchPaths {
|
|
searchPaths = append(searchPaths, baseSearchPath)
|
|
}
|
|
|
|
// 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()
|
|
|
|
if err := config.IsValid(); err != nil {
|
|
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["RestrictTeamInvite"] = *c.TeamSettings.RestrictTeamInvite
|
|
props["RestrictPublicChannelCreation"] = *c.TeamSettings.RestrictPublicChannelCreation
|
|
props["RestrictPrivateChannelCreation"] = *c.TeamSettings.RestrictPrivateChannelCreation
|
|
props["RestrictPublicChannelManagement"] = *c.TeamSettings.RestrictPublicChannelManagement
|
|
props["RestrictPrivateChannelManagement"] = *c.TeamSettings.RestrictPrivateChannelManagement
|
|
props["RestrictPublicChannelDeletion"] = *c.TeamSettings.RestrictPublicChannelDeletion
|
|
props["RestrictPrivateChannelDeletion"] = *c.TeamSettings.RestrictPrivateChannelDeletion
|
|
props["RestrictPrivateChannelManageMembers"] = *c.TeamSettings.RestrictPrivateChannelManageMembers
|
|
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["EnableOnlyAdminIntegrations"] = strconv.FormatBool(*c.ServiceSettings.EnableOnlyAdminIntegrations)
|
|
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["RestrictPostDelete"] = *c.ServiceSettings.RestrictPostDelete
|
|
props["AllowEditPost"] = *c.ServiceSettings.AllowEditPost
|
|
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
|
|
|
|
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["RestrictCustomEmojiCreation"] = *c.ServiceSettings.RestrictCustomEmojiCreation
|
|
props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)
|
|
|
|
props["EnableWebrtc"] = strconv.FormatBool(*c.WebrtcSettings.Enable)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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["EnableTeamCreation"] = strconv.FormatBool(*c.TeamSettings.EnableTeamCreation)
|
|
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
|
|
|
|
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.MFA {
|
|
props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
|
|
}
|
|
|
|
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
|
|
}
|