Merge branch 'master' into plugins-2

This commit is contained in:
JoramWilander
2018-06-27 16:56:50 -04:00
47 changed files with 12552 additions and 26582 deletions

View File

@@ -209,13 +209,20 @@ func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if rchannel, err := c.App.PatchChannel(oldChannel, patch, c.Session.UserId); err != nil {
rchannel, err := c.App.PatchChannel(oldChannel, patch, c.Session.UserId)
if err != nil {
c.Err = err
return
} else {
c.LogAudit("")
w.Write([]byte(rchannel.ToJson()))
}
err = c.App.FillInChannelProps(rchannel)
if err != nil {
c.Err = err
return
}
c.LogAudit("")
w.Write([]byte(rchannel.ToJson()))
}
func restoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -361,6 +368,12 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
err = c.App.FillInChannelProps(channel)
if err != nil {
c.Err = err
return
}
w.Write([]byte(channel.ToJson()))
}
@@ -444,13 +457,19 @@ func getPublicChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request
return
}
if channels, err := c.App.GetPublicChannelsForTeam(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage); err != nil {
channels, err := c.App.GetPublicChannelsForTeam(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage)
if err != nil {
c.Err = err
return
} else {
w.Write([]byte(channels.ToJson()))
}
err = c.App.FillInChannelsProps(channels)
if err != nil {
c.Err = err
return
}
w.Write([]byte(channels.ToJson()))
}
func getDeletedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -464,13 +483,19 @@ func getDeletedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Reques
return
}
if channels, err := c.App.GetDeletedChannels(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage); err != nil {
channels, err := c.App.GetDeletedChannels(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage)
if err != nil {
c.Err = err
return
} else {
w.Write([]byte(channels.ToJson()))
}
err = c.App.FillInChannelsProps(channels)
if err != nil {
c.Err = err
return
}
w.Write([]byte(channels.ToJson()))
}
func getPublicChannelsByIdsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -497,12 +522,19 @@ func getPublicChannelsByIdsForTeam(c *Context, w http.ResponseWriter, r *http.Re
return
}
if channels, err := c.App.GetPublicChannelsByIdsForTeam(c.Params.TeamId, channelIds); err != nil {
channels, err := c.App.GetPublicChannelsByIdsForTeam(c.Params.TeamId, channelIds)
if err != nil {
c.Err = err
return
} else {
w.Write([]byte(channels.ToJson()))
}
err = c.App.FillInChannelsProps(channels)
if err != nil {
c.Err = err
return
}
w.Write([]byte(channels.ToJson()))
}
func getChannelsForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -521,15 +553,24 @@ func getChannelsForTeamForUser(c *Context, w http.ResponseWriter, r *http.Reques
return
}
if channels, err := c.App.GetChannelsForUser(c.Params.TeamId, c.Params.UserId); err != nil {
channels, err := c.App.GetChannelsForUser(c.Params.TeamId, c.Params.UserId)
if err != nil {
c.Err = err
return
} else if c.HandleEtag(channels.Etag(), "Get Channels", w, r) {
return
} else {
w.Header().Set(model.HEADER_ETAG_SERVER, channels.Etag())
w.Write([]byte(channels.ToJson()))
}
if c.HandleEtag(channels.Etag(), "Get Channels", w, r) {
return
}
err = c.App.FillInChannelsProps(channels)
if err != nil {
c.Err = err
return
}
w.Header().Set(model.HEADER_ETAG_SERVER, channels.Etag())
w.Write([]byte(channels.ToJson()))
}
func autocompleteChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -545,12 +586,15 @@ func autocompleteChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Requ
name := r.URL.Query().Get("name")
if channels, err := c.App.AutocompleteChannels(c.Params.TeamId, name); err != nil {
channels, err := c.App.AutocompleteChannels(c.Params.TeamId, name)
if err != nil {
c.Err = err
return
} else {
w.Write([]byte(channels.ToJson()))
}
// Don't fill in channels props, since unused by client and potentially expensive.
w.Write([]byte(channels.ToJson()))
}
func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -570,12 +614,15 @@ func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if channels, err := c.App.SearchChannels(c.Params.TeamId, props.Term); err != nil {
channels, err := c.App.SearchChannels(c.Params.TeamId, props.Term)
if err != nil {
c.Err = err
return
} else {
w.Write([]byte(channels.ToJson()))
}
// Don't fill in channels props, since unused by client and potentially expensive.
w.Write([]byte(channels.ToJson()))
}
func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -638,6 +685,12 @@ func getChannelByName(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
err = c.App.FillInChannelProps(channel)
if err != nil {
c.Err = err
return
}
w.Write([]byte(channel.ToJson()))
}
@@ -660,6 +713,12 @@ func getChannelByNameForTeamName(c *Context, w http.ResponseWriter, r *http.Requ
return
}
err = c.App.FillInChannelProps(channel)
if err != nil {
c.Err = err
return
}
w.Write([]byte(channel.ToJson()))
}

View File

@@ -1127,15 +1127,8 @@ func closeBody(r *http.Response) {
type MattermostTestProvider struct {
}
func (m *MattermostTestProvider) GetIdentifier() string {
return model.SERVICE_GITLAB
}
func (m *MattermostTestProvider) GetUserFromJson(data io.Reader) *model.User {
return model.UserFromJson(data)
}
func (m *MattermostTestProvider) GetAuthDataFromJson(data io.Reader) string {
authData := model.UserFromJson(data)
return authData.Email
user := model.UserFromJson(data)
user.AuthData = &user.Email
return user
}

View File

@@ -206,6 +206,9 @@ func New(options ...Option) (outApp *App, outErr error) {
}
app.initJobs()
app.AddLicenseListener(func() {
app.initJobs()
})
subpath, err := utils.GetSubpathFromConfig(app.Config())
if err != nil {

View File

@@ -397,13 +397,13 @@ func (a *App) UpdateChannelPrivacy(oldChannel *model.Channel, user *model.User)
}
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",
message := (map[string]string{
model.CHANNEL_OPEN: utils.T("api.channel.change_channel_privacy.private_to_public"),
model.CHANNEL_PRIVATE: utils.T("api.channel.change_channel_privacy.public_to_private"),
})[channel.Type]
post := &model.Post{
ChannelId: channel.Id,
Message: utils.T("api.channel.change_channel_privacy." + privacy),
Message: message,
Type: model.POST_CHANGE_CHANNEL_PRIVACY,
UserId: user.Id,
Props: model.StringInterface{
@@ -1591,3 +1591,67 @@ func (a *App) ToggleMuteChannel(channelId string, userId string) *model.ChannelM
a.Srv.Store.Channel().UpdateMember(member)
return member
}
func (a *App) FillInChannelProps(channel *model.Channel) *model.AppError {
return a.FillInChannelsProps(&model.ChannelList{channel})
}
func (a *App) FillInChannelsProps(channelList *model.ChannelList) *model.AppError {
// Group the channels by team and call GetChannelsByNames just once per team.
channelsByTeam := make(map[string]model.ChannelList)
for _, channel := range *channelList {
channelsByTeam[channel.TeamId] = append(channelsByTeam[channel.TeamId], channel)
}
for teamId, channelList := range channelsByTeam {
allChannelMentions := make(map[string]bool)
channelMentions := make(map[*model.Channel][]string, len(channelList))
// Collect mentions across the channels so as to query just once for this team.
for _, channel := range channelList {
channelMentions[channel] = model.ChannelMentions(channel.Header)
for _, channelMention := range channelMentions[channel] {
allChannelMentions[channelMention] = true
}
}
allChannelMentionNames := make([]string, 0, len(allChannelMentions))
for channelName := range allChannelMentions {
allChannelMentionNames = append(allChannelMentionNames, channelName)
}
if len(allChannelMentionNames) > 0 {
mentionedChannels, err := a.GetChannelsByNames(allChannelMentionNames, teamId)
if err != nil {
return err
}
mentionedChannelsByName := make(map[string]*model.Channel)
for _, channel := range mentionedChannels {
mentionedChannelsByName[channel.Name] = channel
}
for _, channel := range channelList {
channelMentionsProp := make(map[string]interface{}, len(channelMentions[channel]))
for _, channelMention := range channelMentions[channel] {
if mentioned, ok := mentionedChannelsByName[channelMention]; ok {
if mentioned.Type == model.CHANNEL_OPEN {
channelMentionsProp[mentioned.Name] = map[string]interface{}{
"display_name": mentioned.DisplayName,
}
}
}
}
if len(channelMentionsProp) > 0 {
channel.AddProp("channel_mentions", channelMentionsProp)
} else if channel.Props != nil {
delete(channel.Props, "channel_mentions")
}
}
}
}
return nil
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPermanentDeleteChannel(t *testing.T) {
@@ -399,3 +400,208 @@ func TestAppUpdateChannelScheme(t *testing.T) {
t.Fatal("Wrong Channel SchemeId")
}
}
func TestFillInChannelProps(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
channelPublic1, err := th.App.CreateChannel(&model.Channel{DisplayName: "Public 1", Name: "public1", Type: model.CHANNEL_OPEN, TeamId: th.BasicTeam.Id}, false)
require.Nil(t, err)
defer th.App.PermanentDeleteChannel(channelPublic1)
channelPublic2, err := th.App.CreateChannel(&model.Channel{DisplayName: "Public 2", Name: "public2", Type: model.CHANNEL_OPEN, TeamId: th.BasicTeam.Id}, false)
require.Nil(t, err)
defer th.App.PermanentDeleteChannel(channelPublic2)
channelPrivate, err := th.App.CreateChannel(&model.Channel{DisplayName: "Private", Name: "private", Type: model.CHANNEL_PRIVATE, TeamId: th.BasicTeam.Id}, false)
require.Nil(t, err)
defer th.App.PermanentDeleteChannel(channelPrivate)
otherTeamId := model.NewId()
otherTeam := &model.Team{
DisplayName: "dn_" + otherTeamId,
Name: "name" + otherTeamId,
Email: "success+" + otherTeamId + "@simulator.amazonses.com",
Type: model.TEAM_OPEN,
}
otherTeam, err = th.App.CreateTeam(otherTeam)
require.Nil(t, err)
defer th.App.PermanentDeleteTeam(otherTeam)
channelOtherTeam, err := th.App.CreateChannel(&model.Channel{DisplayName: "Other Team Channel", Name: "other-team", Type: model.CHANNEL_OPEN, TeamId: otherTeam.Id}, false)
require.Nil(t, err)
defer th.App.PermanentDeleteChannel(channelOtherTeam)
// Note that purpose is intentionally plaintext below.
t.Run("single channels", func(t *testing.T) {
testCases := []struct {
Description string
Channel *model.Channel
ExpectedChannelProps map[string]interface{}
}{
{
"channel on basic team without references",
&model.Channel{
TeamId: th.BasicTeam.Id,
Header: "No references",
Purpose: "No references",
},
nil,
},
{
"channel on basic team",
&model.Channel{
TeamId: th.BasicTeam.Id,
Header: "~public1, ~private, ~other-team",
Purpose: "~public2, ~private, ~other-team",
},
map[string]interface{}{
"channel_mentions": map[string]interface{}{
"public1": map[string]interface{}{
"display_name": "Public 1",
},
},
},
},
{
"channel on other team",
&model.Channel{
TeamId: otherTeam.Id,
Header: "~public1, ~private, ~other-team",
Purpose: "~public2, ~private, ~other-team",
},
map[string]interface{}{
"channel_mentions": map[string]interface{}{
"other-team": map[string]interface{}{
"display_name": "Other Team Channel",
},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
err = th.App.FillInChannelProps(testCase.Channel)
require.Nil(t, err)
assert.Equal(t, testCase.ExpectedChannelProps, testCase.Channel.Props)
})
}
})
t.Run("multiple channels", func(t *testing.T) {
testCases := []struct {
Description string
Channels *model.ChannelList
ExpectedChannelProps map[string]interface{}
}{
{
"single channel on basic team",
&model.ChannelList{
{
Name: "test",
TeamId: th.BasicTeam.Id,
Header: "~public1, ~private, ~other-team",
Purpose: "~public2, ~private, ~other-team",
},
},
map[string]interface{}{
"test": map[string]interface{}{
"channel_mentions": map[string]interface{}{
"public1": map[string]interface{}{
"display_name": "Public 1",
},
},
},
},
},
{
"multiple channels on basic team",
&model.ChannelList{
{
Name: "test",
TeamId: th.BasicTeam.Id,
Header: "~public1, ~private, ~other-team",
Purpose: "~public2, ~private, ~other-team",
},
{
Name: "test2",
TeamId: th.BasicTeam.Id,
Header: "~private, ~other-team",
Purpose: "~public2, ~private, ~other-team",
},
{
Name: "test3",
TeamId: th.BasicTeam.Id,
Header: "No references",
Purpose: "No references",
},
},
map[string]interface{}{
"test": map[string]interface{}{
"channel_mentions": map[string]interface{}{
"public1": map[string]interface{}{
"display_name": "Public 1",
},
},
},
"test2": map[string]interface{}(nil),
"test3": map[string]interface{}(nil),
},
},
{
"multiple channels across teams",
&model.ChannelList{
{
Name: "test",
TeamId: th.BasicTeam.Id,
Header: "~public1, ~private, ~other-team",
Purpose: "~public2, ~private, ~other-team",
},
{
Name: "test2",
TeamId: otherTeam.Id,
Header: "~private, ~other-team",
Purpose: "~public2, ~private, ~other-team",
},
{
Name: "test3",
TeamId: th.BasicTeam.Id,
Header: "No references",
Purpose: "No references",
},
},
map[string]interface{}{
"test": map[string]interface{}{
"channel_mentions": map[string]interface{}{
"public1": map[string]interface{}{
"display_name": "Public 1",
},
},
},
"test2": map[string]interface{}{
"channel_mentions": map[string]interface{}{
"other-team": map[string]interface{}{
"display_name": "Other Team Channel",
},
},
},
"test3": map[string]interface{}(nil),
},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
err = th.App.FillInChannelsProps(testCase.Channels)
require.Nil(t, err)
for _, channel := range *testCase.Channels {
assert.Equal(t, testCase.ExpectedChannelProps[channel.Name], channel.Props)
}
})
}
})
}

View File

@@ -6,6 +6,7 @@ package app
import (
"github.com/mattermost/mattermost-server/model"
goi18n "github.com/nicksnyder/go-i18n/i18n"
"strings"
)
type JoinProvider struct {
@@ -34,12 +35,18 @@ func (me *JoinProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Comman
}
func (me *JoinProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse {
if result := <-a.Srv.Store.Channel().GetByName(args.TeamId, message, true); result.Err != nil {
channelName := message
if strings.HasPrefix(message, "~") {
channelName = message[1:]
}
if result := <-a.Srv.Store.Channel().GetByName(args.TeamId, channelName, true); result.Err != nil {
return &model.CommandResponse{Text: args.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else {
channel := result.Data.(*model.Channel)
if channel.Name == message {
if channel.Name == channelName {
allowed := false
if (channel.Type == model.CHANNEL_PRIVATE && a.SessionHasPermissionToChannel(args.Session, channel.Id, model.PERMISSION_READ_CHANNEL)) || channel.Type == model.CHANNEL_OPEN {
allowed = true

88
app/command_join_test.go Normal file
View File

@@ -0,0 +1,88 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package app
import (
"testing"
"github.com/mattermost/mattermost-server/model"
"github.com/nicksnyder/go-i18n/i18n"
"github.com/stretchr/testify/assert"
)
func TestJoinCommandNoChannel(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
if testing.Short() {
t.SkipNow()
}
cmd := &JoinProvider{}
resp := cmd.DoCommand(th.App, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser2.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}, "asdsad")
assert.Equal(t, "api.command_join.list.app_error", resp.Text)
}
func TestJoinCommandForExistingChannel(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
if testing.Short() {
t.SkipNow()
}
channel2, _ := th.App.CreateChannel(&model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.CHANNEL_OPEN,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
cmd := &JoinProvider{}
resp := cmd.DoCommand(th.App, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser2.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}, channel2.Name)
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+th.BasicTeam.Name+"/channels/"+channel2.Name, resp.GotoLocation)
}
func TestJoinCommandWithTilde(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
if testing.Short() {
t.SkipNow()
}
channel2, _ := th.App.CreateChannel(&model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.CHANNEL_OPEN,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
cmd := &JoinProvider{}
resp := cmd.DoCommand(th.App, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser2.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}, "~"+channel2.Name)
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+th.BasicTeam.Name+"/channels/"+channel2.Name, resp.GotoLocation)
}

View File

@@ -36,6 +36,7 @@ const (
TRACK_CONFIG_WEBRTC = "config_webrtc"
TRACK_CONFIG_SUPPORT = "config_support"
TRACK_CONFIG_NATIVEAPP = "config_nativeapp"
TRACK_CONFIG_EXPERIMENTAL = "config_experimental"
TRACK_CONFIG_ANALYTICS = "config_analytics"
TRACK_CONFIG_ANNOUNCEMENT = "config_announcement"
TRACK_CONFIG_ELASTICSEARCH = "config_elasticsearch"
@@ -252,6 +253,7 @@ func (a *App) trackConfig() {
"allow_cookies_for_subdomains": *cfg.ServiceSettings.AllowCookiesForSubdomains,
"enable_api_team_deletion": *cfg.ServiceSettings.EnableAPITeamDeletion,
"experimental_enable_hardened_mode": *cfg.ServiceSettings.ExperimentalEnableHardenedMode,
"experimental_limit_client_config": *cfg.ServiceSettings.ExperimentalLimitClientConfig,
})
a.SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{
@@ -475,6 +477,11 @@ func (a *App) trackConfig() {
"isdefault_turn_uri": isDefault(*cfg.WebrtcSettings.TurnURI, model.WEBRTC_SETTINGS_DEFAULT_TURN_URI),
})
a.SendDiagnostic(TRACK_CONFIG_EXPERIMENTAL, map[string]interface{}{
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.CLIENT_SIDE_CERT_CHECK_PRIMARY_AUTH),
})
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),
})

View File

@@ -54,7 +54,7 @@ const (
MaxImageSize = 6048 * 4032 // 24 megapixels, roughly 36MB as a raw image
IMAGE_THUMBNAIL_PIXEL_WIDTH = 120
IMAGE_THUMBNAIL_PIXEL_HEIGHT = 100
IMAGE_PREVIEW_PIXEL_WIDTH = 1024
IMAGE_PREVIEW_PIXEL_WIDTH = 1920
)
func (a *App) FileBackend() (utils.FileBackend, *model.AppError) {

View File

@@ -209,6 +209,9 @@ func (a *App) BulkImport(fileReader io.Reader, dryRun bool, workers int) (*model
scanner := bufio.NewScanner(fileReader)
lineNumber := 0
a.Srv.Store.LockToMaster()
defer a.Srv.Store.UnlockFromMaster()
errorsChan := make(chan LineImportWorkerError, (2*workers)+1) // size chosen to ensure it never gets filled up completely.
var wg sync.WaitGroup
var linesChan chan LineImportWorkerData

View File

@@ -457,7 +457,13 @@ func (a *App) LoginByOAuth(service string, userData io.Reader, teamId string) (*
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.not_available.app_error",
map[string]interface{}{"Service": strings.Title(service)}, "", http.StatusNotImplemented)
} else {
authData = provider.GetAuthDataFromJson(bytes.NewReader(buf.Bytes()))
authUser := provider.GetUserFromJson(bytes.NewReader(buf.Bytes()))
if authUser.AuthData != nil {
authData = *authUser.AuthData
} else {
authData = ""
}
}
if len(authData) == 0 {

View File

@@ -44,6 +44,9 @@
"WebserverMode": "gzip",
"EnableCustomEmoji": false,
"EnableEmojiPicker": true,
"EnableGifPicker": true,
"GfycatApiKey": "",
"GfycatApiSecret": "",
"RestrictCustomEmojiCreation": "all",
"RestrictPostDelete": "all",
"AllowEditPost": "always",

View File

@@ -10,9 +10,7 @@ import (
)
type OauthProvider interface {
GetIdentifier() string
GetUserFromJson(data io.Reader) *model.User
GetAuthDataFromJson(data io.Reader) string
}
var oauthProviders = make(map[string]OauthProvider)

File diff suppressed because it is too large Load Diff

View File

@@ -99,6 +99,14 @@
"id": "api.channel.add_user_to_channel.type.app_error",
"translation": "Can not add user to this channel type"
},
{
"id": "api.channel.change_channel_privacy.private_to_public",
"translation": "This channel has been converted to a Public Channel and can be joined by any team member."
},
{
"id": "api.channel.change_channel_privacy.public_to_private",
"translation": "This channel has been converted to a Private Channel."
},
{
"id": "api.channel.convert_channel_to_private.default_channel_error",
"translation": "This default channel cannot be converted into a private channel."
@@ -1534,66 +1542,6 @@
"id": "api.slackimport.slack_import.zip.app_error",
"translation": "Unable to open the Slack export zip file.\r\n"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.open_transaction.app_error",
"translation": "Failed to begin the database transaction"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.rollback_transaction.app_error",
"translation": "Failed to rollback the database transaction"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.select.app_error",
"translation": "Failed to retrieve the users"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.update.app_error",
"translation": "Failed to update the user"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.commit_transaction.app_error",
"translation": "Failed to commit the database transaction"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.open_transaction.app_error",
"translation": "Failed to begin the database transaction"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.rollback_transaction.app_error",
"translation": "Failed to rollback the database transaction"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.select.app_error",
"translation": "Failed to retrieve the team members"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.update.app_error",
"translation": "Failed to update the team member"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.commit_transaction.app_error",
"translation": "Failed to commit the database transaction"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.open_transaction.app_error",
"translation": "Failed to begin the database transaction"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.rollback_transaction.app_error",
"translation": "Failed to rollback the database transaction"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.select.app_error",
"translation": "Failed to retrieve the channel members"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.update.app_error",
"translation": "Failed to update the channel member"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.commit_transaction.app_error",
"translation": "Failed to commit the database transaction"
},
{
"id": "api.status.user_not_found.app_error",
"translation": "User not found"
@@ -5126,6 +5074,26 @@
"id": "store.sql_channel.analytics_type_count.app_error",
"translation": "We couldn't get channel type counts"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.commit_transaction.app_error",
"translation": "Failed to commit the database transaction"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.open_transaction.app_error",
"translation": "Failed to begin the database transaction"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.rollback_transaction.app_error",
"translation": "Failed to rollback the database transaction"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.select.app_error",
"translation": "Failed to retrieve the channel members"
},
{
"id": "store.sql_channel.clear_all_custom_role_assignments.update.app_error",
"translation": "Failed to update the channel member"
},
{
"id": "store.sql_channel.delete.channel.app_error",
"translation": "We couldn't delete the channel"
@@ -6110,6 +6078,26 @@
"id": "store.sql_team.analytics_team_count.app_error",
"translation": "We couldn't count the teams"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.commit_transaction.app_error",
"translation": "Failed to commit the database transaction"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.open_transaction.app_error",
"translation": "Failed to begin the database transaction"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.rollback_transaction.app_error",
"translation": "Failed to rollback the database transaction"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.select.app_error",
"translation": "Failed to retrieve the team members"
},
{
"id": "store.sql_team.clear_all_custom_role_assignments.update.app_error",
"translation": "Failed to update the team member"
},
{
"id": "store.sql_team.get.find.app_error",
"translation": "We couldn't find the existing team"
@@ -6266,6 +6254,26 @@
"id": "store.sql_user.analytics_unique_user_count.app_error",
"translation": "We couldn't get the unique user count"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.commit_transaction.app_error",
"translation": "Failed to commit the database transaction"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.open_transaction.app_error",
"translation": "Failed to begin the database transaction"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.rollback_transaction.app_error",
"translation": "Failed to rollback the database transaction"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.select.app_error",
"translation": "Failed to retrieve the users"
},
{
"id": "store.sql_user.clear_all_custom_role_assignments.update.app_error",
"translation": "Failed to update the user"
},
{
"id": "store.sql_user.get.app_error",
"translation": "We encountered an error finding the account"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -95,6 +95,8 @@ func (workers *Workers) Start() *Workers {
}
func (workers *Workers) handleConfigChange(oldConfig *model.Config, newConfig *model.Config) {
mlog.Debug("Workers received config change.")
if workers.DataRetention != nil {
if (!*oldConfig.DataRetentionSettings.EnableMessageDeletion && !*oldConfig.DataRetentionSettings.EnableFileDeletion) && (*newConfig.DataRetentionSettings.EnableMessageDeletion || *newConfig.DataRetentionSettings.EnableFileDeletion) {
go workers.DataRetention.Run()

View File

@@ -32,21 +32,22 @@ const (
)
type Channel struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
LastPostAt int64 `json:"last_post_at"`
TotalMsgCount int64 `json:"total_msg_count"`
ExtraUpdateAt int64 `json:"extra_update_at"`
CreatorId string `json:"creator_id"`
SchemeId *string `json:"scheme_id"`
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
LastPostAt int64 `json:"last_post_at"`
TotalMsgCount int64 `json:"total_msg_count"`
ExtraUpdateAt int64 `json:"extra_update_at"`
CreatorId string `json:"creator_id"`
SchemeId *string `json:"scheme_id"`
Props map[string]interface{} `json:"props" db:"-"`
}
type ChannelPatch struct {
@@ -163,6 +164,18 @@ func (o *Channel) Patch(patch *ChannelPatch) {
}
}
func (o *Channel) MakeNonNil() {
if o.Props == nil {
o.Props = make(map[string]interface{})
}
}
func (o *Channel) AddProp(key string, value interface{}) {
o.MakeNonNil()
o.Props[key] = value
}
func GetDMNameFromIds(userId1, userId2 string) string {
if userId1 > userId2 {
return userId2 + "__" + userId1

28
model/channel_mentions.go Normal file
View File

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"regexp"
"strings"
)
var channelMentionRegexp = regexp.MustCompile(`\B~[a-zA-Z0-9\-_]+`)
func ChannelMentions(message string) []string {
var names []string
if strings.Contains(message, "~") {
alreadyMentioned := make(map[string]bool)
for _, match := range channelMentionRegexp.FindAllString(message, -1) {
name := match[1:]
if !alreadyMentioned[name] {
names = append(names, name)
alreadyMentioned[name] = true
}
}
}
return names
}

View File

@@ -210,6 +210,9 @@ type ServiceSettings struct {
WebserverMode *string
EnableCustomEmoji *bool
EnableEmojiPicker *bool
EnableGifPicker *bool
GfycatApiKey *string
GfycatApiSecret *string
RestrictCustomEmojiCreation *string
RestrictPostDelete *string
AllowEditPost *string
@@ -413,6 +416,18 @@ func (s *ServiceSettings) SetDefaults() {
s.EnableEmojiPicker = NewBool(true)
}
if s.EnableGifPicker == nil {
s.EnableGifPicker = NewBool(true)
}
if s.GfycatApiKey == nil {
s.GfycatApiKey = NewString("")
}
if s.GfycatApiSecret == nil {
s.GfycatApiSecret = NewString("")
}
if s.RestrictCustomEmojiCreation == nil {
s.RestrictCustomEmojiCreation = NewString(RESTRICT_EMOJI_CREATION_ALL)
}

View File

@@ -47,7 +47,7 @@ func userFromGitLabUser(glu *GitLabUser) *model.User {
user.FirstName = glu.Name
}
user.Email = glu.Email
userId := strconv.FormatInt(glu.Id, 10)
userId := glu.getAuthData()
user.AuthData = &userId
user.AuthService = model.USER_AUTH_SERVICE_GITLAB
@@ -90,10 +90,6 @@ func (glu *GitLabUser) getAuthData() string {
return strconv.FormatInt(glu.Id, 10)
}
func (m *GitLabProvider) GetIdentifier() string {
return model.USER_AUTH_SERVICE_GITLAB
}
func (m *GitLabProvider) GetUserFromJson(data io.Reader) *model.User {
glu := gitLabUserFromJson(data)
if glu.IsValid() {
@@ -102,13 +98,3 @@ func (m *GitLabProvider) GetUserFromJson(data io.Reader) *model.User {
return &model.User{}
}
func (m *GitLabProvider) GetAuthDataFromJson(data io.Reader) string {
glu := gitLabUserFromJson(data)
if glu.IsValid() {
return glu.getAuthData()
}
return ""
}

View File

@@ -7,7 +7,6 @@ import (
"encoding/json"
"io"
"net/http"
"regexp"
"sort"
"strings"
"unicode/utf8"
@@ -343,20 +342,8 @@ func PostPatchFromJson(data io.Reader) *PostPatch {
return &post
}
var channelMentionRegexp = regexp.MustCompile(`\B~[a-zA-Z0-9\-_]+`)
func (o *Post) ChannelMentions() (names []string) {
if strings.Contains(o.Message, "~") {
alreadyMentioned := make(map[string]bool)
for _, match := range channelMentionRegexp.FindAllString(o.Message, -1) {
name := match[1:]
if !alreadyMentioned[name] {
names = append(names, name)
alreadyMentioned[name] = true
}
}
}
return
func (o *Post) ChannelMentions() []string {
return ChannelMentions(o.Message)
}
func (r *PostActionIntegrationRequest) ToJson() string {

View File

@@ -181,6 +181,14 @@ func (s *LayeredStore) Close() {
s.DatabaseLayer.Close()
}
func (s *LayeredStore) LockToMaster() {
s.DatabaseLayer.LockToMaster()
}
func (s *LayeredStore) UnlockFromMaster() {
s.DatabaseLayer.UnlockFromMaster()
}
func (s *LayeredStore) DropAllTables() {
s.DatabaseLayer.DropAllTables()
}

View File

@@ -298,7 +298,7 @@ func (s SqlChannelStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_channelmembers_channel_id", "ChannelMembers", "ChannelId")
s.CreateIndexIfNotExists("idx_channelmembers_user_id", "ChannelMembers", "UserId")
s.CreateFullTextIndexIfNotExists("idx_channels_txt", "Channels", "Name, DisplayName")
s.CreateFullTextIndexIfNotExists("idx_channel_search_txt", "Channels", "Name, DisplayName, Purpose")
}
func (s SqlChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) store.StoreChannel {
@@ -1573,7 +1573,7 @@ func (s SqlChannelStore) SearchMore(userId string, teamId string, term string) s
func (s SqlChannelStore) buildLIKEClause(term string) (likeClause, likeTerm string) {
likeTerm = term
searchColumns := "Name, DisplayName"
searchColumns := "Name, DisplayName, Purpose"
// These chars must be removed from the like query.
for _, c := range ignoreLikeSearchChar {
@@ -1608,7 +1608,7 @@ func (s SqlChannelStore) buildFulltextClause(term string) (fulltextClause, fullt
// Copy the terms as we will need to prepare them differently for each search type.
fulltextTerm = term
searchColumns := "Name, DisplayName"
searchColumns := "Name, DisplayName, Purpose"
// These chars must be treated as spaces in the fulltext query.
for _, c := range spaceFulltextSearchChar {

View File

@@ -65,6 +65,8 @@ type SqlStore interface {
RemoveIndexIfExists(indexName string, tableName string) bool
GetAllConns() []*gorp.DbMap
Close()
LockToMaster()
UnlockFromMaster()
Team() store.TeamStore
Channel() store.ChannelStore
Post() store.PostStore

View File

@@ -105,6 +105,7 @@ type SqlSupplier struct {
searchReplicas []*gorp.DbMap
oldStores SqlSupplierOldStores
settings *model.SqlSettings
lockedToMaster bool
}
func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlSupplier {
@@ -283,7 +284,7 @@ func (ss *SqlSupplier) GetSearchReplica() *gorp.DbMap {
}
func (ss *SqlSupplier) GetReplica() *gorp.DbMap {
if len(ss.settings.DataSourceReplicas) == 0 {
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster {
return ss.GetMaster()
}
@@ -801,6 +802,14 @@ func (ss *SqlSupplier) Close() {
}
}
func (ss *SqlSupplier) LockToMaster() {
ss.lockedToMaster = true
}
func (ss *SqlSupplier) UnlockFromMaster() {
ss.lockedToMaster = false
}
func (ss *SqlSupplier) Team() store.TeamStore {
return ss.oldStores.team
}

View File

@@ -446,6 +446,8 @@ func UpgradeDatabaseToVersion50(sqlStore SqlStore) {
sqlStore.GetMaster().Exec("UPDATE Roles SET SchemeManaged=false WHERE Name NOT IN ('system_user', 'system_admin', 'team_user', 'team_admin', 'channel_user', 'channel_admin')")
sqlStore.CreateColumnIfNotExists("IncomingWebhooks", "ChannelLocked", "boolean", "boolean", "0")
sqlStore.RemoveIndexIfExists("idx_channels_txt", "Channels")
saveSchemaVersion(sqlStore, VERSION_5_0_0)
}
}

View File

@@ -67,6 +67,8 @@ type Store interface {
Plugin() PluginStore
MarkSystemRanUnitTests()
Close()
LockToMaster()
UnlockFromMaster()
DropAllTables()
TotalMasterDbConnections() int
TotalReadDbConnections() int

View File

@@ -1708,6 +1708,14 @@ func testChannelStoreSearchMore(t *testing.T, ss store.Store) {
o8.Type = model.CHANNEL_PRIVATE
store.Must(ss.Channel().Save(&o8, -1))
o9 := model.Channel{}
o9.TeamId = o1.TeamId
o9.DisplayName = "Channel With Purpose"
o9.Purpose = "This can now be searchable!"
o9.Name = "with-purpose"
o9.Type = model.CHANNEL_OPEN
store.Must(ss.Channel().Save(&o9, -1))
if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, "ChannelA"); result.Err != nil {
t.Fatal(result.Err)
} else {
@@ -1773,6 +1781,19 @@ func testChannelStoreSearchMore(t *testing.T, ss store.Store) {
}
}
if result := <-ss.Channel().SearchMore(m1.UserId, o1.TeamId, "now searchable"); result.Err != nil {
t.Fatal(result.Err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) != 1 {
t.Fatal("should return 1 channel")
}
if (*channels)[0].Name != o9.Name {
t.Fatal("wrong channel returned")
}
}
/*
// Disabling this check as it will fail on PostgreSQL as we have "liberalised" channel matching to deal with
// Full-Text Stemming Limitations.
@@ -1884,6 +1905,14 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) {
o11.Type = model.CHANNEL_OPEN
store.Must(ss.Channel().Save(&o11, -1))
o12 := model.Channel{}
o12.TeamId = o1.TeamId
o12.DisplayName = "Channel With Purpose"
o12.Purpose = "This can now be searchable!"
o12.Name = "with-purpose"
o12.Type = model.CHANNEL_OPEN
store.Must(ss.Channel().Save(&o12, -1))
for name, search := range map[string]func(teamId string, term string) store.StoreChannel{
"AutocompleteInTeam": ss.Channel().AutocompleteInTeam,
"SearchInTeam": ss.Channel().SearchInTeam,
@@ -1986,6 +2015,19 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) {
t.Fatal("wrong channel returned")
}
}
if result := <-search(o1.TeamId, "now searchable"); result.Err != nil {
t.Fatal(result.Err)
} else {
channels := result.Data.(*model.ChannelList)
if len(*channels) != 1 {
t.Fatal("should return 1 channel")
}
if (*channels)[0].Name != o12.Name {
t.Fatal("wrong channel returned")
}
}
})
}
}

View File

@@ -200,6 +200,11 @@ func (_m *LayeredStoreDatabaseLayer) License() store.LicenseStore {
return r0
}
// LockToMaster provides a mock function with given fields:
func (_m *LayeredStoreDatabaseLayer) LockToMaster() {
_m.Called()
}
// MarkSystemRanUnitTests provides a mock function with given fields:
func (_m *LayeredStoreDatabaseLayer) MarkSystemRanUnitTests() {
_m.Called()
@@ -851,6 +856,11 @@ func (_m *LayeredStoreDatabaseLayer) TotalSearchDbConnections() int {
return r0
}
// UnlockFromMaster provides a mock function with given fields:
func (_m *LayeredStoreDatabaseLayer) UnlockFromMaster() {
_m.Called()
}
// User provides a mock function with given fields:
func (_m *LayeredStoreDatabaseLayer) User() store.UserStore {
ret := _m.Called()

View File

@@ -411,6 +411,11 @@ func (_m *SqlStore) License() store.LicenseStore {
return r0
}
// LockToMaster provides a mock function with given fields:
func (_m *SqlStore) LockToMaster() {
_m.Called()
}
// MarkSystemRanUnitTests provides a mock function with given fields:
func (_m *SqlStore) MarkSystemRanUnitTests() {
_m.Called()
@@ -706,6 +711,11 @@ func (_m *SqlStore) TotalSearchDbConnections() int {
return r0
}
// UnlockFromMaster provides a mock function with given fields:
func (_m *SqlStore) UnlockFromMaster() {
_m.Called()
}
// User provides a mock function with given fields:
func (_m *SqlStore) User() store.UserStore {
ret := _m.Called()

View File

@@ -198,6 +198,11 @@ func (_m *Store) License() store.LicenseStore {
return r0
}
// LockToMaster provides a mock function with given fields:
func (_m *Store) LockToMaster() {
_m.Called()
}
// MarkSystemRanUnitTests provides a mock function with given fields:
func (_m *Store) MarkSystemRanUnitTests() {
_m.Called()
@@ -437,6 +442,11 @@ func (_m *Store) TotalSearchDbConnections() int {
return r0
}
// UnlockFromMaster provides a mock function with given fields:
func (_m *Store) UnlockFromMaster() {
_m.Called()
}
// User provides a mock function with given fields:
func (_m *Store) User() store.UserStore {
ret := _m.Called()

View File

@@ -77,6 +77,8 @@ func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
}
func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ }
func (s *Store) Close() { /* do nothing */ }
func (s *Store) LockToMaster() { /* do nothing */ }
func (s *Store) UnlockFromMaster() { /* do nothing */ }
func (s *Store) DropAllTables() { /* do nothing */ }
func (s *Store) TotalMasterDbConnections() int { return 1 }
func (s *Store) TotalReadDbConnections() int { return 1 }

View File

@@ -555,6 +555,9 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
props["SQLDriverName"] = *c.SqlSettings.DriverName
props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker)
props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker)
props["GfycatApiKey"] = *c.ServiceSettings.GfycatApiKey
props["GfycatApiSecret"] = *c.ServiceSettings.GfycatApiSecret
props["RestrictCustomEmojiCreation"] = *c.ServiceSettings.RestrictCustomEmojiCreation
props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)

View File

@@ -157,7 +157,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.Err.IsOAuth = false
}
if IsApiCall(c.App, r) || len(r.Header.Get("X-Mobile-App")) > 0 {
if IsApiCall(c.App, r) || IsWebhookCall(c.App, r) || len(r.Header.Get("X-Mobile-App")) > 0 {
w.WriteHeader(c.Err.StatusCode)
w.Write([]byte(c.Err.ToJson()))
} else {

View File

@@ -73,7 +73,13 @@ func Handle404(a *app.App, w http.ResponseWriter, r *http.Request) {
func IsApiCall(a *app.App, r *http.Request) bool {
subpath, _ := utils.GetSubpathFromConfig(a.Config())
return strings.Index(r.URL.Path, path.Join(subpath, "api")+"/") == 0
return strings.HasPrefix(r.URL.Path, path.Join(subpath, "api")+"/")
}
func IsWebhookCall(a *app.App, r *http.Request) bool {
subpath, _ := utils.GetSubpathFromConfig(a.Config())
return strings.HasPrefix(r.URL.Path, path.Join(subpath, "hooks")+"/")
}
func ReturnStatusOK(w http.ResponseWriter) {