summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/admin.go6
-rw-r--r--app/app.go101
-rw-r--r--app/app_test.go10
-rw-r--r--app/apptestlib.go34
-rw-r--r--app/auto_channels.go1
-rw-r--r--app/auto_users.go2
-rw-r--r--app/channel.go61
-rw-r--r--app/channel_test.go15
-rw-r--r--app/command.go12
-rw-r--r--app/config.go168
-rw-r--r--app/config_test.go56
-rw-r--r--app/diagnostics.go175
-rw-r--r--app/diagnostics_test.go16
-rw-r--r--app/email.go26
-rw-r--r--app/emoji.go20
-rw-r--r--app/import.go236
-rw-r--r--app/import_test.go320
-rw-r--r--app/license.go2
-rw-r--r--app/notification.go210
-rw-r--r--app/notification_test.go524
-rw-r--r--app/oauth_test.go4
-rw-r--r--app/options.go4
-rw-r--r--app/plugin.go17
-rw-r--r--app/plugin/ldapextras/plugin.go2
-rw-r--r--app/post.go132
-rw-r--r--app/post_test.go81
-rw-r--r--app/reaction.go2
-rw-r--r--app/security_update_check.go2
-rw-r--r--app/session.go25
-rw-r--r--app/team.go16
-rw-r--r--app/team_test.go2
-rw-r--r--app/user.go3
-rw-r--r--app/user_test.go4
-rw-r--r--app/web_conn.go2
-rw-r--r--app/webhook_test.go16
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})