diff options
Diffstat (limited to 'app')
35 files changed, 1657 insertions, 650 deletions
diff --git a/app/admin.go b/app/admin.go index e46d9073c..b838ed3bd 100644 --- a/app/admin.go +++ b/app/admin.go @@ -159,7 +159,7 @@ func (a *App) GetConfig() *model.Config { func (a *App) SaveConfig(cfg *model.Config, sendConfigChangeClusterMessage bool) *model.AppError { oldCfg := a.Config() cfg.SetDefaults() - utils.Desanitize(cfg) + a.Desanitize(cfg) if err := cfg.IsValid(); err != nil { return err @@ -173,13 +173,13 @@ func (a *App) SaveConfig(cfg *model.Config, sendConfigChangeClusterMessage bool) return model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, "", http.StatusForbidden) } - utils.DisableConfigWatch() + a.DisableConfigWatch() a.UpdateConfig(func(update *model.Config) { *update = *cfg }) a.PersistConfig() a.ReloadConfig() - utils.EnableConfigWatch() + a.EnableConfigWatch() if a.Metrics != nil { if *a.Config().MetricsSettings.Enable { diff --git a/app/app.go b/app/app.go index 959c99306..1e46d29d0 100644 --- a/app/app.go +++ b/app/app.go @@ -7,13 +7,13 @@ import ( "html/template" "net" "net/http" - "runtime/debug" "strings" "sync" "sync/atomic" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/pkg/errors" "github.com/mattermost/mattermost-server/einterfaces" ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs" @@ -54,23 +54,33 @@ type App struct { Mfa einterfaces.MfaInterface Saml einterfaces.SamlInterface - configFile string - newStore func() store.Store + config atomic.Value + configFile string + configListeners map[string]func(*model.Config, *model.Config) + + newStore func() store.Store htmlTemplateWatcher *utils.HTMLTemplateWatcher sessionCache *utils.Cache roles map[string]*model.Role configListenerId string + licenseListenerId string + disableConfigWatch bool + configWatcher *utils.ConfigWatcher pluginCommands []*PluginCommand pluginCommandsLock sync.RWMutex + + clientConfig map[string]string + clientConfigHash string + diagnosticId string } var appCount = 0 // New creates a new App. You must call Shutdown when you're done with it. // XXX: For now, only one at a time is allowed as some resources are still shared. -func New(options ...Option) *App { +func New(options ...Option) (*App, error) { appCount++ if appCount > 1 { panic("Only one App should exist at a time. Did you forget to call Shutdown()?") @@ -81,8 +91,10 @@ func New(options ...Option) *App { Srv: &Server{ Router: mux.NewRouter(), }, - sessionCache: utils.NewLru(model.SESSION_CACHE_SIZE), - configFile: "config.json", + sessionCache: utils.NewLru(model.SESSION_CACHE_SIZE), + configFile: "config.json", + configListeners: make(map[string]func(*model.Config, *model.Config)), + clientConfig: make(map[string]string), } for _, option := range options { @@ -90,14 +102,24 @@ func New(options ...Option) *App { } if utils.T == nil { - utils.TranslationsPreInit() + if err := utils.TranslationsPreInit(); err != nil { + return nil, errors.Wrapf(err, "unable to load Mattermost translation files") + } + } + model.AppErrorInit(utils.T) + if err := app.LoadConfig(app.configFile); err != nil { + return nil, err + } + app.EnableConfigWatch() + if err := utils.InitTranslations(app.Config().LocalizationSettings); err != nil { + return nil, errors.Wrapf(err, "unable to load Mattermost translation files") } - utils.LoadGlobalConfig(app.configFile) - utils.InitTranslations(utils.Cfg.LocalizationSettings) - app.configListenerId = utils.AddConfigListener(func(_, cfg *model.Config) { - app.SetDefaultRolesBasedOnConfig() + app.configListenerId = app.AddConfigListener(func(_, _ *model.Config) { + app.configOrLicenseListener() }) + app.licenseListenerId = utils.AddLicenseListener(app.configOrLicenseListener) + app.regenerateClientConfig() app.SetDefaultRolesBasedOnConfig() l4g.Info(utils.T("api.server.new_server.init.info")) @@ -130,7 +152,12 @@ func New(options ...Option) *App { handlers: make(map[string]webSocketHandler), } - return app + return app, nil +} + +func (a *App) configOrLicenseListener() { + a.regenerateClientConfig() + a.SetDefaultRolesBasedOnConfig() } func (a *App) Shutdown() { @@ -151,8 +178,11 @@ func (a *App) Shutdown() { a.htmlTemplateWatcher.Close() } - utils.RemoveConfigListener(a.configListenerId) + a.RemoveConfigListener(a.configListenerId) + utils.RemoveLicenseListener(a.licenseListenerId) l4g.Info(utils.T("api.server.stop_server.stopped.info")) + + a.DisableConfigWatch() } var accountMigrationInterface func(*App) einterfaces.AccountMigrationInterface @@ -278,7 +308,7 @@ func (a *App) initEnterprise() { } if ldapInterface != nil { a.Ldap = ldapInterface(a) - utils.AddConfigListener(func(_, cfg *model.Config) { + a.AddConfigListener(func(_, cfg *model.Config) { if err := utils.ValidateLdapFilter(cfg, a.Ldap); err != nil { panic(utils.T(err.Id)) } @@ -295,7 +325,7 @@ func (a *App) initEnterprise() { } if samlInterface != nil { a.Saml = samlInterface(a) - utils.AddConfigListener(func(_, cfg *model.Config) { + a.AddConfigListener(func(_, cfg *model.Config) { a.Saml.ConfigureSP() }) } @@ -305,7 +335,7 @@ func (a *App) initEnterprise() { } func (a *App) initJobs() { - a.Jobs = jobs.NewJobServer(a.Config, a.Srv.Store) + a.Jobs = jobs.NewJobServer(a, a.Srv.Store) if jobsDataRetentionJobInterface != nil { a.Jobs.DataRetentionJob = jobsDataRetentionJobInterface(a) } @@ -323,30 +353,30 @@ func (a *App) initJobs() { } } -func (a *App) Config() *model.Config { - return utils.Cfg +func (a *App) DiagnosticId() string { + return a.diagnosticId } -func (a *App) UpdateConfig(f func(*model.Config)) { - old := utils.Cfg.Clone() - f(utils.Cfg) - utils.InvokeGlobalConfigListeners(old, utils.Cfg) +func (a *App) SetDiagnosticId(id string) { + a.diagnosticId = id } -func (a *App) PersistConfig() { - utils.SaveConfig(a.ConfigFileName(), a.Config()) -} - -func (a *App) ReloadConfig() { - debug.FreeOSMemory() - utils.LoadGlobalConfig(a.ConfigFileName()) - - // start/restart email batching job if necessary - a.InitEmailBatching() -} +func (a *App) EnsureDiagnosticId() { + if a.diagnosticId != "" { + return + } + if result := <-a.Srv.Store.System().Get(); result.Err == nil { + props := result.Data.(model.StringMap) + + id := props[model.SYSTEM_DIAGNOSTIC_ID] + if len(id) == 0 { + id = model.NewId() + systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id} + <-a.Srv.Store.System().Save(systemId) + } -func (a *App) ConfigFileName() string { - return utils.CfgFileName + a.diagnosticId = id + } } // Go creates a goroutine, but maintains a record of it to ensure that execution completes before @@ -415,7 +445,6 @@ func (a *App) HTTPClient(trustURLs bool) *http.Client { func (a *App) Handle404(w http.ResponseWriter, r *http.Request) { err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound) - err.Translate(utils.T) l4g.Debug("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r)) diff --git a/app/app_test.go b/app/app_test.go index 2058ddd79..25b19ead8 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -11,6 +11,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store/storetest" @@ -47,7 +48,8 @@ func TestMain(m *testing.M) { func TestAppRace(t *testing.T) { for i := 0; i < 10; i++ { - a := New() + a, err := New() + require.NoError(t, err) a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) a.StartServer() a.Shutdown() @@ -59,15 +61,11 @@ func TestUpdateConfig(t *testing.T) { defer th.TearDown() prev := *th.App.Config().ServiceSettings.SiteURL - defer th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.ServiceSettings.SiteURL = prev - }) - listener := utils.AddConfigListener(func(old, current *model.Config) { + th.App.AddConfigListener(func(old, current *model.Config) { assert.Equal(t, prev, *old.ServiceSettings.SiteURL) assert.Equal(t, "foo", *current.ServiceSettings.SiteURL) }) - defer utils.RemoveConfigListener(listener) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "foo" diff --git a/app/apptestlib.go b/app/apptestlib.go index 912433290..09afc8f76 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -5,6 +5,7 @@ package app import ( "encoding/json" + "io" "io/ioutil" "os" "path/filepath" @@ -29,8 +30,9 @@ type TestHelper struct { BasicChannel *model.Channel BasicPost *model.Post - tempWorkspace string - pluginHooks map[string]plugin.Hooks + tempConfigPath string + tempWorkspace string + pluginHooks map[string]plugin.Hooks } type persistentTestStore struct { @@ -57,14 +59,35 @@ func StopTestStore() { } func setupTestHelper(enterprise bool) *TestHelper { - var options []Option + permConfig, err := os.Open(utils.FindConfigFile("config.json")) + if err != nil { + panic(err) + } + defer permConfig.Close() + tempConfig, err := ioutil.TempFile("", "") + if err != nil { + panic(err) + } + _, err = io.Copy(tempConfig, permConfig) + tempConfig.Close() + if err != nil { + panic(err) + } + + options := []Option{ConfigFile(tempConfig.Name()), DisableConfigWatch} if testStore != nil { options = append(options, StoreOverride(testStore)) } + a, err := New(options...) + if err != nil { + panic(err) + } + th := &TestHelper{ - App: New(options...), - pluginHooks: make(map[string]plugin.Hooks), + App: a, + pluginHooks: make(map[string]plugin.Hooks), + tempConfigPath: tempConfig.Name(), } th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 }) @@ -227,6 +250,7 @@ func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) { func (me *TestHelper) TearDown() { me.App.Shutdown() + os.Remove(me.tempConfigPath) if err := recover(); err != nil { StopTestStore() panic(err) diff --git a/app/auto_channels.go b/app/auto_channels.go index bb67e1aa8..78b500961 100644 --- a/app/auto_channels.go +++ b/app/auto_channels.go @@ -50,7 +50,6 @@ func (cfg *AutoChannelCreator) createRandomChannel() (*model.Channel, bool) { println(cfg.client.GetTeamRoute()) result, err := cfg.client.CreateChannel(channel) if err != nil { - err.Translate(utils.T) println(err.Error()) println(err.DetailedError) return nil, false diff --git a/app/auto_users.go b/app/auto_users.go index 36b5b7279..8ed6767ad 100644 --- a/app/auto_users.go +++ b/app/auto_users.go @@ -75,7 +75,6 @@ func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) { result, err := cfg.client.CreateUserWithInvite(user, "", "", cfg.team.InviteId) if err != nil { - err.Translate(utils.T) l4g.Error(err.Error()) return nil, false } @@ -84,7 +83,6 @@ func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) { status := &model.Status{UserId: ruser.Id, Status: model.STATUS_ONLINE, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""} if result := <-cfg.app.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - result.Err.Translate(utils.T) l4g.Error(result.Err.Error()) return nil, false } diff --git a/app/channel.go b/app/channel.go index 3480d5b0e..0054fe14b 100644 --- a/app/channel.go +++ b/app/channel.go @@ -63,13 +63,15 @@ func (a *App) JoinDefaultChannels(teamId string, user *model.User, channelRole s l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) } - if requestor == nil { - if err := a.postJoinTeamMessage(user, townSquare); err != nil { - l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) - } - } else { - if err := a.postAddToTeamMessage(requestor, user, townSquare, ""); err != nil { - l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { + if requestor == nil { + if err := a.postJoinTeamMessage(user, townSquare); err != nil { + l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + } + } else { + if err := a.postAddToTeamMessage(requestor, user, townSquare, ""); err != nil { + l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + } } } @@ -341,6 +343,47 @@ func (a *App) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppE } } +func (a *App) UpdateChannelPrivacy(oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) { + if channel, err := a.UpdateChannel(oldChannel); err != nil { + return channel, err + } else { + if err := a.postChannelPrivacyMessage(user, channel); err != nil { + if channel.Type == model.CHANNEL_OPEN { + channel.Type = model.CHANNEL_PRIVATE + } else { + channel.Type = model.CHANNEL_OPEN + } + // revert to previous channel privacy + a.UpdateChannel(channel) + return channel, err + } + + return channel, nil + } +} + +func (a *App) postChannelPrivacyMessage(user *model.User, channel *model.Channel) *model.AppError { + privacy := (map[string]string{ + model.CHANNEL_OPEN: "private_to_public", + model.CHANNEL_PRIVATE: "public_to_private", + })[channel.Type] + post := &model.Post{ + ChannelId: channel.Id, + Message: fmt.Sprintf(utils.T("api.channel.change_channel_privacy." + privacy)), + Type: model.POST_CHANGE_CHANNEL_PRIVACY, + UserId: user.Id, + Props: model.StringInterface{ + "username": user.Username, + }, + } + + if _, err := a.CreatePost(post, channel, false); err != nil { + return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} + func (a *App) RestoreChannel(channel *model.Channel) (*model.Channel, *model.AppError) { if result := <-a.Srv.Store.Channel().Restore(channel.Id, model.GetMillis()); result.Err != nil { return nil, result.Err @@ -1012,6 +1055,10 @@ func (a *App) LeaveChannel(channelId string, userId string) *model.AppError { return err } + if channel.Name == model.DEFAULT_CHANNEL && *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages == false { + return nil + } + a.Go(func() { a.postLeaveChannelMessage(user, channel) }) diff --git a/app/channel_test.go b/app/channel_test.go index d44af467d..a414fbb35 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -190,6 +190,21 @@ func TestCreateChannelPrivate(t *testing.T) { assert.Equal(t, privateChannel.Id, histories[0].ChannelId) } +func TestUpdateChannelPrivacy(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + privateChannel := th.createChannel(th.BasicTeam, model.CHANNEL_PRIVATE) + privateChannel.Type = model.CHANNEL_OPEN + + if publicChannel, err := th.App.UpdateChannelPrivacy(privateChannel, th.BasicUser); err != nil { + t.Fatal("Failed to update channel privacy. Error: " + err.Error()) + } else { + assert.Equal(t, publicChannel.Id, privateChannel.Id) + assert.Equal(t, publicChannel.Type, model.CHANNEL_OPEN) + } +} + func TestCreateGroupChannel(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/command.go b/app/command.go index 4c26eae71..fa9b38bf3 100644 --- a/app/command.go +++ b/app/command.go @@ -274,28 +274,34 @@ func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandA post.Type = response.Type post.Props = response.Props - if !builtIn { - post.AddProp("from_webhook", "true") - } + isBotPost := !builtIn if a.Config().ServiceSettings.EnablePostUsernameOverride { if len(command.Username) != 0 { post.AddProp("override_username", command.Username) + isBotPost = true } else if len(response.Username) != 0 { post.AddProp("override_username", response.Username) + isBotPost = true } } if a.Config().ServiceSettings.EnablePostIconOverride { if len(command.IconURL) != 0 { post.AddProp("override_icon_url", command.IconURL) + isBotPost = true } else if len(response.IconURL) != 0 { post.AddProp("override_icon_url", response.IconURL) + isBotPost = true } else { post.AddProp("override_icon_url", "") } } + if isBotPost { + post.AddProp("from_webhook", "true") + } + // Process Slack text replacements response.Text = a.ProcessSlackText(response.Text) response.Attachments = a.ProcessSlackAttachments(response.Attachments) diff --git a/app/config.go b/app/config.go new file mode 100644 index 000000000..526d47a77 --- /dev/null +++ b/app/config.go @@ -0,0 +1,168 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "crypto/md5" + "encoding/json" + "fmt" + "runtime/debug" + + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" +) + +func (a *App) Config() *model.Config { + if cfg := a.config.Load(); cfg != nil { + return cfg.(*model.Config) + } + return &model.Config{} +} + +func (a *App) UpdateConfig(f func(*model.Config)) { + old := a.Config() + updated := old.Clone() + f(updated) + a.config.Store(updated) + a.InvokeConfigListeners(old, updated) +} + +func (a *App) PersistConfig() { + utils.SaveConfig(a.ConfigFileName(), a.Config()) +} + +func (a *App) LoadConfig(configFile string) *model.AppError { + old := a.Config() + + cfg, configPath, err := utils.LoadConfig(configFile) + if err != nil { + return err + } + + a.configFile = configPath + + utils.ConfigureLog(&cfg.LogSettings) + + a.config.Store(cfg) + + utils.SetSiteURL(*cfg.ServiceSettings.SiteURL) + + a.InvokeConfigListeners(old, cfg) + return nil +} + +func (a *App) ReloadConfig() *model.AppError { + debug.FreeOSMemory() + if err := a.LoadConfig(a.configFile); err != nil { + return err + } + + // start/restart email batching job if necessary + a.InitEmailBatching() + return nil +} + +func (a *App) ConfigFileName() string { + return a.configFile +} + +func (a *App) ClientConfig() map[string]string { + return a.clientConfig +} + +func (a *App) ClientConfigHash() string { + return a.clientConfigHash +} + +func (a *App) EnableConfigWatch() { + if a.configWatcher == nil && !a.disableConfigWatch { + configWatcher, err := utils.NewConfigWatcher(a.ConfigFileName(), func() { + a.ReloadConfig() + }) + if err != nil { + l4g.Error(err) + } + a.configWatcher = configWatcher + } +} + +func (a *App) DisableConfigWatch() { + if a.configWatcher != nil { + a.configWatcher.Close() + a.configWatcher = nil + } +} + +// Registers a function with a given to be called when the config is reloaded and may have changed. The function +// will be called with two arguments: the old config and the new config. AddConfigListener returns a unique ID +// for the listener that can later be used to remove it. +func (a *App) AddConfigListener(listener func(*model.Config, *model.Config)) string { + id := model.NewId() + a.configListeners[id] = listener + return id +} + +// Removes a listener function by the unique ID returned when AddConfigListener was called +func (a *App) RemoveConfigListener(id string) { + delete(a.configListeners, id) +} + +func (a *App) InvokeConfigListeners(old, current *model.Config) { + for _, listener := range a.configListeners { + listener(old, current) + } +} + +func (a *App) regenerateClientConfig() { + a.clientConfig = utils.GenerateClientConfig(a.Config(), a.DiagnosticId()) + clientConfigJSON, _ := json.Marshal(a.clientConfig) + a.clientConfigHash = fmt.Sprintf("%x", md5.Sum(clientConfigJSON)) +} + +func (a *App) Desanitize(cfg *model.Config) { + actual := a.Config() + + if cfg.LdapSettings.BindPassword != nil && *cfg.LdapSettings.BindPassword == model.FAKE_SETTING { + *cfg.LdapSettings.BindPassword = *actual.LdapSettings.BindPassword + } + + if *cfg.FileSettings.PublicLinkSalt == model.FAKE_SETTING { + *cfg.FileSettings.PublicLinkSalt = *actual.FileSettings.PublicLinkSalt + } + if cfg.FileSettings.AmazonS3SecretAccessKey == model.FAKE_SETTING { + cfg.FileSettings.AmazonS3SecretAccessKey = actual.FileSettings.AmazonS3SecretAccessKey + } + + if cfg.EmailSettings.InviteSalt == model.FAKE_SETTING { + cfg.EmailSettings.InviteSalt = actual.EmailSettings.InviteSalt + } + if cfg.EmailSettings.SMTPPassword == model.FAKE_SETTING { + cfg.EmailSettings.SMTPPassword = actual.EmailSettings.SMTPPassword + } + + if cfg.GitLabSettings.Secret == model.FAKE_SETTING { + cfg.GitLabSettings.Secret = actual.GitLabSettings.Secret + } + + if *cfg.SqlSettings.DataSource == model.FAKE_SETTING { + *cfg.SqlSettings.DataSource = *actual.SqlSettings.DataSource + } + if cfg.SqlSettings.AtRestEncryptKey == model.FAKE_SETTING { + cfg.SqlSettings.AtRestEncryptKey = actual.SqlSettings.AtRestEncryptKey + } + + if *cfg.ElasticsearchSettings.Password == model.FAKE_SETTING { + *cfg.ElasticsearchSettings.Password = *actual.ElasticsearchSettings.Password + } + + for i := range cfg.SqlSettings.DataSourceReplicas { + cfg.SqlSettings.DataSourceReplicas[i] = actual.SqlSettings.DataSourceReplicas[i] + } + + for i := range cfg.SqlSettings.DataSourceSearchReplicas { + cfg.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i] + } +} diff --git a/app/config_test.go b/app/config_test.go new file mode 100644 index 000000000..e3d50b958 --- /dev/null +++ b/app/config_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/mattermost-server/model" +) + +func TestConfigListener(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + originalSiteName := th.App.Config().TeamSettings.SiteName + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.TeamSettings.SiteName = "test123" + }) + + listenerCalled := false + listener := func(oldConfig *model.Config, newConfig *model.Config) { + if listenerCalled { + t.Fatal("listener called twice") + } + + if oldConfig.TeamSettings.SiteName != "test123" { + t.Fatal("old config contains incorrect site name") + } else if newConfig.TeamSettings.SiteName != originalSiteName { + t.Fatal("new config contains incorrect site name") + } + + listenerCalled = true + } + listenerId := th.App.AddConfigListener(listener) + defer th.App.RemoveConfigListener(listenerId) + + listener2Called := false + listener2 := func(oldConfig *model.Config, newConfig *model.Config) { + if listener2Called { + t.Fatal("listener2 called twice") + } + + listener2Called = true + } + listener2Id := th.App.AddConfigListener(listener2) + defer th.App.RemoveConfigListener(listener2Id) + + th.App.ReloadConfig() + + if !listenerCalled { + t.Fatal("listener should've been called") + } else if !listener2Called { + t.Fatal("listener 2 should've been called") + } +} diff --git a/app/diagnostics.go b/app/diagnostics.go index c427578d7..135875de6 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -56,16 +56,16 @@ var client *analytics.Client func (a *App) SendDailyDiagnostics() { if *a.Config().LogSettings.EnableDiagnostics && a.IsLeader() { - initDiagnostics("") + a.initDiagnostics("") a.trackActivity() a.trackConfig() - trackLicense() + a.trackLicense() a.trackPlugins() a.trackServer() } } -func initDiagnostics(endpoint string) { +func (a *App) initDiagnostics(endpoint string) { if client == nil { client = analytics.New(SEGMENT_KEY) // For testing @@ -76,15 +76,15 @@ func initDiagnostics(endpoint string) { client.Logger = log.New(os.Stdout, "segment ", log.LstdFlags) } client.Identify(&analytics.Identify{ - UserId: utils.CfgDiagnosticId, + UserId: a.DiagnosticId(), }) } } -func SendDiagnostic(event string, properties map[string]interface{}) { +func (a *App) SendDiagnostic(event string, properties map[string]interface{}) { client.Track(&analytics.Track{ Event: event, - UserId: utils.CfgDiagnosticId, + UserId: a.DiagnosticId(), Properties: properties, }) } @@ -170,7 +170,7 @@ func (a *App) trackActivity() { postsCount = pcr.Data.(int64) } - SendDiagnostic(TRACK_ACTIVITY, map[string]interface{}{ + a.SendDiagnostic(TRACK_ACTIVITY, map[string]interface{}{ "registered_users": userCount, "active_users": activeUserCount, "registered_inactive_users": inactiveUserCount, @@ -189,59 +189,60 @@ func (a *App) trackActivity() { func (a *App) trackConfig() { cfg := a.Config() - SendDiagnostic(TRACK_CONFIG_SERVICE, map[string]interface{}{ - "web_server_mode": *cfg.ServiceSettings.WebserverMode, - "enable_security_fix_alert": *cfg.ServiceSettings.EnableSecurityFixAlert, - "enable_insecure_outgoing_connections": *cfg.ServiceSettings.EnableInsecureOutgoingConnections, - "enable_incoming_webhooks": cfg.ServiceSettings.EnableIncomingWebhooks, - "enable_outgoing_webhooks": cfg.ServiceSettings.EnableOutgoingWebhooks, - "enable_commands": *cfg.ServiceSettings.EnableCommands, - "enable_only_admin_integrations": *cfg.ServiceSettings.EnableOnlyAdminIntegrations, - "enable_post_username_override": cfg.ServiceSettings.EnablePostUsernameOverride, - "enable_post_icon_override": cfg.ServiceSettings.EnablePostIconOverride, - "enable_apiv3": *cfg.ServiceSettings.EnableAPIv3, - "enable_user_access_tokens": *cfg.ServiceSettings.EnableUserAccessTokens, - "enable_custom_emoji": *cfg.ServiceSettings.EnableCustomEmoji, - "enable_emoji_picker": *cfg.ServiceSettings.EnableEmojiPicker, - "experimental_enable_authentication_transfer": *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer, - "restrict_custom_emoji_creation": *cfg.ServiceSettings.RestrictCustomEmojiCreation, - "enable_testing": cfg.ServiceSettings.EnableTesting, - "enable_developer": *cfg.ServiceSettings.EnableDeveloper, - "enable_multifactor_authentication": *cfg.ServiceSettings.EnableMultifactorAuthentication, - "enforce_multifactor_authentication": *cfg.ServiceSettings.EnforceMultifactorAuthentication, - "enable_oauth_service_provider": cfg.ServiceSettings.EnableOAuthServiceProvider, - "connection_security": *cfg.ServiceSettings.ConnectionSecurity, - "uses_letsencrypt": *cfg.ServiceSettings.UseLetsEncrypt, - "forward_80_to_443": *cfg.ServiceSettings.Forward80To443, - "maximum_login_attempts": *cfg.ServiceSettings.MaximumLoginAttempts, - "session_length_web_in_days": *cfg.ServiceSettings.SessionLengthWebInDays, - "session_length_mobile_in_days": *cfg.ServiceSettings.SessionLengthMobileInDays, - "session_length_sso_in_days": *cfg.ServiceSettings.SessionLengthSSOInDays, - "session_cache_in_minutes": *cfg.ServiceSettings.SessionCacheInMinutes, - "session_idle_timeout_in_minutes": *cfg.ServiceSettings.SessionIdleTimeoutInMinutes, - "isdefault_site_url": isDefault(*cfg.ServiceSettings.SiteURL, model.SERVICE_SETTINGS_DEFAULT_SITE_URL), - "isdefault_tls_cert_file": isDefault(*cfg.ServiceSettings.TLSCertFile, model.SERVICE_SETTINGS_DEFAULT_TLS_CERT_FILE), - "isdefault_tls_key_file": isDefault(*cfg.ServiceSettings.TLSKeyFile, model.SERVICE_SETTINGS_DEFAULT_TLS_KEY_FILE), - "isdefault_read_timeout": isDefault(*cfg.ServiceSettings.ReadTimeout, model.SERVICE_SETTINGS_DEFAULT_READ_TIMEOUT), - "isdefault_write_timeout": isDefault(*cfg.ServiceSettings.WriteTimeout, model.SERVICE_SETTINGS_DEFAULT_WRITE_TIMEOUT), - "isdefault_google_developer_key": isDefault(cfg.ServiceSettings.GoogleDeveloperKey, ""), - "isdefault_allow_cors_from": isDefault(*cfg.ServiceSettings.AllowCorsFrom, model.SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM), - "isdefault_allowed_untrusted_internal_connections": isDefault(*cfg.ServiceSettings.AllowedUntrustedInternalConnections, ""), - "restrict_post_delete": *cfg.ServiceSettings.RestrictPostDelete, - "allow_edit_post": *cfg.ServiceSettings.AllowEditPost, - "post_edit_time_limit": *cfg.ServiceSettings.PostEditTimeLimit, - "enable_user_typing_messages": *cfg.ServiceSettings.EnableUserTypingMessages, - "enable_channel_viewed_messages": *cfg.ServiceSettings.EnableChannelViewedMessages, - "time_between_user_typing_updates_milliseconds": *cfg.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, - "cluster_log_timeout_milliseconds": *cfg.ServiceSettings.ClusterLogTimeoutMilliseconds, - "enable_post_search": *cfg.ServiceSettings.EnablePostSearch, - "enable_user_statuses": *cfg.ServiceSettings.EnableUserStatuses, - "close_unused_direct_messages": *cfg.ServiceSettings.CloseUnusedDirectMessages, - "enable_preview_features": *cfg.ServiceSettings.EnablePreviewFeatures, - "enable_tutorial": *cfg.ServiceSettings.EnableTutorial, + a.SendDiagnostic(TRACK_CONFIG_SERVICE, map[string]interface{}{ + "web_server_mode": *cfg.ServiceSettings.WebserverMode, + "enable_security_fix_alert": *cfg.ServiceSettings.EnableSecurityFixAlert, + "enable_insecure_outgoing_connections": *cfg.ServiceSettings.EnableInsecureOutgoingConnections, + "enable_incoming_webhooks": cfg.ServiceSettings.EnableIncomingWebhooks, + "enable_outgoing_webhooks": cfg.ServiceSettings.EnableOutgoingWebhooks, + "enable_commands": *cfg.ServiceSettings.EnableCommands, + "enable_only_admin_integrations": *cfg.ServiceSettings.EnableOnlyAdminIntegrations, + "enable_post_username_override": cfg.ServiceSettings.EnablePostUsernameOverride, + "enable_post_icon_override": cfg.ServiceSettings.EnablePostIconOverride, + "enable_apiv3": *cfg.ServiceSettings.EnableAPIv3, + "enable_user_access_tokens": *cfg.ServiceSettings.EnableUserAccessTokens, + "enable_custom_emoji": *cfg.ServiceSettings.EnableCustomEmoji, + "enable_emoji_picker": *cfg.ServiceSettings.EnableEmojiPicker, + "experimental_enable_authentication_transfer": *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer, + "restrict_custom_emoji_creation": *cfg.ServiceSettings.RestrictCustomEmojiCreation, + "enable_testing": cfg.ServiceSettings.EnableTesting, + "enable_developer": *cfg.ServiceSettings.EnableDeveloper, + "enable_multifactor_authentication": *cfg.ServiceSettings.EnableMultifactorAuthentication, + "enforce_multifactor_authentication": *cfg.ServiceSettings.EnforceMultifactorAuthentication, + "enable_oauth_service_provider": cfg.ServiceSettings.EnableOAuthServiceProvider, + "connection_security": *cfg.ServiceSettings.ConnectionSecurity, + "uses_letsencrypt": *cfg.ServiceSettings.UseLetsEncrypt, + "forward_80_to_443": *cfg.ServiceSettings.Forward80To443, + "maximum_login_attempts": *cfg.ServiceSettings.MaximumLoginAttempts, + "session_length_web_in_days": *cfg.ServiceSettings.SessionLengthWebInDays, + "session_length_mobile_in_days": *cfg.ServiceSettings.SessionLengthMobileInDays, + "session_length_sso_in_days": *cfg.ServiceSettings.SessionLengthSSOInDays, + "session_cache_in_minutes": *cfg.ServiceSettings.SessionCacheInMinutes, + "session_idle_timeout_in_minutes": *cfg.ServiceSettings.SessionIdleTimeoutInMinutes, + "isdefault_site_url": isDefault(*cfg.ServiceSettings.SiteURL, model.SERVICE_SETTINGS_DEFAULT_SITE_URL), + "isdefault_tls_cert_file": isDefault(*cfg.ServiceSettings.TLSCertFile, model.SERVICE_SETTINGS_DEFAULT_TLS_CERT_FILE), + "isdefault_tls_key_file": isDefault(*cfg.ServiceSettings.TLSKeyFile, model.SERVICE_SETTINGS_DEFAULT_TLS_KEY_FILE), + "isdefault_read_timeout": isDefault(*cfg.ServiceSettings.ReadTimeout, model.SERVICE_SETTINGS_DEFAULT_READ_TIMEOUT), + "isdefault_write_timeout": isDefault(*cfg.ServiceSettings.WriteTimeout, model.SERVICE_SETTINGS_DEFAULT_WRITE_TIMEOUT), + "isdefault_google_developer_key": isDefault(cfg.ServiceSettings.GoogleDeveloperKey, ""), + "isdefault_allow_cors_from": isDefault(*cfg.ServiceSettings.AllowCorsFrom, model.SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM), + "isdefault_allowed_untrusted_internal_connections": isDefault(*cfg.ServiceSettings.AllowedUntrustedInternalConnections, ""), + "restrict_post_delete": *cfg.ServiceSettings.RestrictPostDelete, + "allow_edit_post": *cfg.ServiceSettings.AllowEditPost, + "post_edit_time_limit": *cfg.ServiceSettings.PostEditTimeLimit, + "enable_user_typing_messages": *cfg.ServiceSettings.EnableUserTypingMessages, + "enable_channel_viewed_messages": *cfg.ServiceSettings.EnableChannelViewedMessages, + "time_between_user_typing_updates_milliseconds": *cfg.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, + "cluster_log_timeout_milliseconds": *cfg.ServiceSettings.ClusterLogTimeoutMilliseconds, + "enable_post_search": *cfg.ServiceSettings.EnablePostSearch, + "enable_user_statuses": *cfg.ServiceSettings.EnableUserStatuses, + "close_unused_direct_messages": *cfg.ServiceSettings.CloseUnusedDirectMessages, + "enable_preview_features": *cfg.ServiceSettings.EnablePreviewFeatures, + "enable_tutorial": *cfg.ServiceSettings.EnableTutorial, + "experimental_enable_default_channel_leave_join_messages": *cfg.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages, }) - SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{ "enable_user_creation": cfg.TeamSettings.EnableUserCreation, "enable_team_creation": cfg.TeamSettings.EnableTeamCreation, "restrict_team_invite": *cfg.TeamSettings.RestrictTeamInvite, @@ -269,7 +270,7 @@ func (a *App) trackConfig() { "experimental_primary_team": isDefault(*cfg.TeamSettings.ExperimentalPrimaryTeam, ""), }) - SendDiagnostic(TRACK_CONFIG_CLIENT_REQ, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_CLIENT_REQ, map[string]interface{}{ "android_latest_version": cfg.ClientRequirements.AndroidLatestVersion, "android_min_version": cfg.ClientRequirements.AndroidMinVersion, "desktop_latest_version": cfg.ClientRequirements.DesktopLatestVersion, @@ -278,7 +279,7 @@ func (a *App) trackConfig() { "ios_min_version": cfg.ClientRequirements.IosMinVersion, }) - SendDiagnostic(TRACK_CONFIG_SQL, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_SQL, map[string]interface{}{ "driver_name": *cfg.SqlSettings.DriverName, "trace": cfg.SqlSettings.Trace, "max_idle_conns": *cfg.SqlSettings.MaxIdleConns, @@ -288,7 +289,7 @@ func (a *App) trackConfig() { "query_timeout": *cfg.SqlSettings.QueryTimeout, }) - SendDiagnostic(TRACK_CONFIG_LOG, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_LOG, map[string]interface{}{ "enable_console": cfg.LogSettings.EnableConsole, "console_level": cfg.LogSettings.ConsoleLevel, "enable_file": cfg.LogSettings.EnableFile, @@ -298,7 +299,7 @@ func (a *App) trackConfig() { "isdefault_file_location": isDefault(cfg.LogSettings.FileLocation, ""), }) - SendDiagnostic(TRACK_CONFIG_PASSWORD, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_PASSWORD, map[string]interface{}{ "minimum_length": *cfg.PasswordSettings.MinimumLength, "lowercase": *cfg.PasswordSettings.Lowercase, "number": *cfg.PasswordSettings.Number, @@ -306,7 +307,7 @@ func (a *App) trackConfig() { "symbol": *cfg.PasswordSettings.Symbol, }) - SendDiagnostic(TRACK_CONFIG_FILE, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_FILE, map[string]interface{}{ "enable_public_links": cfg.FileSettings.EnablePublicLink, "driver_name": *cfg.FileSettings.DriverName, "amazon_s3_ssl": *cfg.FileSettings.AmazonS3SSL, @@ -319,7 +320,7 @@ func (a *App) trackConfig() { "enable_mobile_download": *cfg.FileSettings.EnableMobileDownload, }) - SendDiagnostic(TRACK_CONFIG_EMAIL, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_EMAIL, map[string]interface{}{ "enable_sign_up_with_email": cfg.EmailSettings.EnableSignUpWithEmail, "enable_sign_in_with_email": *cfg.EmailSettings.EnableSignInWithEmail, "enable_sign_in_with_username": *cfg.EmailSettings.EnableSignInWithUsername, @@ -343,7 +344,7 @@ func (a *App) trackConfig() { "isdefault_login_button_text_color": isDefault(*cfg.EmailSettings.LoginButtonTextColor, ""), }) - SendDiagnostic(TRACK_CONFIG_RATE, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_RATE, map[string]interface{}{ "enable_rate_limiter": *cfg.RateLimitSettings.Enable, "vary_by_remote_address": cfg.RateLimitSettings.VaryByRemoteAddr, "per_sec": *cfg.RateLimitSettings.PerSec, @@ -352,25 +353,25 @@ func (a *App) trackConfig() { "isdefault_vary_by_header": isDefault(cfg.RateLimitSettings.VaryByHeader, ""), }) - SendDiagnostic(TRACK_CONFIG_PRIVACY, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_PRIVACY, map[string]interface{}{ "show_email_address": cfg.PrivacySettings.ShowEmailAddress, "show_full_name": cfg.PrivacySettings.ShowFullName, }) - SendDiagnostic(TRACK_CONFIG_THEME, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_THEME, map[string]interface{}{ "enable_theme_selection": *cfg.ThemeSettings.EnableThemeSelection, "isdefault_default_theme": isDefault(*cfg.ThemeSettings.DefaultTheme, model.TEAM_SETTINGS_DEFAULT_TEAM_TEXT), "allow_custom_themes": *cfg.ThemeSettings.AllowCustomThemes, "allowed_themes": len(cfg.ThemeSettings.AllowedThemes), }) - SendDiagnostic(TRACK_CONFIG_OAUTH, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_OAUTH, map[string]interface{}{ "enable_gitlab": cfg.GitLabSettings.Enable, "enable_google": cfg.GoogleSettings.Enable, "enable_office365": cfg.Office365Settings.Enable, }) - SendDiagnostic(TRACK_CONFIG_SUPPORT, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_SUPPORT, map[string]interface{}{ "isdefault_terms_of_service_link": isDefault(*cfg.SupportSettings.TermsOfServiceLink, model.SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK), "isdefault_privacy_policy_link": isDefault(*cfg.SupportSettings.PrivacyPolicyLink, model.SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK), "isdefault_about_link": isDefault(*cfg.SupportSettings.AboutLink, model.SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK), @@ -379,7 +380,7 @@ func (a *App) trackConfig() { "isdefault_support_email": isDefault(*cfg.SupportSettings.SupportEmail, model.SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL), }) - SendDiagnostic(TRACK_CONFIG_LDAP, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_LDAP, map[string]interface{}{ "enable": *cfg.LdapSettings.Enable, "enable_sync": *cfg.LdapSettings.EnableSync, "connection_security": *cfg.LdapSettings.ConnectionSecurity, @@ -400,18 +401,18 @@ func (a *App) trackConfig() { "isdefault_login_button_text_color": isDefault(*cfg.LdapSettings.LoginButtonTextColor, ""), }) - SendDiagnostic(TRACK_CONFIG_COMPLIANCE, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_COMPLIANCE, map[string]interface{}{ "enable": *cfg.ComplianceSettings.Enable, "enable_daily": *cfg.ComplianceSettings.EnableDaily, }) - SendDiagnostic(TRACK_CONFIG_LOCALIZATION, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_LOCALIZATION, map[string]interface{}{ "default_server_locale": *cfg.LocalizationSettings.DefaultServerLocale, "default_client_locale": *cfg.LocalizationSettings.DefaultClientLocale, "available_locales": *cfg.LocalizationSettings.AvailableLocales, }) - SendDiagnostic(TRACK_CONFIG_SAML, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_SAML, map[string]interface{}{ "enable": *cfg.SamlSettings.Enable, "enable_sync_with_ldap": *cfg.SamlSettings.EnableSyncWithLdap, "verify": *cfg.SamlSettings.Verify, @@ -429,42 +430,42 @@ func (a *App) trackConfig() { "isdefault_login_button_text_color": isDefault(*cfg.SamlSettings.LoginButtonTextColor, ""), }) - SendDiagnostic(TRACK_CONFIG_CLUSTER, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_CLUSTER, map[string]interface{}{ "enable": *cfg.ClusterSettings.Enable, "use_ip_address": *cfg.ClusterSettings.UseIpAddress, "use_experimental_gossip": *cfg.ClusterSettings.UseExperimentalGossip, "read_only_config": *cfg.ClusterSettings.ReadOnlyConfig, }) - SendDiagnostic(TRACK_CONFIG_METRICS, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_METRICS, map[string]interface{}{ "enable": *cfg.MetricsSettings.Enable, "block_profile_rate": *cfg.MetricsSettings.BlockProfileRate, }) - SendDiagnostic(TRACK_CONFIG_NATIVEAPP, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_NATIVEAPP, map[string]interface{}{ "isdefault_app_download_link": isDefault(*cfg.NativeAppSettings.AppDownloadLink, model.NATIVEAPP_SETTINGS_DEFAULT_APP_DOWNLOAD_LINK), "isdefault_android_app_download_link": isDefault(*cfg.NativeAppSettings.AndroidAppDownloadLink, model.NATIVEAPP_SETTINGS_DEFAULT_ANDROID_APP_DOWNLOAD_LINK), "isdefault_iosapp_download_link": isDefault(*cfg.NativeAppSettings.IosAppDownloadLink, model.NATIVEAPP_SETTINGS_DEFAULT_IOS_APP_DOWNLOAD_LINK), }) - SendDiagnostic(TRACK_CONFIG_WEBRTC, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_WEBRTC, map[string]interface{}{ "enable": *cfg.WebrtcSettings.Enable, "isdefault_stun_uri": isDefault(*cfg.WebrtcSettings.StunURI, model.WEBRTC_SETTINGS_DEFAULT_STUN_URI), "isdefault_turn_uri": isDefault(*cfg.WebrtcSettings.TurnURI, model.WEBRTC_SETTINGS_DEFAULT_TURN_URI), }) - SendDiagnostic(TRACK_CONFIG_ANALYTICS, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_ANALYTICS, map[string]interface{}{ "isdefault_max_users_for_statistics": isDefault(*cfg.AnalyticsSettings.MaxUsersForStatistics, model.ANALYTICS_SETTINGS_DEFAULT_MAX_USERS_FOR_STATISTICS), }) - SendDiagnostic(TRACK_CONFIG_ANNOUNCEMENT, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_ANNOUNCEMENT, map[string]interface{}{ "enable_banner": *cfg.AnnouncementSettings.EnableBanner, "isdefault_banner_color": isDefault(*cfg.AnnouncementSettings.BannerColor, model.ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_COLOR), "isdefault_banner_text_color": isDefault(*cfg.AnnouncementSettings.BannerTextColor, model.ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_TEXT_COLOR), "allow_banner_dismissal": *cfg.AnnouncementSettings.AllowBannerDismissal, }) - SendDiagnostic(TRACK_CONFIG_ELASTICSEARCH, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_ELASTICSEARCH, map[string]interface{}{ "isdefault_connection_url": isDefault(*cfg.ElasticsearchSettings.ConnectionUrl, model.ELASTICSEARCH_SETTINGS_DEFAULT_CONNECTION_URL), "isdefault_username": isDefault(*cfg.ElasticsearchSettings.Username, model.ELASTICSEARCH_SETTINGS_DEFAULT_USERNAME), "isdefault_password": isDefault(*cfg.ElasticsearchSettings.Password, model.ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD), @@ -479,14 +480,14 @@ func (a *App) trackConfig() { "request_timeout_seconds": *cfg.ElasticsearchSettings.RequestTimeoutSeconds, }) - SendDiagnostic(TRACK_CONFIG_PLUGIN, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_PLUGIN, map[string]interface{}{ "enable_jira": pluginSetting(&cfg.PluginSettings, "jira", "enabled", false), "enable_zoom": pluginActivated(cfg.PluginSettings.PluginStates, "zoom"), "enable": *cfg.PluginSettings.Enable, "enable_uploads": *cfg.PluginSettings.EnableUploads, }) - SendDiagnostic(TRACK_CONFIG_DATA_RETENTION, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_DATA_RETENTION, map[string]interface{}{ "enable_message_deletion": *cfg.DataRetentionSettings.EnableMessageDeletion, "enable_file_deletion": *cfg.DataRetentionSettings.EnableFileDeletion, "message_retention_days": *cfg.DataRetentionSettings.MessageRetentionDays, @@ -494,7 +495,7 @@ func (a *App) trackConfig() { "deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime, }) - SendDiagnostic(TRACK_CONFIG_MESSAGE_EXPORT, map[string]interface{}{ + a.SendDiagnostic(TRACK_CONFIG_MESSAGE_EXPORT, map[string]interface{}{ "enable_message_export": *cfg.MessageExportSettings.EnableExport, "daily_run_time": *cfg.MessageExportSettings.DailyRunTime, "default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp, @@ -502,7 +503,7 @@ func (a *App) trackConfig() { }) } -func trackLicense() { +func (a *App) trackLicense() { if utils.IsLicensed() { data := map[string]interface{}{ "customer_id": utils.License().Customer.Id, @@ -518,7 +519,7 @@ func trackLicense() { data["feature_"+featureName] = featureValue } - SendDiagnostic(TRACK_LICENSE, data) + a.SendDiagnostic(TRACK_LICENSE, data) } } @@ -568,7 +569,7 @@ func (a *App) trackPlugins() { } } - SendDiagnostic(TRACK_PLUGINS, map[string]interface{}{ + a.SendDiagnostic(TRACK_PLUGINS, map[string]interface{}{ "active_plugins": totalActiveCount, "active_webapp_plugins": webappActiveCount, "active_backend_plugins": backendActiveCount, @@ -592,5 +593,5 @@ func (a *App) trackServer() { data["system_admins"] = scr.Data.(int64) } - SendDiagnostic(TRACK_SERVER, data) + a.SendDiagnostic(TRACK_SERVER, data) } diff --git a/app/diagnostics_test.go b/app/diagnostics_test.go index 0d1c7acd8..869e5ddc6 100644 --- a/app/diagnostics_test.go +++ b/app/diagnostics_test.go @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) func newTestServer() (chan string, *httptest.Server) { @@ -68,18 +67,15 @@ func TestDiagnostics(t *testing.T) { data, server := newTestServer() defer server.Close() - oldId := utils.CfgDiagnosticId - utils.CfgDiagnosticId = "i am not real" - defer func() { - utils.CfgDiagnosticId = oldId - }() - initDiagnostics(server.URL) + diagnosticId := "i am not real" + th.App.SetDiagnosticId(diagnosticId) + th.App.initDiagnostics(server.URL) // Should send a client identify message select { case identifyMessage := <-data: t.Log("Got idmessage:\n" + identifyMessage) - if !strings.Contains(identifyMessage, utils.CfgDiagnosticId) { + if !strings.Contains(identifyMessage, diagnosticId) { t.Fail() } case <-time.After(time.Second * 1): @@ -88,7 +84,7 @@ func TestDiagnostics(t *testing.T) { t.Run("Send", func(t *testing.T) { const TEST_VALUE = "stuff548959847" - SendDiagnostic("Testing Diagnostic", map[string]interface{}{ + th.App.SendDiagnostic("Testing Diagnostic", map[string]interface{}{ "hey": TEST_VALUE, }) select { @@ -159,9 +155,7 @@ func TestDiagnostics(t *testing.T) { }) t.Run("SendDailyDiagnosticsDisabled", func(t *testing.T) { - oldSetting := *th.App.Config().LogSettings.EnableDiagnostics th.App.UpdateConfig(func(cfg *model.Config) { *cfg.LogSettings.EnableDiagnostics = false }) - defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.LogSettings.EnableDiagnostics = oldSetting }) th.App.SendDailyDiagnostics() diff --git a/app/email.go b/app/email.go index 867411960..b809b972d 100644 --- a/app/email.go +++ b/app/email.go @@ -20,7 +20,7 @@ func (a *App) SendChangeUsernameEmail(oldUsername, newUsername, email, locale, s T := utils.GetUserTranslations(locale) subject := T("api.templates.username_change_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "TeamDisplayName": a.Config().TeamSettings.SiteName}) bodyPage := a.NewEmailTemplate("email_change_body", locale) @@ -42,7 +42,7 @@ func (a *App) SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token st link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token, url.QueryEscape(newUserEmail)) subject := T("api.templates.email_change_verify_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "TeamDisplayName": a.Config().TeamSettings.SiteName}) bodyPage := a.NewEmailTemplate("email_change_verify_body", locale) @@ -64,7 +64,7 @@ func (a *App) SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) * T := utils.GetUserTranslations(locale) subject := T("api.templates.email_change_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "TeamDisplayName": a.Config().TeamSettings.SiteName}) bodyPage := a.NewEmailTemplate("email_change_body", locale) @@ -88,7 +88,7 @@ func (a *App) SendVerifyEmail(userEmail, locale, siteURL, token string) *model.A url, _ := url.Parse(siteURL) subject := T("api.templates.verify_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"]}) bodyPage := a.NewEmailTemplate("verify_body", locale) bodyPage.Props["SiteURL"] = siteURL @@ -108,13 +108,13 @@ func (a *App) SendSignInChangeEmail(email, method, locale, siteURL string) *mode T := utils.GetUserTranslations(locale) subject := T("api.templates.signin_change_email.subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"]}) bodyPage := a.NewEmailTemplate("signin_change_body", locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = T("api.templates.signin_change_email.body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.signin_change_email.body.info", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "Method": method}) + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "Method": method}) if err := a.SendMail(email, subject, bodyPage.Render()); err != nil { return model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) @@ -129,7 +129,7 @@ func (a *App) SendWelcomeEmail(userId string, email string, verified bool, local rawUrl, _ := url.Parse(siteURL) subject := T("api.templates.welcome_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "ServerURL": rawUrl.Host}) bodyPage := a.NewEmailTemplate("welcome_body", locale) @@ -166,7 +166,7 @@ func (a *App) SendPasswordChangeEmail(email, method, locale, siteURL string) *mo T := utils.GetUserTranslations(locale) subject := T("api.templates.password_change_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "TeamDisplayName": a.Config().TeamSettings.SiteName}) bodyPage := a.NewEmailTemplate("password_change_body", locale) @@ -186,12 +186,12 @@ func (a *App) SendUserAccessTokenAddedEmail(email, locale string) *model.AppErro T := utils.GetUserTranslations(locale) subject := T("api.templates.user_access_token_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"]}) bodyPage := a.NewEmailTemplate("password_change_body", locale) bodyPage.Props["Title"] = T("api.templates.user_access_token_body.title") bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.user_access_token_body.info", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "SiteURL": utils.GetSiteURL()}) + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "SiteURL": utils.GetSiteURL()}) if err := a.SendMail(email, subject, bodyPage.Render()); err != nil { return model.NewAppError("SendUserAccessTokenAddedEmail", "api.user.send_user_access_token.error", nil, err.Error(), http.StatusInternalServerError) @@ -207,7 +207,7 @@ func (a *App) SendPasswordResetEmail(email string, token *model.Token, locale, s link := fmt.Sprintf("%s/reset_password_complete?token=%s", siteURL, url.QueryEscape(token.Token)) subject := T("api.templates.reset_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"]}) bodyPage := a.NewEmailTemplate("reset_body", locale) bodyPage.Props["SiteURL"] = siteURL @@ -227,7 +227,7 @@ func (a *App) SendMfaChangeEmail(email string, activated bool, locale, siteURL s T := utils.GetUserTranslations(locale) subject := T("api.templates.mfa_change_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"]}) bodyPage := a.NewEmailTemplate("mfa_change_body", locale) bodyPage.Props["SiteURL"] = siteURL @@ -258,7 +258,7 @@ func (a *App) SendInviteEmails(team *model.Team, senderName string, invites []st subject := utils.T("api.templates.invite_subject", map[string]interface{}{"SenderName": senderName, "TeamDisplayName": team.DisplayName, - "SiteName": utils.ClientCfg["SiteName"]}) + "SiteName": a.ClientConfig()["SiteName"]}) bodyPage := a.NewEmailTemplate("invite_body", model.DEFAULT_LOCALE) bodyPage.Props["SiteURL"] = siteURL diff --git a/app/emoji.go b/app/emoji.go index f62a8686b..2271d650d 100644 --- a/app/emoji.go +++ b/app/emoji.go @@ -66,8 +66,8 @@ func (a *App) CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartIma } } -func (a *App) GetEmojiList(page, perPage int) ([]*model.Emoji, *model.AppError) { - if result := <-a.Srv.Store.Emoji().GetList(page*perPage, perPage); result.Err != nil { +func (a *App) GetEmojiList(page, perPage int, sort string) ([]*model.Emoji, *model.AppError) { + if result := <-a.Srv.Store.Emoji().GetList(page*perPage, perPage, sort); result.Err != nil { return nil, result.Err } else { return result.Data.([]*model.Emoji), nil @@ -134,11 +134,11 @@ func (a *App) DeleteEmoji(emoji *model.Emoji) *model.AppError { func (a *App) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) { if !*a.Config().ServiceSettings.EnableCustomEmoji { - return nil, model.NewAppError("deleteEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented) + return nil, model.NewAppError("GetEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented) } if len(*a.Config().FileSettings.DriverName) == 0 { - return nil, model.NewAppError("deleteImage", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented) + return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented) } if result := <-a.Srv.Store.Emoji().Get(emojiId, false); result.Err != nil { @@ -169,6 +169,18 @@ func (a *App) GetEmojiImage(emojiId string) (imageByte []byte, imageType string, } } +func (a *App) SearchEmoji(name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) { + if !*a.Config().ServiceSettings.EnableCustomEmoji { + return nil, model.NewAppError("SearchEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-a.Srv.Store.Emoji().Search(name, prefixOnly, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.Emoji), nil + } +} + func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF { // Create a new RGBA image to hold the incremental frames. firstFrame := gifImg.Image[0].Bounds() diff --git a/app/import.go b/app/import.go index 850e9c43d..6291794b0 100644 --- a/app/import.go +++ b/app/import.go @@ -9,6 +9,7 @@ import ( "encoding/json" "io" "net/http" + "os" "regexp" "strings" "sync" @@ -53,17 +54,18 @@ type ChannelImportData struct { } type UserImportData struct { - Username *string `json:"username"` - Email *string `json:"email"` - AuthService *string `json:"auth_service"` - AuthData *string `json:"auth_data"` - Password *string `json:"password"` - Nickname *string `json:"nickname"` - FirstName *string `json:"first_name"` - LastName *string `json:"last_name"` - Position *string `json:"position"` - Roles *string `json:"roles"` - Locale *string `json:"locale"` + ProfileImage *string `json:"profile_image"` + Username *string `json:"username"` + Email *string `json:"email"` + AuthService *string `json:"auth_service"` + AuthData *string `json:"auth_data"` + Password *string `json:"password"` + Nickname *string `json:"nickname"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + Position *string `json:"position"` + Roles *string `json:"roles"` + Locale *string `json:"locale"` Teams *[]UserTeamImportData `json:"teams"` @@ -111,6 +113,22 @@ type UserChannelNotifyPropsImportData struct { MarkUnread *string `json:"mark_unread"` } +type ReactionImportData struct { + User *string `json:"user"` + CreateAt *int64 `json:"create_at"` + EmojiName *string `json:"emoji_name"` +} + +type ReplyImportData struct { + User *string `json:"user"` + + Message *string `json:"message"` + CreateAt *int64 `json:"create_at"` + + FlaggedBy *[]string `json:"flagged_by"` + Reactions *[]ReactionImportData `json:"reactions"` +} + type PostImportData struct { Team *string `json:"team"` Channel *string `json:"channel"` @@ -119,7 +137,9 @@ type PostImportData struct { Message *string `json:"message"` CreateAt *int64 `json:"create_at"` - FlaggedBy *[]string `json:"flagged_by"` + FlaggedBy *[]string `json:"flagged_by"` + Reactions *[]ReactionImportData `json:"reactions"` + Replies *[]ReplyImportData `json:"replies"` } type DirectChannelImportData struct { @@ -136,7 +156,9 @@ type DirectPostImportData struct { Message *string `json:"message"` CreateAt *int64 `json:"create_at"` - FlaggedBy *[]string `json:"flagged_by"` + FlaggedBy *[]string `json:"flagged_by"` + Reactions *[]ReactionImportData `json:"reactions"` + Replies *[]ReplyImportData `json:"replies"` } type LineImportWorkerData struct { @@ -690,6 +712,16 @@ func (a *App) ImportUser(data *UserImportData, dryRun bool) *model.AppError { savedUser = user } + if data.ProfileImage != nil { + file, err := os.Open(*data.ProfileImage) + if err != nil { + l4g.Error(utils.T("api.import.import_user.profile_image.error"), err) + } + if err := a.SetProfileImageFromFile(savedUser.Id, file); err != nil { + l4g.Error(utils.T("api.import.import_user.profile_image.error"), err) + } + } + // Preferences. var preferences model.Preferences @@ -869,6 +901,11 @@ func (a *App) ImportUserChannels(user *model.User, team *model.Team, teamMember } func validateUserImportData(data *UserImportData) *model.AppError { + if data.ProfileImage != nil { + if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusBadRequest) + } + } if data.Username == nil { return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_missing.error", nil, "", http.StatusBadRequest) @@ -1019,6 +1056,79 @@ func validateUserChannelsImportData(data *[]UserChannelImportData) *model.AppErr return nil } +func (a *App) ImportReaction(data *ReactionImportData, post *model.Post, dryRun bool) *model.AppError { + if err := validateReactionImportData(data, post.CreateAt); err != nil { + return err + } + + var user *model.User + if result := <-a.Srv.Store.User().GetByUsername(*data.User); result.Err != nil { + return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": data.User}, "", http.StatusBadRequest) + } else { + user = result.Data.(*model.User) + } + reaction := &model.Reaction{ + UserId: user.Id, + PostId: post.Id, + EmojiName: *data.EmojiName, + CreateAt: *data.CreateAt, + } + if result := <-a.Srv.Store.Reaction().Save(reaction); result.Err != nil { + return result.Err + } + return nil +} + +func (a *App) ImportReply(data *ReplyImportData, post *model.Post, dryRun bool) *model.AppError { + if err := validateReplyImportData(data, post.CreateAt); err != nil { + return err + } + + var user *model.User + if result := <-a.Srv.Store.User().GetByUsername(*data.User); result.Err != nil { + return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": data.User}, "", http.StatusBadRequest) + } else { + user = result.Data.(*model.User) + } + + // Check if this post already exists. + var replies []*model.Post + if result := <-a.Srv.Store.Post().GetPostsCreatedAt(post.ChannelId, *data.CreateAt); result.Err != nil { + return result.Err + } else { + replies = result.Data.([]*model.Post) + } + + var reply *model.Post + for _, r := range replies { + if r.Message == *data.Message { + reply = r + break + } + } + + if reply == nil { + reply = &model.Post{} + } + reply.UserId = user.Id + reply.ChannelId = post.ChannelId + reply.ParentId = post.Id + reply.RootId = post.Id + reply.Message = *data.Message + reply.CreateAt = *data.CreateAt + + if reply.Id == "" { + if result := <-a.Srv.Store.Post().Save(reply); result.Err != nil { + return result.Err + } + } else { + if result := <-a.Srv.Store.Post().Overwrite(reply); result.Err != nil { + return result.Err + } + } + return nil +} + func (a *App) ImportPost(data *PostImportData, dryRun bool) *model.AppError { if err := validatePostImportData(data); err != nil { return err @@ -1114,6 +1224,66 @@ func (a *App) ImportPost(data *PostImportData, dryRun bool) *model.AppError { } } + if data.Reactions != nil { + for _, reaction := range *data.Reactions { + if err := a.ImportReaction(&reaction, post, dryRun); err != nil { + return err + } + } + } + + if data.Replies != nil { + for _, reply := range *data.Replies { + if err := a.ImportReply(&reply, post, dryRun); err != nil { + return err + } + } + } + + return nil +} + +func validateReactionImportData(data *ReactionImportData, parentCreateAt int64) *model.AppError { + if data.User == nil { + return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.user_missing.error", nil, "", http.StatusBadRequest) + } + + if data.EmojiName == nil { + return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.emoji_name_missing.error", nil, "", http.StatusBadRequest) + } else if utf8.RuneCountInString(*data.EmojiName) > model.EMOJI_NAME_MAX_LENGTH { + return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.emoji_name_length.error", nil, "", http.StatusBadRequest) + } + + if data.CreateAt == nil { + return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_missing.error", nil, "", http.StatusBadRequest) + } else if *data.CreateAt == 0 { + return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_zero.error", nil, "", http.StatusBadRequest) + } else if *data.CreateAt < parentCreateAt { + return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_before_parent.error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func validateReplyImportData(data *ReplyImportData, parentCreateAt int64) *model.AppError { + if data.User == nil { + return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.user_missing.error", nil, "", http.StatusBadRequest) + } + + if data.Message == nil { + return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.message_missing.error", nil, "", http.StatusBadRequest) + } else if utf8.RuneCountInString(*data.Message) > model.POST_MESSAGE_MAX_RUNES { + return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.message_length.error", nil, "", http.StatusBadRequest) + } + + if data.CreateAt == nil { + return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_missing.error", nil, "", http.StatusBadRequest) + } else if *data.CreateAt == 0 { + return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_zero.error", nil, "", http.StatusBadRequest) + } else if *data.CreateAt < parentCreateAt { + return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_before_parent.error", nil, "", http.StatusBadRequest) + } + return nil } @@ -1142,6 +1312,18 @@ func validatePostImportData(data *PostImportData) *model.AppError { return model.NewAppError("BulkImport", "app.import.validate_post_import_data.create_at_zero.error", nil, "", http.StatusBadRequest) } + if data.Reactions != nil { + for _, reaction := range *data.Reactions { + validateReactionImportData(&reaction, *data.CreateAt) + } + } + + if data.Replies != nil { + for _, reply := range *data.Replies { + validateReplyImportData(&reply, *data.CreateAt) + } + } + return nil } @@ -1365,6 +1547,22 @@ func (a *App) ImportDirectPost(data *DirectPostImportData, dryRun bool) *model.A } } + if data.Reactions != nil { + for _, reaction := range *data.Reactions { + if err := a.ImportReaction(&reaction, post, dryRun); err != nil { + return err + } + } + } + + if data.Replies != nil { + for _, reply := range *data.Replies { + if err := a.ImportReply(&reply, post, dryRun); err != nil { + return err + } + } + } + return nil } @@ -1412,6 +1610,18 @@ func validateDirectPostImportData(data *DirectPostImportData) *model.AppError { } } + if data.Reactions != nil { + for _, reaction := range *data.Reactions { + validateReactionImportData(&reaction, *data.CreateAt) + } + } + + if data.Replies != nil { + for _, reply := range *data.Replies { + validateReplyImportData(&reply, *data.CreateAt) + } + } + return nil } diff --git a/app/import_test.go b/app/import_test.go index 630077603..abe32caa8 100644 --- a/app/import_test.go +++ b/app/import_test.go @@ -10,6 +10,7 @@ import ( "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" + "github.com/mattermost/mattermost-server/utils" ) func ptrStr(s string) *string { @@ -325,6 +326,13 @@ func TestImportValidateUserImportData(t *testing.T) { data.Username = ptrStr("bob") + // Unexisting Picture Image + data.ProfileImage = ptrStr("not-existing-file") + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to not existing profile image file.") + } + data.ProfileImage = nil + // Invalid Emails data.Email = nil if err := validateUserImportData(&data); err == nil { @@ -360,17 +368,19 @@ func TestImportValidateUserImportData(t *testing.T) { } // Test a valid User with all fields populated. + testsDir, _ := utils.FindDir("tests") data = UserImportData{ - Username: ptrStr("bob"), - Email: ptrStr("bob@example.com"), - AuthService: ptrStr("ldap"), - AuthData: ptrStr("bob"), - Nickname: ptrStr("BobNick"), - FirstName: ptrStr("Bob"), - LastName: ptrStr("Blob"), - Position: ptrStr("The Boss"), - Roles: ptrStr("system_user"), - Locale: ptrStr("en"), + ProfileImage: ptrStr(testsDir + "test.png"), + Username: ptrStr("bob"), + Email: ptrStr("bob@example.com"), + AuthService: ptrStr("ldap"), + AuthData: ptrStr("bob"), + Nickname: ptrStr("BobNick"), + FirstName: ptrStr("Bob"), + LastName: ptrStr("Blob"), + Position: ptrStr("The Boss"), + Roles: ptrStr("system_user"), + Locale: ptrStr("en"), } if err := validateUserImportData(&data); err != nil { t.Fatal("Validation failed but should have been valid.") @@ -563,6 +573,140 @@ func TestImportValidateUserChannelsImportData(t *testing.T) { } } +func TestImportValidateReactionImportData(t *testing.T) { + // Test with minimum required valid properties. + parentCreateAt := model.GetMillis() - 100 + data := ReactionImportData{ + User: ptrStr("username"), + EmojiName: ptrStr("emoji"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateReactionImportData(&data, parentCreateAt); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + // Test with missing required properties. + data = ReactionImportData{ + EmojiName: ptrStr("emoji"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateReactionImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + data = ReactionImportData{ + User: ptrStr("username"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateReactionImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + data = ReactionImportData{ + User: ptrStr("username"), + EmojiName: ptrStr("emoji"), + } + if err := validateReactionImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + // Test with invalid emoji name. + data = ReactionImportData{ + User: ptrStr("username"), + EmojiName: ptrStr(strings.Repeat("1234567890", 500)), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateReactionImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to too long emoji name.") + } + + // Test with invalid CreateAt + data = ReactionImportData{ + User: ptrStr("username"), + EmojiName: ptrStr("emoji"), + CreateAt: ptrInt64(0), + } + if err := validateReactionImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to 0 create-at value.") + } + + data = ReactionImportData{ + User: ptrStr("username"), + EmojiName: ptrStr("emoji"), + CreateAt: ptrInt64(parentCreateAt - 100), + } + if err := validateReactionImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due parent with newer create-at value.") + } +} + +func TestImportValidateReplyImportData(t *testing.T) { + // Test with minimum required valid properties. + parentCreateAt := model.GetMillis() - 100 + data := ReplyImportData{ + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateReplyImportData(&data, parentCreateAt); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + // Test with missing required properties. + data = ReplyImportData{ + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateReplyImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + data = ReplyImportData{ + User: ptrStr("username"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateReplyImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + data = ReplyImportData{ + User: ptrStr("username"), + Message: ptrStr("message"), + } + if err := validateReplyImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + // Test with invalid message. + data = ReplyImportData{ + User: ptrStr("username"), + Message: ptrStr(strings.Repeat("1234567890", 500)), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateReplyImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to too long message.") + } + + // Test with invalid CreateAt + data = ReplyImportData{ + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(0), + } + if err := validateReplyImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due to 0 create-at value.") + } + + data = ReplyImportData{ + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(parentCreateAt - 100), + } + if err := validateReplyImportData(&data, parentCreateAt); err == nil { + t.Fatal("Should have failed due parent with newer create-at value.") + } +} + func TestImportValidatePostImportData(t *testing.T) { // Test with minimum required valid properties. @@ -653,12 +797,24 @@ func TestImportValidatePostImportData(t *testing.T) { } // Test with valid all optional parameters. - data = PostImportData{ - Team: ptrStr("teamname"), - Channel: ptrStr("channelname"), + reactions := []ReactionImportData{ReactionImportData{ + User: ptrStr("username"), + EmojiName: ptrStr("emoji"), + CreateAt: ptrInt64(model.GetMillis()), + }} + replies := []ReplyImportData{ReplyImportData{ User: ptrStr("username"), Message: ptrStr("message"), CreateAt: ptrInt64(model.GetMillis()), + }} + data = PostImportData{ + Team: ptrStr("teamname"), + Channel: ptrStr("channelname"), + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + Reactions: &reactions, + Replies: &replies, } if err := validatePostImportData(&data); err != nil { t.Fatal("Should have succeeded.") @@ -961,6 +1117,37 @@ func TestImportValidateDirectPostImportData(t *testing.T) { if err := validateDirectPostImportData(&data); err != nil { t.Fatal(err) } + + // Test with valid all optional parameters. + reactions := []ReactionImportData{ReactionImportData{ + User: ptrStr("username"), + EmojiName: ptrStr("emoji"), + CreateAt: ptrInt64(model.GetMillis()), + }} + replies := []ReplyImportData{ReplyImportData{ + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + }} + data = DirectPostImportData{ + ChannelMembers: &[]string{ + member1, + member2, + }, + FlaggedBy: &[]string{ + member1, + member2, + }, + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + Reactions: &reactions, + Replies: &replies, + } + + if err := validateDirectPostImportData(&data); err != nil { + t.Fatal(err) + } } func TestImportImportTeam(t *testing.T) { @@ -1298,13 +1485,15 @@ func TestImportImportUser(t *testing.T) { // Do a valid user in apply mode. username := model.NewId() + testsDir, _ := utils.FindDir("tests") data = UserImportData{ - Username: &username, - Email: ptrStr(model.NewId() + "@example.com"), - Nickname: ptrStr(model.NewId()), - FirstName: ptrStr(model.NewId()), - LastName: ptrStr(model.NewId()), - Position: ptrStr(model.NewId()), + ProfileImage: ptrStr(testsDir + "test.png"), + Username: &username, + Email: ptrStr(model.NewId() + "@example.com"), + Nickname: ptrStr(model.NewId()), + FirstName: ptrStr(model.NewId()), + LastName: ptrStr(model.NewId()), + Position: ptrStr(model.NewId()), } if err := th.App.ImportUser(&data, false); err != nil { t.Fatalf("Should have succeeded to import valid user.") @@ -1354,6 +1543,7 @@ func TestImportImportUser(t *testing.T) { // Alter all the fields of that user. data.Email = ptrStr(model.NewId() + "@example.com") + data.ProfileImage = ptrStr(testsDir + "testgif.gif") data.AuthService = ptrStr("ldap") data.AuthData = &username data.Nickname = ptrStr(model.NewId()) @@ -2189,6 +2379,98 @@ func TestImportImportPost(t *testing.T) { checkPreference(t, th.App, user.Id, model.PREFERENCE_CATEGORY_FLAGGED_POST, post.Id, "true") checkPreference(t, th.App, user2.Id, model.PREFERENCE_CATEGORY_FLAGGED_POST, post.Id, "true") } + + // Post with reaction. + reactionPostTime := hashtagTime + 2 + reactionTime := hashtagTime + 3 + data = &PostImportData{ + Team: &teamName, + Channel: &channelName, + User: &username, + Message: ptrStr("Message with reaction"), + CreateAt: &reactionPostTime, + Reactions: &[]ReactionImportData{{ + User: &user2.Username, + EmojiName: ptrStr("+1"), + CreateAt: &reactionTime, + }}, + } + if err := th.App.ImportPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, th.App, initialPostCount, 6, team.Id) + + // Check the post values. + if result := <-th.App.Srv.Store.Post().GetPostsCreatedAt(channel.Id, reactionPostTime); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + posts := result.Data.([]*model.Post) + if len(posts) != 1 { + t.Fatal("Unexpected number of posts found.") + } + post := posts[0] + if post.Message != *data.Message || post.CreateAt != *data.CreateAt || post.UserId != user.Id || !post.HasReactions { + t.Fatal("Post properties not as expected") + } + + if result := <-th.App.Srv.Store.Reaction().GetForPost(post.Id, false); result.Err != nil { + t.Fatal("Can't get reaction") + } else if len(result.Data.([]*model.Reaction)) != 1 { + t.Fatal("Invalid number of reactions") + } + } + + // Post with reply. + replyPostTime := hashtagTime + 4 + replyTime := hashtagTime + 5 + data = &PostImportData{ + Team: &teamName, + Channel: &channelName, + User: &username, + Message: ptrStr("Message with reply"), + CreateAt: &replyPostTime, + Replies: &[]ReplyImportData{{ + User: &user2.Username, + Message: ptrStr("Message reply"), + CreateAt: &replyTime, + }}, + } + if err := th.App.ImportPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, th.App, initialPostCount, 8, team.Id) + + // Check the post values. + if result := <-th.App.Srv.Store.Post().GetPostsCreatedAt(channel.Id, replyPostTime); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + posts := result.Data.([]*model.Post) + if len(posts) != 1 { + t.Fatal("Unexpected number of posts found.") + } + post := posts[0] + if post.Message != *data.Message || post.CreateAt != *data.CreateAt || post.UserId != user.Id { + t.Fatal("Post properties not as expected") + } + + // Check the reply values. + if result := <-th.App.Srv.Store.Post().GetPostsCreatedAt(channel.Id, replyTime); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + replies := result.Data.([]*model.Post) + if len(replies) != 1 { + t.Fatal("Unexpected number of posts found.") + } + reply := replies[0] + if reply.Message != *(*data.Replies)[0].Message || reply.CreateAt != *(*data.Replies)[0].CreateAt || reply.UserId != user2.Id { + t.Fatal("Post properties not as expected") + } + + if reply.RootId != post.Id { + t.Fatal("Unexpected reply RootId") + } + } + } } func TestImportImportDirectChannel(t *testing.T) { diff --git a/app/license.go b/app/license.go index cacc71524..c7fd07197 100644 --- a/app/license.go +++ b/app/license.go @@ -23,7 +23,7 @@ func (a *App) LoadLicense() { if len(licenseId) != 26 { // Lets attempt to load the file from disk since it was missing from the DB - license, licenseBytes := utils.GetAndValidateLicenseFileFromDisk() + license, licenseBytes := utils.GetAndValidateLicenseFileFromDisk(*a.Config().ServiceSettings.LicenseFileLocation) if license != nil { if _, err := a.SaveLicense(licenseBytes); err != nil { diff --git a/app/notification.go b/app/notification.go index a7093e17f..7223fb3aa 100644 --- a/app/notification.go +++ b/app/notification.go @@ -10,7 +10,6 @@ import ( "net/http" "net/url" "path/filepath" - "regexp" "sort" "strings" "time" @@ -20,6 +19,7 @@ import ( "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/utils" + "github.com/mattermost/mattermost-server/utils/markdown" "github.com/nicksnyder/go-i18n/i18n" ) @@ -71,8 +71,8 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod } else { keywords := a.GetMentionKeywordsInChannel(profileMap, post.Type != model.POST_HEADER_CHANGE && post.Type != model.POST_PURPOSE_CHANGE) - var potentialOtherMentions []string - mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords) + m := GetExplicitMentions(post.Message, keywords) + mentionedUserIds, hereNotification, channelNotification, allNotification = m.MentionedUserIds, m.HereMentioned, m.ChannelMentioned, m.AllMentioned // get users that have comment thread mentions enabled if len(post.RootId) > 0 && parentPostList != nil { @@ -89,8 +89,8 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod delete(mentionedUserIds, post.UserId) } - if len(potentialOtherMentions) > 0 { - if result := <-a.Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { + if len(m.OtherPotentialMentions) > 0 { + if result := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, team.Id); result.Err == nil { outOfChannelMentions := result.Data.([]*model.User) if channel.Type != model.CHANNEL_GROUP { a.Go(func() { @@ -270,7 +270,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil) - message.Add("post", post.ToJson()) + message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) message.Add("channel_type", channel.Type) message.Add("channel_display_name", channelName) message.Add("channel_name", channel.Name) @@ -693,7 +693,7 @@ func (a *App) ClearPushNotification(userId string, channelId string) { } func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) { - msg.ServerId = utils.CfgDiagnosticId + msg.ServerId = a.DiagnosticId() request, _ := http.NewRequest("POST", *a.Config().EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) @@ -788,125 +788,133 @@ func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, cha return nil } +type ExplicitMentions struct { + // MentionedUserIds contains a key for each user mentioned by keyword. + MentionedUserIds map[string]bool + + // OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have + // a corresponding keyword. + OtherPotentialMentions []string + + // HereMentioned is true if the message contained @here. + HereMentioned bool + + // AllMentioned is true if the message contained @all. + AllMentioned bool + + // ChannelMentioned is true if the message contained @channel. + ChannelMentioned bool +} + // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned // users and a slice of potential mention users not in the channel and whether or not @here was mentioned. -func GetExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool, bool, bool) { - mentioned := make(map[string]bool) - potentialOthersMentioned := make([]string, 0) +func GetExplicitMentions(message string, keywords map[string][]string) *ExplicitMentions { + ret := &ExplicitMentions{ + MentionedUserIds: make(map[string]bool), + } systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true} - hereMentioned := false - allMentioned := false - channelMentioned := false addMentionedUsers := func(ids []string) { for _, id := range ids { - mentioned[id] = true + ret.MentionedUserIds[id] = true } } - message = removeCodeFromMessage(message) - - for _, word := range strings.FieldsFunc(message, func(c rune) bool { - // Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern - return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c)) - }) { - isMention := false - - // skip word with format ':word:' with an assumption that it is an emoji format only - if word[0] == ':' && word[len(word)-1] == ':' { - continue - } - - if word == "@here" { - hereMentioned = true - } + processText := func(text string) { + for _, word := range strings.FieldsFunc(text, func(c rune) bool { + // Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern + return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c)) + }) { + isMention := false - if word == "@channel" { - channelMentioned = true - } - - if word == "@all" { - allMentioned = true - } - - // Non-case-sensitive check for regular keys - if ids, match := keywords[strings.ToLower(word)]; match { - addMentionedUsers(ids) - isMention = true - } + // skip word with format ':word:' with an assumption that it is an emoji format only + if word[0] == ':' && word[len(word)-1] == ':' { + continue + } - // Case-sensitive check for first name - if ids, match := keywords[word]; match { - addMentionedUsers(ids) - isMention = true - } + if word == "@here" { + ret.HereMentioned = true + } - if isMention { - continue - } + if word == "@channel" { + ret.ChannelMentioned = true + } - if strings.ContainsAny(word, ".-:") { - // This word contains a character that may be the end of a sentence, so split further - splitWords := strings.FieldsFunc(word, func(c rune) bool { - return c == '.' || c == '-' || c == ':' - }) + if word == "@all" { + ret.AllMentioned = true + } - for _, splitWord := range splitWords { - if splitWord == "@here" { - hereMentioned = true - } + // Non-case-sensitive check for regular keys + if ids, match := keywords[strings.ToLower(word)]; match { + addMentionedUsers(ids) + isMention = true + } - if splitWord == "@all" { - allMentioned = true - } + // Case-sensitive check for first name + if ids, match := keywords[word]; match { + addMentionedUsers(ids) + isMention = true + } - if splitWord == "@channel" { - channelMentioned = true - } + if isMention { + continue + } - // Non-case-sensitive check for regular keys - if ids, match := keywords[strings.ToLower(splitWord)]; match { - addMentionedUsers(ids) - } + if strings.ContainsAny(word, ".-:") { + // This word contains a character that may be the end of a sentence, so split further + splitWords := strings.FieldsFunc(word, func(c rune) bool { + return c == '.' || c == '-' || c == ':' + }) - // Case-sensitive check for first name - if ids, match := keywords[splitWord]; match { - addMentionedUsers(ids) - } else if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") { - username := splitWord[1:] - potentialOthersMentioned = append(potentialOthersMentioned, username) + for _, splitWord := range splitWords { + if splitWord == "@here" { + ret.HereMentioned = true + } + + if splitWord == "@all" { + ret.AllMentioned = true + } + + if splitWord == "@channel" { + ret.ChannelMentioned = true + } + + // Non-case-sensitive check for regular keys + if ids, match := keywords[strings.ToLower(splitWord)]; match { + addMentionedUsers(ids) + } + + // Case-sensitive check for first name + if ids, match := keywords[splitWord]; match { + addMentionedUsers(ids) + } else if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") { + username := splitWord[1:] + ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username) + } } } - } - if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { - username := word[1:] - potentialOthersMentioned = append(potentialOthersMentioned, username) + if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { + username := word[1:] + ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username) + } } } - return mentioned, potentialOthersMentioned, hereMentioned, channelMentioned, allMentioned -} - -// Matches a line containing only ``` and a potential language definition, any number of lines not containing ```, -// and then either a line containing only ``` or the end of the text -var codeBlockPattern = regexp.MustCompile("(?m)^[^\\S\n]*[\\`~]{3}.*$[\\s\\S]+?(^[^\\S\n]*[`~]{3}$|\\z)") - -// Matches a backquote, either some text or any number of non-empty lines, and then a final backquote -var inlineCodePattern = regexp.MustCompile("(?m)\\`+(?:.+?|.*?\n(.*?\\S.*?\n)*.*?)\\`+") - -// Strips pre-formatted text and code blocks from a Markdown string by replacing them with whitespace -func removeCodeFromMessage(message string) string { - if strings.Contains(message, "```") || strings.Contains(message, "~~~") { - message = codeBlockPattern.ReplaceAllString(message, "") - } - - // Replace with a space to prevent cases like "user`code`name" from turning into "username" - if strings.Contains(message, "`") { - message = inlineCodePattern.ReplaceAllString(message, " ") - } + buf := "" + markdown.Inspect(message, func(node interface{}) bool { + text, ok := node.(*markdown.Text) + if !ok { + processText(buf) + buf = "" + return true + } + buf += text.Text + return false + }) + processText(buf) - return message + return ret } // Given a map of user IDs to profiles, returns a list of mention diff --git a/app/notification_test.go b/app/notification_test.go index c5d0f8478..11f4df685 100644 --- a/app/notification_test.go +++ b/app/notification_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) @@ -82,147 +84,229 @@ func TestGetExplicitMentions(t *testing.T) { id2 := model.NewId() id3 := model.NewId() - // not mentioning anybody - message := "this is a message" - keywords := map[string][]string{} - if mentions, potential, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 0 || len(potential) != 0 { - t.Fatal("shouldn't have mentioned anybody or have any potencial mentions") - } - - // mentioning a user that doesn't exist - message = "this is a message for @user" - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 0 { - t.Fatal("shouldn't have mentioned user that doesn't exist") - } - - // mentioning one person - keywords = map[string][]string{"@user": {id1}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { - t.Fatal("should've mentioned @user") - } - - // mentioning one person without an @mention - message = "this is a message for @user" - keywords = map[string][]string{"this": {id1}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { - t.Fatal("should've mentioned this") - } - - // mentioning multiple people with one word - message = "this is a message for @user" - keywords = map[string][]string{"@user": {id1, id2}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { - t.Fatal("should've mentioned two users with @user") - } - - // mentioning only one of multiple people - keywords = map[string][]string{"@user": {id1}, "@mention": {id2}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { - t.Fatal("should've mentioned @user and not @mention") - } - - // mentioning multiple people with multiple words - message = "this is an @mention for @user" - keywords = map[string][]string{"@user": {id1}, "@mention": {id2}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { - t.Fatal("should've mentioned two users with @user and @mention") - } - - // mentioning @channel (not a special case, but it's good to double check) - message = "this is an message for @channel" - keywords = map[string][]string{"@channel": {id1, id2}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { - t.Fatal("should've mentioned two users with @channel") - } - - // mentioning @all (not a special case, but it's good to double check) - message = "this is an message for @all" - keywords = map[string][]string{"@all": {id1, id2}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { - t.Fatal("should've mentioned two users with @all") - } - - // mentioning user.period without mentioning user (PLT-3222) - message = "user.period doesn't complicate things at all by including periods in their username" - keywords = map[string][]string{"user.period": {id1}, "user": {id2}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { - t.Fatal("should've mentioned user.period and not user") - } - - // mentioning a potential out of channel user - message = "this is an message for @potential and @user" - keywords = map[string][]string{"@user": {id1}} - if mentions, potential, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || len(potential) != 1 { - t.Fatal("should've mentioned user and have a potential not in channel") - } - - // words in inline code shouldn't trigger mentions - message = "`this shouldn't mention @channel at all`" - keywords = map[string][]string{} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 0 { - t.Fatal("@channel in inline code shouldn't cause a mention") - } - - // words in code blocks shouldn't trigger mentions - message = "```\nthis shouldn't mention @channel at all\n```" - keywords = map[string][]string{} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 0 { - t.Fatal("@channel in code block shouldn't cause a mention") - } - - // Markdown-formatted text that isn't code should trigger mentions - message = "*@aaa @bbb @ccc*" - keywords = map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 3 || !mentions[id1] || !mentions[id2] || !mentions[id3] { - t.Fatal("should've mentioned all 3 users", mentions) - } - - message = "**@aaa @bbb @ccc**" - keywords = map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 3 || !mentions[id1] || !mentions[id2] || !mentions[id3] { - t.Fatal("should've mentioned all 3 users") - } - - message = "~~@aaa @bbb @ccc~~" - keywords = map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 3 || !mentions[id1] || !mentions[id2] || !mentions[id3] { - t.Fatal("should've mentioned all 3 users") - } - - message = "### @aaa" - keywords = map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] || mentions[id3] { - t.Fatal("should've only mentioned aaa") - } - - message = "> @aaa" - keywords = map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] || mentions[id3] { - t.Fatal("should've only mentioned aaa") - } - - message = ":smile:" - keywords = map[string][]string{"smile": {id1}, "smiley": {id2}, "smiley_cat": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) == 1 || mentions[id1] { - t.Fatal("should not mentioned smile") - } - - message = "smile" - keywords = map[string][]string{"smile": {id1}, "smiley": {id2}, "smiley_cat": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] || mentions[id3] { - t.Fatal("should've only mentioned smile") - } - - message = ":smile" - keywords = map[string][]string{"smile": {id1}, "smiley": {id2}, "smiley_cat": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] || mentions[id3] { - t.Fatal("should've only mentioned smile") - } - - message = "smile:" - keywords = map[string][]string{"smile": {id1}, "smiley": {id2}, "smiley_cat": {id3}} - if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] || mentions[id3] { - t.Fatal("should've only mentioned smile") + for name, tc := range map[string]struct { + Message string + Keywords map[string][]string + Expected *ExplicitMentions + }{ + "Nobody": { + Message: "this is a message", + Keywords: map[string][]string{}, + Expected: &ExplicitMentions{}, + }, + "NonexistentUser": { + Message: "this is a message for @user", + Expected: &ExplicitMentions{ + OtherPotentialMentions: []string{"user"}, + }, + }, + "OnePerson": { + Message: "this is a message for @user", + Keywords: map[string][]string{"@user": {id1}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + }, + }, + "OnePersonWithoutAtMention": { + Message: "this is a message for @user", + Keywords: map[string][]string{"this": {id1}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + OtherPotentialMentions: []string{"user"}, + }, + }, + "MultiplePeopleWithOneWord": { + Message: "this is a message for @user", + Keywords: map[string][]string{"@user": {id1, id2}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + id2: true, + }, + }, + }, + "OneOfMultiplePeople": { + Message: "this is a message for @user", + Keywords: map[string][]string{"@user": {id1}, "@mention": {id2}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + }, + }, + "MultiplePeopleWithMultipleWords": { + Message: "this is an @mention for @user", + Keywords: map[string][]string{"@user": {id1}, "@mention": {id2}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + id2: true, + }, + }, + }, + "Channel": { + Message: "this is an message for @channel", + Keywords: map[string][]string{"@channel": {id1, id2}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + id2: true, + }, + ChannelMentioned: true, + }, + }, + "All": { + Message: "this is an message for @all", + Keywords: map[string][]string{"@all": {id1, id2}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + id2: true, + }, + AllMentioned: true, + }, + }, + "UserWithPeriod": { + Message: "user.period doesn't complicate things at all by including periods in their username", + Keywords: map[string][]string{"user.period": {id1}, "user": {id2}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + }, + }, + "PotentialOutOfChannelUser": { + Message: "this is an message for @potential and @user", + Keywords: map[string][]string{"@user": {id1}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + OtherPotentialMentions: []string{"potential"}, + }, + }, + "InlineCode": { + Message: "`this shouldn't mention @channel at all`", + Keywords: map[string][]string{}, + Expected: &ExplicitMentions{}, + }, + "FencedCodeBlock": { + Message: "```\nthis shouldn't mention @channel at all\n```", + Keywords: map[string][]string{}, + Expected: &ExplicitMentions{}, + }, + "Emphasis": { + Message: "*@aaa @bbb @ccc*", + Keywords: map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + id2: true, + id3: true, + }, + }, + }, + "StrongEmphasis": { + Message: "**@aaa @bbb @ccc**", + Keywords: map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + id2: true, + id3: true, + }, + }, + }, + "Strikethrough": { + Message: "~~@aaa @bbb @ccc~~", + Keywords: map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + id2: true, + id3: true, + }, + }, + }, + "Heading": { + Message: "### @aaa", + Keywords: map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + }, + }, + "BlockQuote": { + Message: "> @aaa", + Keywords: map[string][]string{"@aaa": {id1}, "@bbb": {id2}, "@ccc": {id3}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + }, + }, + "Emoji": { + Message: ":smile:", + Keywords: map[string][]string{"smile": {id1}, "smiley": {id2}, "smiley_cat": {id3}}, + Expected: &ExplicitMentions{}, + }, + "NotEmoji": { + Message: "smile", + Keywords: map[string][]string{"smile": {id1}, "smiley": {id2}, "smiley_cat": {id3}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + }, + }, + "UnclosedEmoji": { + Message: ":smile", + Keywords: map[string][]string{"smile": {id1}, "smiley": {id2}, "smiley_cat": {id3}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + }, + }, + "UnopenedEmoji": { + Message: "smile:", + Keywords: map[string][]string{"smile": {id1}, "smiley": {id2}, "smiley_cat": {id3}}, + Expected: &ExplicitMentions{ + MentionedUserIds: map[string]bool{ + id1: true, + }, + }, + }, + "IndentedCodeBlock": { + Message: " this shouldn't mention @channel at all", + Keywords: map[string][]string{}, + Expected: &ExplicitMentions{}, + }, + "LinkTitle": { + Message: `[foo](this "shouldn't mention @channel at all")`, + Keywords: map[string][]string{}, + Expected: &ExplicitMentions{}, + }, + "MalformedInlineCode": { + Message: "`this should mention @channel``", + Keywords: map[string][]string{}, + Expected: &ExplicitMentions{ + ChannelMentioned: true, + }, + }, + } { + t.Run(name, func(t *testing.T) { + m := GetExplicitMentions(tc.Message, tc.Keywords) + if tc.Expected.MentionedUserIds == nil { + tc.Expected.MentionedUserIds = make(map[string]bool) + } + assert.EqualValues(t, tc.Expected, m) + }) } } @@ -268,170 +352,24 @@ func TestGetExplicitMentionsAtHere(t *testing.T) { } for message, shouldMention := range cases { - if _, _, hereMentioned, _, _ := GetExplicitMentions(message, nil); hereMentioned && !shouldMention { + if m := GetExplicitMentions(message, nil); m.HereMentioned && !shouldMention { t.Fatalf("shouldn't have mentioned @here with \"%v\"", message) - } else if !hereMentioned && shouldMention { - t.Fatalf("should've have mentioned @here with \"%v\"", message) + } else if !m.HereMentioned && shouldMention { + t.Fatalf("should've mentioned @here with \"%v\"", message) } } // mentioning @here and someone id := model.NewId() - if mentions, potential, hereMentioned, _, _ := GetExplicitMentions("@here @user @potential", map[string][]string{"@user": {id}}); !hereMentioned { + if m := GetExplicitMentions("@here @user @potential", map[string][]string{"@user": {id}}); !m.HereMentioned { t.Fatal("should've mentioned @here with \"@here @user\"") - } else if len(mentions) != 1 || !mentions[id] { + } else if len(m.MentionedUserIds) != 1 || !m.MentionedUserIds[id] { t.Fatal("should've mentioned @user with \"@here @user\"") - } else if len(potential) > 1 { + } else if len(m.OtherPotentialMentions) > 1 { t.Fatal("should've potential mentions for @potential") } } -func TestRemoveCodeFromMessage(t *testing.T) { - input := "this is regular text" - expected := input - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is text with\n```\na code block\n```\nin it" - expected = "this is text with\n\nin it" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is text with\n```javascript\na JS code block\n```\nin it" - expected = "this is text with\n\nin it" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is text with\n```java script?\na JS code block\n```\nin it" - expected = "this is text with\n\nin it" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is text with an empty\n```\n\n\n\n```\nin it" - expected = "this is text with an empty\n\nin it" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is text with\n```\ntwo\n```\ncode\n```\nblocks\n```\nin it" - expected = "this is text with\n\ncode\n\nin it" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is text with indented\n ```\ncode\n ```\nin it" - expected = "this is text with indented\n\nin it" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is text ending with\n```\nan unfinished code block" - expected = "this is text ending with\n" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `code` in a sentence" - expected = "this is in a sentence" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `two` things of `code` in a sentence" - expected = "this is things of in a sentence" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `code with spaces` in a sentence" - expected = "this is in a sentence" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `code\nacross multiple` lines" - expected = "this is lines" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `code\non\nmany\ndifferent` lines" - expected = "this is lines" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `\ncode on its own line\n` across multiple lines" - expected = "this is across multiple lines" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `\n some more code \n` across multiple lines" - expected = "this is across multiple lines" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `\ncode` on its own line" - expected = "this is on its own line" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `code\n` on its own line" - expected = "this is on its own line" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is *italics mixed with `code in a way that has the code` take precedence*" - expected = "this is *italics mixed with take precedence*" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is code within a wo` `rd for some reason" - expected = "this is code within a wo rd for some reason" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `not\n\ncode` because it has a blank line" - expected = input - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is `not\n \ncode` because it has a line with only whitespace" - expected = input - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is just `` two backquotes" - expected = input - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "these are ``multiple backquotes`` around code" - expected = "these are around code" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } - - input = "this is text with\n~~~\na code block\n~~~\nin it" - expected = "this is text with\n\nin it" - if actual := removeCodeFromMessage(input); actual != expected { - t.Fatalf("received incorrect output\n\nGot:\n%v\n\nExpected:\n%v\n", actual, expected) - } -} - func TestGetMentionKeywords(t *testing.T) { th := Setup() defer th.TearDown() diff --git a/app/oauth_test.go b/app/oauth_test.go index b964b377d..60854a354 100644 --- a/app/oauth_test.go +++ b/app/oauth_test.go @@ -49,10 +49,6 @@ func TestOAuthDeleteApp(t *testing.T) { th := Setup() defer th.TearDown() - oldSetting := th.App.Config().ServiceSettings.EnableOAuthServiceProvider - defer th.App.UpdateConfig(func(cfg *model.Config) { - cfg.ServiceSettings.EnableOAuthServiceProvider = oldSetting - }) th.App.Config().ServiceSettings.EnableOAuthServiceProvider = true a1 := &model.OAuthApp{} diff --git a/app/options.go b/app/options.go index 9b40806f3..464566024 100644 --- a/app/options.go +++ b/app/options.go @@ -35,3 +35,7 @@ func ConfigFile(file string) Option { a.configFile = file } } + +func DisableConfigWatch(a *App) { + a.disableConfigWatch = true +} diff --git a/app/plugin.go b/app/plugin.go index 9083f4785..3f06a000f 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -30,6 +30,8 @@ import ( "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/plugin/pluginenv" + "github.com/mattermost/mattermost-server/plugin/rpcplugin" + "github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox" ) const ( @@ -54,7 +56,7 @@ func (a *App) initBuiltInPlugins() { } p.Initialize(api) } - utils.AddConfigListener(func(before, after *model.Config) { + a.AddConfigListener(func(before, after *model.Config) { for _, p := range plugins { p.OnConfigurationChange() } @@ -382,6 +384,12 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug if supervisorOverride != nil { options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) + } else if err := sandbox.CheckSupport(); err != nil { + l4g.Warn(err.Error()) + l4g.Warn("plugin sandboxing is not supported. plugins will run with the same access level as the server") + options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider)) + } else { + options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider)) } if env, err := pluginenv.New(options...); err != nil { @@ -407,8 +415,8 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug } } - utils.RemoveConfigListener(a.PluginConfigListenerId) - a.PluginConfigListenerId = utils.AddConfigListener(func(prevCfg, cfg *model.Config) { + a.RemoveConfigListener(a.PluginConfigListenerId) + a.PluginConfigListenerId = a.AddConfigListener(func(prevCfg, cfg *model.Config) { if a.PluginEnv == nil { return } @@ -428,7 +436,6 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) { if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented) - err.Translate(utils.T) l4g.Error(err.Error()) w.WriteHeader(err.StatusCode) w.Header().Set("Content-Type", "application/json") @@ -490,7 +497,7 @@ func (a *App) ShutDownPlugins() { for _, err := range a.PluginEnv.Shutdown() { l4g.Error(err.Error()) } - utils.RemoveConfigListener(a.PluginConfigListenerId) + a.RemoveConfigListener(a.PluginConfigListenerId) a.PluginConfigListenerId = "" a.PluginEnv = nil } diff --git a/app/plugin/ldapextras/plugin.go b/app/plugin/ldapextras/plugin.go index 473ec6393..3af55c2ac 100644 --- a/app/plugin/ldapextras/plugin.go +++ b/app/plugin/ldapextras/plugin.go @@ -13,7 +13,6 @@ import ( "github.com/mattermost/mattermost-server/app/plugin" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) type Plugin struct { @@ -65,7 +64,6 @@ func (p *Plugin) handleGetAttributes(w http.ResponseWriter, r *http.Request) { attributes, err := p.api.GetLdapUserAttributes(id, config.Attributes) if err != nil { - err.Translate(utils.T) http.Error(w, fmt.Sprintf("Errored getting attributes: %v", err.Error()), http.StatusInternalServerError) } diff --git a/app/post.go b/app/post.go index 192c2effb..bf4725e77 100644 --- a/app/post.go +++ b/app/post.go @@ -4,6 +4,11 @@ package app import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "net/http" @@ -309,7 +314,7 @@ func (a *App) SendEphemeralPost(userId string, post *model.Post) *model.Post { } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil) - message.Add("post", post.ToJson()) + message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Go(func() { a.Publish(message) @@ -419,7 +424,7 @@ func (a *App) PatchPost(postId string, patch *model.PostPatch) (*model.Post, *mo func (a *App) sendUpdatedPostEvent(post *model.Post) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil) - message.Add("post", post.ToJson()) + message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Go(func() { a.Publish(message) @@ -562,7 +567,7 @@ func (a *App) DeletePost(postId string) (*model.Post, *model.AppError) { } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil) - message.Add("post", post.ToJson()) + message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Go(func() { a.Publish(message) @@ -823,3 +828,124 @@ func (a *App) DoPostAction(postId string, actionId string, userId string) *model return nil } + +func (a *App) PostListWithProxyAddedToImageURLs(list *model.PostList) *model.PostList { + if f := a.ImageProxyAdder(); f != nil { + return list.WithRewrittenImageURLs(f) + } + return list +} + +func (a *App) PostWithProxyAddedToImageURLs(post *model.Post) *model.Post { + if f := a.ImageProxyAdder(); f != nil { + return post.WithRewrittenImageURLs(f) + } + return post +} + +func (a *App) PostWithProxyRemovedFromImageURLs(post *model.Post) *model.Post { + if f := a.ImageProxyRemover(); f != nil { + return post.WithRewrittenImageURLs(f) + } + return post +} + +func (a *App) PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch { + if f := a.ImageProxyRemover(); f != nil { + return patch.WithRewrittenImageURLs(f) + } + return patch +} + +func (a *App) imageProxyConfig() (proxyType, proxyURL, options, siteURL string) { + cfg := a.Config() + + if cfg.ServiceSettings.ImageProxyURL == nil || cfg.ServiceSettings.ImageProxyType == nil || cfg.ServiceSettings.SiteURL == nil { + return + } + + proxyURL = *cfg.ServiceSettings.ImageProxyURL + proxyType = *cfg.ServiceSettings.ImageProxyType + siteURL = *cfg.ServiceSettings.SiteURL + + if proxyURL == "" || proxyType == "" { + return "", "", "", "" + } + + if proxyURL[len(proxyURL)-1] != '/' { + proxyURL += "/" + } + + if cfg.ServiceSettings.ImageProxyOptions != nil { + options = *cfg.ServiceSettings.ImageProxyOptions + } + + return +} + +func (a *App) ImageProxyAdder() func(string) string { + proxyType, proxyURL, options, siteURL := a.imageProxyConfig() + if proxyType == "" { + return nil + } + + return func(url string) string { + if strings.HasPrefix(url, proxyURL) { + return url + } + + if url[0] == '/' { + url = siteURL + url + } + + switch proxyType { + case "atmos/camo": + mac := hmac.New(sha1.New, []byte(options)) + mac.Write([]byte(url)) + digest := hex.EncodeToString(mac.Sum(nil)) + return proxyURL + digest + "/" + hex.EncodeToString([]byte(url)) + case "willnorris/imageproxy": + options := strings.Split(options, "|") + if len(options) > 1 { + mac := hmac.New(sha256.New, []byte(options[1])) + mac.Write([]byte(url)) + digest := base64.URLEncoding.EncodeToString(mac.Sum(nil)) + if options[0] == "" { + return proxyURL + "s" + digest + "/" + url + } + return proxyURL + options[0] + ",s" + digest + "/" + url + } + return proxyURL + options[0] + "/" + url + } + + return url + } +} + +func (a *App) ImageProxyRemover() (f func(string) string) { + proxyType, proxyURL, _, _ := a.imageProxyConfig() + if proxyType == "" { + return nil + } + + return func(url string) string { + switch proxyType { + case "atmos/camo": + if strings.HasPrefix(url, proxyURL) { + if slash := strings.IndexByte(url[len(proxyURL):], '/'); slash >= 0 { + if decoded, err := hex.DecodeString(url[len(proxyURL)+slash+1:]); err == nil { + return string(decoded) + } + } + } + case "willnorris/imageproxy": + if strings.HasPrefix(url, proxyURL) { + if slash := strings.IndexByte(url[len(proxyURL):], '/'); slash >= 0 { + return url[len(proxyURL)+slash+1:] + } + } + } + + return url + } +} diff --git a/app/post_test.go b/app/post_test.go index 82eac3cd1..9854bb707 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -185,3 +185,84 @@ func TestPostChannelMentions(t *testing.T) { }, }, result.Props["channel_mentions"]) } + +func TestImageProxy(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + for name, tc := range map[string]struct { + ProxyType string + ProxyURL string + ProxyOptions string + ImageURL string + ProxiedImageURL string + }{ + "atmos/camo": { + ProxyType: "atmos/camo", + ProxyURL: "https://127.0.0.1", + ProxyOptions: "foo", + ImageURL: "http://mydomain.com/myimage", + ProxiedImageURL: "https://127.0.0.1/f8dace906d23689e8d5b12c3cefbedbf7b9b72f5/687474703a2f2f6d79646f6d61696e2e636f6d2f6d79696d616765", + }, + "willnorris/imageproxy": { + ProxyType: "willnorris/imageproxy", + ProxyURL: "https://127.0.0.1", + ProxyOptions: "x1000", + ImageURL: "http://mydomain.com/myimage", + ProxiedImageURL: "https://127.0.0.1/x1000/http://mydomain.com/myimage", + }, + "willnorris/imageproxy_WithSigning": { + ProxyType: "willnorris/imageproxy", + ProxyURL: "https://127.0.0.1", + ProxyOptions: "x1000|foo", + ImageURL: "http://mydomain.com/myimage", + ProxiedImageURL: "https://127.0.0.1/x1000,sbhHVoG5d60UvnNtGh6Iy6x4PaMmnsh8JfZ7JfErKjGU=/http://mydomain.com/myimage", + }, + } { + t.Run(name, func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.ImageProxyType = model.NewString(tc.ProxyType) + cfg.ServiceSettings.ImageProxyOptions = model.NewString(tc.ProxyOptions) + cfg.ServiceSettings.ImageProxyURL = model.NewString(tc.ProxyURL) + }) + + post := &model.Post{ + Id: model.NewId(), + Message: "![foo](" + tc.ImageURL + ")", + } + + list := model.NewPostList() + list.Posts[post.Id] = post + + assert.Equal(t, "![foo]("+tc.ProxiedImageURL+")", th.App.PostListWithProxyAddedToImageURLs(list).Posts[post.Id].Message) + assert.Equal(t, "![foo]("+tc.ProxiedImageURL+")", th.App.PostWithProxyAddedToImageURLs(post).Message) + + assert.Equal(t, "![foo]("+tc.ImageURL+")", th.App.PostWithProxyRemovedFromImageURLs(post).Message) + post.Message = "![foo](" + tc.ProxiedImageURL + ")" + assert.Equal(t, "![foo]("+tc.ImageURL+")", th.App.PostWithProxyRemovedFromImageURLs(post).Message) + }) + } +} + +var imageProxyBenchmarkSink *model.Post + +func BenchmarkPostWithProxyRemovedFromImageURLs(b *testing.B) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.ImageProxyType = model.NewString("willnorris/imageproxy") + cfg.ServiceSettings.ImageProxyOptions = model.NewString("x1000|foo") + cfg.ServiceSettings.ImageProxyURL = model.NewString("https://127.0.0.1") + }) + + post := &model.Post{ + Message: "![foo](http://mydomain.com/myimage)", + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + imageProxyBenchmarkSink = th.App.PostWithProxyAddedToImageURLs(post) + } +} diff --git a/app/reaction.go b/app/reaction.go index bf0d20e2b..062622f34 100644 --- a/app/reaction.go +++ b/app/reaction.go @@ -62,6 +62,6 @@ func (a *App) sendReactionEvent(event string, reaction *model.Reaction, post *mo post.HasReactions = true post.UpdateAt = model.GetMillis() umessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil) - umessage.Add("post", post.ToJson()) + umessage.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Publish(umessage) } diff --git a/app/security_update_check.go b/app/security_update_check.go index dc26e59b2..1f1759fef 100644 --- a/app/security_update_check.go +++ b/app/security_update_check.go @@ -42,7 +42,7 @@ func (a *App) DoSecurityUpdateCheck() { v := url.Values{} - v.Set(PROP_SECURITY_ID, utils.CfgDiagnosticId) + v.Set(PROP_SECURITY_ID, a.DiagnosticId()) v.Set(PROP_SECURITY_BUILD, model.CurrentVersion+"."+model.BuildNumber) v.Set(PROP_SECURITY_ENTERPRISE_READY, model.BuildEnterpriseReady) v.Set(PROP_SECURITY_DATABASE, *a.Config().SqlSettings.DriverName) diff --git a/app/session.go b/app/session.go index bf5f68fa3..1c5daf29e 100644 --- a/app/session.go +++ b/app/session.go @@ -356,6 +356,19 @@ func (a *App) EnableUserAccessToken(token *model.UserAccessToken) *model.AppErro return nil } +func (a *App) GetUserAccessTokens(page, perPage int) ([]*model.UserAccessToken, *model.AppError) { + if result := <-a.Srv.Store.UserAccessToken().GetAll(page*perPage, perPage); result.Err != nil { + return nil, result.Err + } else { + tokens := result.Data.([]*model.UserAccessToken) + for _, token := range tokens { + token.Token = "" + } + + return tokens, nil + } +} + func (a *App) GetUserAccessTokensForUser(userId string, page, perPage int) ([]*model.UserAccessToken, *model.AppError) { if result := <-a.Srv.Store.UserAccessToken().GetByUser(userId, page*perPage, perPage); result.Err != nil { return nil, result.Err @@ -380,3 +393,15 @@ func (a *App) GetUserAccessToken(tokenId string, sanitize bool) (*model.UserAcce return token, nil } } + +func (a *App) SearchUserAccessTokens(term string) ([]*model.UserAccessToken, *model.AppError) { + if result := <-a.Srv.Store.UserAccessToken().Search(term); result.Err != nil { + return nil, result.Err + } else { + tokens := result.Data.([]*model.UserAccessToken) + for _, token := range tokens { + token.Token = "" + } + return tokens, nil + } +} diff --git a/app/team.go b/app/team.go index b12904ab3..71eb00569 100644 --- a/app/team.go +++ b/app/team.go @@ -616,13 +616,15 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) channel = result.Data.(*model.Channel) } - if requestorId == user.Id { - if err := a.postLeaveTeamMessage(user, channel); err != nil { - l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) - } - } else { - if err := a.postRemoveFromTeamMessage(user, channel); err != nil { - l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { + if requestorId == user.Id { + if err := a.postLeaveTeamMessage(user, channel); err != nil { + l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + } + } else { + if err := a.postRemoveFromTeamMessage(user, channel); err != nil { + l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + } } } diff --git a/app/team_test.go b/app/team_test.go index 10f33f50b..084558fb4 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -69,7 +69,7 @@ func TestCreateTeamWithUser(t *testing.T) { t.Log(err.Message) t.Fatal("Should not create a team with user when user has set email without domain") } else { - if err.Message != "model.team.is_valid.email.app_error" { + if err.Id != "model.team.is_valid.email.app_error" { t.Log(err) t.Fatal("Invalid error message") } diff --git a/app/user.go b/app/user.go index 493b391ae..64e49e293 100644 --- a/app/user.go +++ b/app/user.go @@ -778,7 +778,10 @@ func (a *App) SetProfileImage(userId string, imageData *multipart.FileHeader) *m return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.open.app_error", nil, err.Error(), http.StatusBadRequest) } defer file.Close() + return a.SetProfileImageFromFile(userId, file) +} +func (a *App) SetProfileImageFromFile(userId string, file multipart.File) *model.AppError { // Decode image config first to check dimensions before loading the whole thing into memory later on config, _, err := image.DecodeConfig(file) if err != nil { diff --git a/app/user_test.go b/app/user_test.go index 3a924dfa7..38ff286b3 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -88,10 +88,6 @@ func TestCreateOAuthUser(t *testing.T) { th.App.PermanentDeleteUser(user) - userCreation := th.App.Config().TeamSettings.EnableUserCreation - defer th.App.UpdateConfig(func(cfg *model.Config) { - cfg.TeamSettings.EnableUserCreation = userCreation - }) th.App.Config().TeamSettings.EnableUserCreation = false _, err = th.App.CreateOAuthUser(model.USER_AUTH_SERVICE_GITLAB, strings.NewReader(json), th.BasicTeam.Id) diff --git a/app/web_conn.go b/app/web_conn.go index 1c74e65a5..e625e61b5 100644 --- a/app/web_conn.go +++ b/app/web_conn.go @@ -277,7 +277,7 @@ func (webCon *WebConn) IsAuthenticated() bool { func (webCon *WebConn) SendHello() { msg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_HELLO, "", "", webCon.UserId, nil) - msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.ClientCfgHash, utils.IsLicensed())) + msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, webCon.App.ClientConfigHash(), utils.IsLicensed())) webCon.Send <- msg } diff --git a/app/webhook_test.go b/app/webhook_test.go index 47303ee93..850e74efc 100644 --- a/app/webhook_test.go +++ b/app/webhook_test.go @@ -17,13 +17,6 @@ func TestCreateIncomingWebhookForChannel(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() - enableIncomingHooks := th.App.Config().ServiceSettings.EnableIncomingWebhooks - defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks }) - enablePostUsernameOverride := th.App.Config().ServiceSettings.EnablePostUsernameOverride - defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnablePostUsernameOverride = enablePostUsernameOverride }) - enablePostIconOverride := th.App.Config().ServiceSettings.EnablePostIconOverride - defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnablePostIconOverride = enablePostIconOverride }) - type TestCase struct { EnableIncomingHooks bool EnablePostUsernameOverride bool @@ -155,13 +148,6 @@ func TestUpdateIncomingWebhook(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() - enableIncomingHooks := th.App.Config().ServiceSettings.EnableIncomingWebhooks - defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks }) - enablePostUsernameOverride := th.App.Config().ServiceSettings.EnablePostUsernameOverride - defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnablePostUsernameOverride = enablePostUsernameOverride }) - enablePostIconOverride := th.App.Config().ServiceSettings.EnablePostIconOverride - defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnablePostIconOverride = enablePostIconOverride }) - type TestCase struct { EnableIncomingHooks bool EnablePostUsernameOverride bool @@ -300,8 +286,6 @@ func TestCreateWebhookPost(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() - enableIncomingHooks := th.App.Config().ServiceSettings.EnableIncomingWebhooks - defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks }) th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = true }) hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id}) |