From 509ecf4345188a0dc0ef81022f538eca17e0e899 Mon Sep 17 00:00:00 2001 From: William Gathoye Date: Thu, 1 Nov 2018 22:29:35 +0100 Subject: [PATCH 01/23] Add link to Freenode (#9785) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5a4e3e2be..ec39c8469a 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,6 @@ Receive notifications of critical security updates. The sophistication of online - **Twitter** - Follow [Mattermost](https://twitter.com/mattermost) - **Blog** - Get the latest updates from the [Mattermost blog](https://about.mattermost.com/blog/). - **Email** - Subscribe to our [newsletter](http://mattermost.us11.list-manage.com/subscribe?u=6cdba22349ae374e188e7ab8e&id=2add1c8034) (1 or 2 per month) -- **IRC** - Join us on #matterbridge (thanks to [matterircd](https://github.com/42wim/matterircd)) +- **IRC** - Join the #matterbridge channel on [Freenode](https://freenode.net/) (thanks to [matterircd](https://github.com/42wim/matterircd)) Any other questions, mail us at info@mattermost.com. We’d love to meet you! From 04a6a779e27b274cfab6808dd73b5609ffc5998a Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Fri, 2 Nov 2018 11:22:15 -0400 Subject: [PATCH 02/23] MM-12708: tack on signin_change when completing email to saml change (#9776) --- web/saml.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/saml.go b/web/saml.go index f5400a3782..773b349808 100644 --- a/web/saml.go +++ b/web/saml.go @@ -149,16 +149,19 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { return } - if action == model.OAUTH_ACTION_MOBILE { + switch action { + case model.OAUTH_ACTION_MOBILE: ReturnStatusOK(w) - } else if action == model.OAUTH_ACTION_CLIENT { + case model.OAUTH_ACTION_CLIENT: err = c.App.SendMessageToExtension(w, relayProps["extension_id"], c.Session.Token) if err != nil { c.Err = err return } - } else { + case model.OAUTH_ACTION_EMAIL_TO_SSO: + http.Redirect(w, r, c.GetSiteURLHeader()+"/login?extra=signin_change", http.StatusFound) + default: http.Redirect(w, r, c.GetSiteURLHeader(), http.StatusFound) } } From c155359e58dd2f8f477d5314feb175f6f3093f8d Mon Sep 17 00:00:00 2001 From: Kerry Dougherty Date: Fri, 2 Nov 2018 13:39:38 -0400 Subject: [PATCH 03/23] MM-12369 Add Create Outgoing Webhook Command (#9779) * add create outgoing webhook command * add create outgoing webhook command --- cmd/mattermost/commands/webhook.go | 97 +++++++++++++++++++++++++ cmd/mattermost/commands/webhook_test.go | 65 ++++++++++++++++- 2 files changed, 159 insertions(+), 3 deletions(-) diff --git a/cmd/mattermost/commands/webhook.go b/cmd/mattermost/commands/webhook.go index d91d076a3d..3869cf8637 100644 --- a/cmd/mattermost/commands/webhook.go +++ b/cmd/mattermost/commands/webhook.go @@ -5,6 +5,7 @@ package commands import ( "fmt" + "strings" "github.com/mattermost/mattermost-server/model" "github.com/pkg/errors" @@ -40,6 +41,15 @@ var WebhookModifyIncomingCmd = &cobra.Command{ RunE: modifyIncomingWebhookCmdF, } +var WebhookCreateOutgoingCmd = &cobra.Command{ + Use: "create-outgoing", + Short: "Create outgoing webhook", + Long: "create outgoing webhook which allows external posting of messages from a specific channel", + Example: ` webhook create-outgoing --team myteam --user myusername --display-name mywebhook --trigger-words "build\ntest" --urls http://localhost:8000/my-webhook-handler + webhook create-outgoing --team myteam --channel mychannel --user myusername --display-name mywebhook --description "My cool webhook" --trigger-when 1 --trigger-words "build\ntest" --icon http://localhost:8000/my-slash-handler-bot-icon.png --urls http://localhost:8000/my-webhook-handler --content-type "application/json"`, + RunE: createOutgoingWebhookCmdF, +} + func listWebhookCmdF(command *cobra.Command, args []string) error { app, err := InitDBCommandContextCobra(command) if err != nil { @@ -177,6 +187,81 @@ func modifyIncomingWebhookCmdF(command *cobra.Command, args []string) error { return nil } +func createOutgoingWebhookCmdF(command *cobra.Command, args []string) error { + app, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer app.Shutdown() + + teamArg, errTeam := command.Flags().GetString("team") + if errTeam != nil || teamArg == "" { + return errors.New("Team is required") + } + team := getTeamFromTeamArg(app, teamArg) + if team == nil { + return errors.New("Unable to find team: " + teamArg) + } + + userArg, errUser := command.Flags().GetString("user") + if errUser != nil || userArg == "" { + return errors.New("User is required") + } + user := getUserFromUserArg(app, userArg) + if user == nil { + return errors.New("Unable to find user: " + userArg) + } + + displayName, errName := command.Flags().GetString("display-name") + if errName != nil || displayName == "" { + return errors.New("Display name is required") + } + + triggerWordsString, errWords := command.Flags().GetString("trigger-words") + if errWords != nil || triggerWordsString == "" { + return errors.New("Trigger word or words required") + } + triggerWords := strings.Split(triggerWordsString, "\n") + + callbackURLsString, errURL := command.Flags().GetString("urls") + if errURL != nil || callbackURLsString == "" { + return errors.New("Callback URL or URLs required") + } + callbackURLs := strings.Split(callbackURLsString, "\n") + + triggerWhen, _ := command.Flags().GetInt("trigger-when") + description, _ := command.Flags().GetString("description") + contentType, _ := command.Flags().GetString("content-type") + iconURL, _ := command.Flags().GetString("icon") + + outgoingWebhook := &model.OutgoingWebhook{ + CreatorId: user.Id, + Username: user.Username, + TeamId: team.Id, + TriggerWords: triggerWords, + TriggerWhen: triggerWhen, + CallbackURLs: callbackURLs, + DisplayName: displayName, + Description: description, + ContentType: contentType, + IconURL: iconURL, + } + + channelArg, _ := command.Flags().GetString("channel") + if channelArg != "" { + channel := getChannelFromChannelArg(app, channelArg) + if channel != nil { + outgoingWebhook.ChannelId = channel.Id + } + } + + if _, err := app.CreateOutgoingWebhook(outgoingWebhook); err != nil { + return err + } + + return nil +} + func init() { WebhookCreateIncomingCmd.Flags().String("channel", "", "Channel ID") WebhookCreateIncomingCmd.Flags().String("user", "", "User ID") @@ -191,10 +276,22 @@ func init() { WebhookModifyIncomingCmd.Flags().String("icon", "", "Icon URL") WebhookModifyIncomingCmd.Flags().Bool("lock-to-channel", false, "Lock to channel") + WebhookCreateOutgoingCmd.Flags().String("team", "", "Team name or ID (required)") + WebhookCreateOutgoingCmd.Flags().String("channel", "", "Channel name or ID") + WebhookCreateOutgoingCmd.Flags().String("user", "", "User username, email, or ID (required)") + WebhookCreateOutgoingCmd.Flags().String("display-name", "", "Outgoing webhook display name (required)") + WebhookCreateOutgoingCmd.Flags().String("description", "", "Outgoing webhook description") + WebhookCreateOutgoingCmd.Flags().String("trigger-words", "", "Words to trigger webhook (word1\nword2) (required)") + WebhookCreateOutgoingCmd.Flags().Int("trigger-when", 0, "When to trigger webhook (either when trigger word is first (enter 1) or when it's anywhere (enter 0))") + WebhookCreateOutgoingCmd.Flags().String("icon", "", "Icon URL") + WebhookCreateOutgoingCmd.Flags().String("urls", "", "Callback URLs (url1\nurl2) (required)") + WebhookCreateOutgoingCmd.Flags().String("content-type", "", "Content-type") + WebhookCmd.AddCommand( WebhookListCmd, WebhookCreateIncomingCmd, WebhookModifyIncomingCmd, + WebhookCreateOutgoingCmd, ) RootCmd.AddCommand(WebhookCmd) diff --git a/cmd/mattermost/commands/webhook_test.go b/cmd/mattermost/commands/webhook_test.go index 9791096769..6da960835e 100644 --- a/cmd/mattermost/commands/webhook_test.go +++ b/cmd/mattermost/commands/webhook_test.go @@ -116,9 +116,9 @@ func TestModifyIncomingWebhook(t *testing.T) { displayName := "myhookincname" incomingWebhook := &model.IncomingWebhook{ - ChannelId: th.BasicChannel.Id, - DisplayName: displayName, - Description: description, + ChannelId: th.BasicChannel.Id, + DisplayName: displayName, + Description: description, } oldHook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, incomingWebhook) @@ -150,3 +150,62 @@ func TestModifyIncomingWebhook(t *testing.T) { t.Fatal("Failed to update incoming webhook") } } + +func TestCreateOutgoingWebhook(t *testing.T) { + th := api4.Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableIncomingWebhooks = true }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOutgoingWebhooks = true }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnablePostUsernameOverride = true }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnablePostIconOverride = true }) + + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer func() { + th.RestoreDefaultRolePermissions(defaultRolePermissions) + }() + th.AddPermissionToRole(model.PERMISSION_MANAGE_WEBHOOKS.Id, model.TEAM_ADMIN_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_WEBHOOKS.Id, model.TEAM_USER_ROLE_ID) + + // team, user, display name, trigger words, callback urls are required + team := th.BasicTeam.Id + user := th.BasicUser.Id + displayName := "totally radical webhook" + triggerWords := "build\ndefenestrate" + callbackURLs := "http://localhost:8000/my-webhook-handler\nhttp://localhost:8000/my-webhook-handler2" + + // should fail because team is not specified + require.Error(t, RunCommand(t, "webhook", "create-outgoing", "--display-name", displayName, "--trigger-words", triggerWords, "--urls", callbackURLs, "--user", user)) + + // should fail because user is not specified + require.Error(t, RunCommand(t, "webhook", "create-outgoing", "--team", team, "--display-name", displayName, "--trigger-words", triggerWords, "--urls", callbackURLs)) + + // should fail because display name is not specified + require.Error(t, RunCommand(t, "webhook", "create-outgoing", "--team", team, "--trigger-words", triggerWords, "--urls", callbackURLs, "--user", user)) + + // should fail because trigger words are not specified + require.Error(t, RunCommand(t, "webhook", "create-outgoing", "--team", team, "--display-name", displayName, "--urls", callbackURLs, "--user", user)) + + // should fail because callback URLs are not specified + require.Error(t, RunCommand(t, "webhook", "create-outgoing", "--team", team, "--display-name", displayName, "--trigger-words", triggerWords, "--user", user)) + + // should fail because outgoing webhooks cannot be made for private channels + require.Error(t, RunCommand(t, "webhook", "create-outgoing", "--team", team, "--channel", th.BasicPrivateChannel.Id, "--display-name", displayName, "--trigger-words", triggerWords, "--urls", callbackURLs, "--user", user)) + + CheckCommand(t, "webhook", "create-outgoing", "--team", team, "--channel", th.BasicChannel.Id, "--display-name", displayName, "--trigger-words", triggerWords, "--urls", callbackURLs, "--user", user) + + webhooks, err := th.App.GetOutgoingWebhooksPage(0, 1000) + if err != nil { + t.Fatal("Unable to retreive outgoing webhooks") + } + + found := false + for _, webhook := range webhooks { + if webhook.DisplayName == displayName && webhook.CreatorId == th.BasicUser.Id { + found = true + } + } + if !found { + t.Fatal("Failed to create incoming webhook") + } +} From 90f279c7d561360664639071b422618c332d0272 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Mon, 5 Nov 2018 17:39:46 +0800 Subject: [PATCH 04/23] [MM-12805] Remove ephemeral post after leaving a channel (#9772) * remove ephemeral post after leaving a channel * remove unnecessary debugging line --- app/command_leave.go | 2 +- app/command_leave_test.go | 86 +++++++++++++++++++++++++++++++++++++++ i18n/en.json | 4 -- 3 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 app/command_leave_test.go diff --git a/app/command_leave.go b/app/command_leave.go index f127947d5b..5fd84f0118 100644 --- a/app/command_leave.go +++ b/app/command_leave.go @@ -51,5 +51,5 @@ func (me *LeaveProvider) DoCommand(a *App, args *model.CommandArgs, message stri return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } - return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + model.DEFAULT_CHANNEL, Text: args.T("api.command_leave.success"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + model.DEFAULT_CHANNEL} } diff --git a/app/command_leave_test.go b/app/command_leave_test.go new file mode 100644 index 0000000000..a1c185be04 --- /dev/null +++ b/app/command_leave_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" +) + +func TestLeaveProviderDoCommand(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + lp := LeaveProvider{} + + publicChannel, _ := 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) + + privateChannel, _ := th.App.CreateChannel(&model.Channel{ + DisplayName: "BB", + Name: "aa" + model.NewId() + "a", + Type: model.CHANNEL_OPEN, + TeamId: th.BasicTeam.Id, + CreatorId: th.BasicUser.Id, + }, false) + + th.App.AddUserToTeam(th.BasicTeam.Id, th.BasicUser.Id, th.BasicUser.Id) + th.App.AddUserToChannel(th.BasicUser, publicChannel) + th.App.AddUserToChannel(th.BasicUser, privateChannel) + + args := &model.CommandArgs{ + T: func(s string, args ...interface{}) string { return s }, + } + + // Should error when no Channel ID in args + actual := lp.DoCommand(th.App, args, "") + assert.Equal(t, "api.command_leave.fail.app_error", actual.Text) + assert.Equal(t, model.COMMAND_RESPONSE_TYPE_EPHEMERAL, actual.ResponseType) + + // Should error when no Team ID in args + args.ChannelId = publicChannel.Id + actual = lp.DoCommand(th.App, args, "") + assert.Equal(t, "api.command_leave.fail.app_error", actual.Text) + assert.Equal(t, model.COMMAND_RESPONSE_TYPE_EPHEMERAL, actual.ResponseType) + + // Leave a public channel + siteURL := "http://localhost:8065" + args.TeamId = th.BasicTeam.Id + args.SiteURL = siteURL + actual = lp.DoCommand(th.App, args, "") + assert.Equal(t, "", actual.Text) + assert.Equal(t, siteURL+"/"+th.BasicTeam.Name+"/channels/"+model.DEFAULT_CHANNEL, actual.GotoLocation) + assert.Equal(t, "", actual.ResponseType) + + time.Sleep(100 * time.Millisecond) + + member, err := th.App.GetChannelMember(publicChannel.Id, th.BasicUser.Id) + if member == nil { + t.Errorf("Expected member object, got nil") + } + + if err != nil { + t.Errorf("Expected nil object, got %s", err) + } + + // Leave a private channel + args.ChannelId = privateChannel.Id + actual = lp.DoCommand(th.App, args, "") + assert.Equal(t, "", actual.Text) + + // Should not leave a default channel + defaultChannel, _ := th.App.GetChannelByName(model.DEFAULT_CHANNEL, th.BasicTeam.Id, false) + args.ChannelId = defaultChannel.Id + actual = lp.DoCommand(th.App, args, "") + assert.Equal(t, "api.channel.leave.default.app_error", actual.Text) +} diff --git a/i18n/en.json b/i18n/en.json index 70b36a1738..b98d13b471 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -694,10 +694,6 @@ "id": "api.command_leave.name", "translation": "leave" }, - { - "id": "api.command_leave.success", - "translation": "Left the channel." - }, { "id": "api.command_logout.desc", "translation": "Logout of Mattermost" From bce7a7c73de5ce7c119a1d9b1208c8c98ab9db26 Mon Sep 17 00:00:00 2001 From: Shobhit Gupta Date: Mon, 5 Nov 2018 05:18:00 -0800 Subject: [PATCH 05/23] [MM-12462] Include favorite channels in bulk export (#9692) * Include favorite channels in bulk export * Remove duplicate method --- app/export.go | 10 +++++++--- app/export_converters.go | 10 +++++++++- app/export_test.go | 17 +++++++++++++++-- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/export.go b/app/export.go index 7c1736157a..5ae02720b7 100644 --- a/app/export.go +++ b/app/export.go @@ -206,17 +206,21 @@ func (a *App) buildUserChannelMemberships(userId string, teamId string) (*[]User var memberships []UserChannelImportData result := <-a.Srv.Store.Channel().GetChannelMembersForExport(userId, teamId) - if result.Err != nil { return nil, result.Err } members := result.Data.([]*model.ChannelMemberForExport) - for _, member := range members { - memberships = append(memberships, *ImportUserChannelDataFromChannelMember(member)) + category := model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL + preferences, err := a.GetPreferenceByCategoryForUser(userId, category) + if err != nil { + return nil, err } + for _, member := range members { + memberships = append(memberships, *ImportUserChannelDataFromChannelMemberAndPreferences(member, &preferences)) + } return &memberships, nil } diff --git a/app/export_converters.go b/app/export_converters.go index a2ce20abf6..efc3a7266a 100644 --- a/app/export_converters.go +++ b/app/export_converters.go @@ -71,7 +71,7 @@ func ImportUserTeamDataFromTeamMember(member *model.TeamMemberForExport) *UserTe } } -func ImportUserChannelDataFromChannelMember(member *model.ChannelMemberForExport) *UserChannelImportData { +func ImportUserChannelDataFromChannelMemberAndPreferences(member *model.ChannelMemberForExport, preferences *model.Preferences) *UserChannelImportData { rolesList := strings.Fields(member.Roles) if member.SchemeAdmin { rolesList = append(rolesList, model.CHANNEL_ADMIN_ROLE_ID) @@ -95,11 +95,19 @@ func ImportUserChannelDataFromChannelMember(member *model.ChannelMemberForExport notifyProps.MarkUnread = &markUnread } + favorite := false + for _, preference := range *preferences { + if member.ChannelId == preference.Name { + favorite = true + } + } + roles := strings.Join(rolesList, " ") return &UserChannelImportData{ Name: &member.ChannelName, Roles: &roles, NotifyProps: ¬ifyProps, + Favorite: &favorite, } } diff --git a/app/export_test.go b/app/export_test.go index 3d12b4d505..015fde3d07 100644 --- a/app/export_test.go +++ b/app/export_test.go @@ -3,8 +3,9 @@ package app import ( "testing" - "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/require" ) @@ -60,7 +61,7 @@ func TestExportUserNotifyProps(t *testing.T) { require.Equal(t, userNotifyProps[model.MENTION_KEYS_NOTIFY_PROP], *exportNotifyProps.MentionKeys) } -func TestExportUserChannelsNotifyProps(t *testing.T) { +func TestExportUserChannels(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() channel := th.BasicChannel @@ -71,22 +72,34 @@ func TestExportUserChannelsNotifyProps(t *testing.T) { model.DESKTOP_NOTIFY_PROP: model.USER_NOTIFY_ALL, model.PUSH_NOTIFY_PROP: model.USER_NOTIFY_NONE, } + preference := model.Preference{ + UserId: user.Id, + Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + Name: channel.Id, + Value: "true", + } + var preferences model.Preferences + preferences = append(preferences, preference) channelMember := model.ChannelMember{ ChannelId: channel.Id, UserId: user.Id, } th.App.Srv.Store.Channel().SaveMember(&channelMember) + th.App.Srv.Store.Preference().Save(&preferences) th.App.UpdateChannelMemberNotifyProps(notifyProps, channel.Id, user.Id) exportData, _ := th.App.buildUserChannelMemberships(user.Id, team.Id) + assert.Equal(t, len(*exportData), 3) for _, data := range *exportData { if *data.Name == channelName { assert.Equal(t, *data.NotifyProps.Desktop, "all") assert.Equal(t, *data.NotifyProps.Mobile, "none") assert.Equal(t, *data.NotifyProps.MarkUnread, "all") // default value + assert.True(t, *data.Favorite) } else { // default values assert.Equal(t, *data.NotifyProps.Desktop, "default") assert.Equal(t, *data.NotifyProps.Mobile, "default") assert.Equal(t, *data.NotifyProps.MarkUnread, "all") + assert.False(t, *data.Favorite) } } } From 789b2f72dd6480c4528f4a1378f7a62aaaffafd6 Mon Sep 17 00:00:00 2001 From: Hanzei <16541325+hanzei@users.noreply.github.com> Date: Mon, 5 Nov 2018 14:29:25 +0100 Subject: [PATCH 06/23] GH-9737: Allow setting min_server_version in plugin manifest (#9743) * Add github.com/blang/semver as vendor * Add MinServerVersion check for plugins * Add tests for MinServerVersion in manifest * Move logic to model/manifest.go & add tests --- Gopkg.lock | 9 + Gopkg.toml | 4 + model/manifest.go | 19 + model/manifest_test.go | 66 +++- plugin/environment.go | 10 + vendor/github.com/blang/semver/.travis.yml | 21 + vendor/github.com/blang/semver/LICENSE | 22 ++ vendor/github.com/blang/semver/README.md | 194 +++++++++ vendor/github.com/blang/semver/json.go | 23 ++ vendor/github.com/blang/semver/package.json | 17 + vendor/github.com/blang/semver/range.go | 416 +++++++++++++++++++ vendor/github.com/blang/semver/semver.go | 418 ++++++++++++++++++++ vendor/github.com/blang/semver/sort.go | 28 ++ vendor/github.com/blang/semver/sql.go | 30 ++ 14 files changed, 1272 insertions(+), 5 deletions(-) create mode 100644 vendor/github.com/blang/semver/.travis.yml create mode 100644 vendor/github.com/blang/semver/LICENSE create mode 100644 vendor/github.com/blang/semver/README.md create mode 100644 vendor/github.com/blang/semver/json.go create mode 100644 vendor/github.com/blang/semver/package.json create mode 100644 vendor/github.com/blang/semver/range.go create mode 100644 vendor/github.com/blang/semver/semver.go create mode 100644 vendor/github.com/blang/semver/sort.go create mode 100644 vendor/github.com/blang/semver/sql.go diff --git a/Gopkg.lock b/Gopkg.lock index 5f4f23a91b..245ade8e6f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -33,6 +33,14 @@ pruneopts = "UT" revision = "3a771d992973f24aa725d07868b467d1ddfceafb" +[[projects]] + digest = "1:705c40022f5c03bf96ffeb6477858d88565064485a513abcd0f11a0911546cb6" + name = "github.com/blang/semver" + packages = ["."] + pruneopts = "UT" + revision = "2ee87856327ba09384cabd113bc6b5d174e9ec0f" + version = "v3.5.1" + [[projects]] branch = "master" digest = "1:cc439e1d9d8cff3d575642f5401033b00f2b8d0cd9f859db45604701c990879a" @@ -984,6 +992,7 @@ input-imports = [ "github.com/NYTimes/gziphandler", "github.com/avct/uasurfer", + "github.com/blang/semver", "github.com/dgryski/dgoogauth", "github.com/disintegration/imaging", "github.com/dyatlov/go-opengraph/opengraph", diff --git a/Gopkg.toml b/Gopkg.toml index 993e374bd8..ac196bb50a 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -54,6 +54,10 @@ name = "github.com/hashicorp/go-plugin" revision = "a4620f9913d19f03a6bf19b2f304daaaf83ea130" +[[constraint]] + name = "github.com/blang/semver" + version = "~3.5.0" + [prune] go-tests = true unused-packages = true diff --git a/model/manifest.go b/model/manifest.go index 2eb7b8e053..7cd14bfcbf 100644 --- a/model/manifest.go +++ b/model/manifest.go @@ -5,6 +5,7 @@ package model import ( "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -12,6 +13,7 @@ import ( "path/filepath" "strings" + "github.com/blang/semver" "gopkg.in/yaml.v2" ) @@ -111,6 +113,11 @@ type Manifest struct { // A version number for your plugin. Semantic versioning is recommended: http://semver.org Version string `json:"version" yaml:"version"` + // The minimum Mattermost server version required for your plugin. + // + // Minimum server version: 5.6 + MinServerVersion string `json:"min_server_version,omitempty" yaml:"min_server_version,omitempty"` + // Server defines the server-side portion of your plugin. Server *ManifestServer `json:"server,omitempty" yaml:"server,omitempty"` @@ -242,6 +249,18 @@ func (m *Manifest) HasWebapp() bool { return m.Webapp != nil } +func (m *Manifest) MeetMinServerVersion(serverVersion string) (bool, error) { + minServerVersion, err := semver.Parse(m.MinServerVersion) + if err != nil { + return false, errors.New("failed to parse MinServerVersion") + } + sv := semver.MustParse(serverVersion) + if sv.LT(minServerVersion) { + return false, nil + } + return true, nil +} + // FindManifest will find and parse the manifest in a given directory. // // In all cases other than a does-not-exist error, path is set to the path of the manifest file that was diff --git a/model/manifest_test.go b/model/manifest_test.go index 80d22a3ad7..0f31e6272b 100644 --- a/model/manifest_test.go +++ b/model/manifest_test.go @@ -63,7 +63,8 @@ func TestFindManifest(t *testing.T) { func TestManifestUnmarshal(t *testing.T) { expected := Manifest{ - Id: "theid", + Id: "theid", + MinServerVersion: "5.6.0", Server: &ManifestServer{ Executable: "theexecutable", Executables: &ManifestExecutables{ @@ -101,6 +102,7 @@ func TestManifestUnmarshal(t *testing.T) { var yamlResult Manifest require.NoError(t, yaml.Unmarshal([]byte(` id: theid +min_server_version: 5.6.0 server: executable: theexecutable executables: @@ -129,6 +131,7 @@ settings_schema: var jsonResult Manifest require.NoError(t, json.Unmarshal([]byte(`{ "id": "theid", + "min_server_version": "5.6.0", "server": { "executable": "theexecutable", "executables": { @@ -246,10 +249,11 @@ func TestManifestHasClient(t *testing.T) { func TestManifestClientManifest(t *testing.T) { manifest := &Manifest{ - Id: "theid", - Name: "thename", - Description: "thedescription", - Version: "0.0.1", + Id: "theid", + Name: "thename", + Description: "thedescription", + Version: "0.0.1", + MinServerVersion: "5.6.0", Server: &ManifestServer{ Executable: "theexecutable", }, @@ -284,6 +288,7 @@ func TestManifestClientManifest(t *testing.T) { assert.Equal(t, manifest.Id, sanitized.Id) assert.Equal(t, manifest.Version, sanitized.Version) + assert.Equal(t, manifest.MinServerVersion, sanitized.MinServerVersion) assert.Equal(t, "/static/theid/theid_000102030405060708090a0b0c0d0e0f_bundle.js", sanitized.Webapp.BundlePath) assert.Equal(t, manifest.Webapp.BundleHash, sanitized.Webapp.BundleHash) assert.Equal(t, manifest.SettingsSchema, sanitized.SettingsSchema) @@ -293,6 +298,7 @@ func TestManifestClientManifest(t *testing.T) { assert.NotEmpty(t, manifest.Id) assert.NotEmpty(t, manifest.Version) + assert.NotEmpty(t, manifest.MinServerVersion) assert.NotEmpty(t, manifest.Webapp) assert.NotEmpty(t, manifest.Name) assert.NotEmpty(t, manifest.Description) @@ -594,3 +600,53 @@ func TestManifestHasWebapp(t *testing.T) { }) } } + +func TestManifestMeetMinServerVersion(t *testing.T) { + for name, test := range map[string]struct { + MinServerVersion string + ServerVersion string + ShouldError bool + ShouldFulfill bool + }{ + "generously fulfilled": { + MinServerVersion: "5.5.0", + ServerVersion: "5.6.0", + ShouldError: false, + ShouldFulfill: true, + }, + "exactly fulfilled": { + MinServerVersion: "5.6.0", + ServerVersion: "5.6.0", + ShouldError: false, + ShouldFulfill: true, + }, + "not fulfilled": { + MinServerVersion: "5.6.0", + ServerVersion: "5.5.0", + ShouldError: false, + ShouldFulfill: false, + }, + "fail to parse MinServerVersion": { + MinServerVersion: "abc", + ServerVersion: "5.5.0", + ShouldError: true, + }, + } { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + manifest := Manifest{ + MinServerVersion: test.MinServerVersion, + } + fulfilled, err := manifest.MeetMinServerVersion(test.ServerVersion) + + if test.ShouldError { + assert.NotNil(err) + assert.False(fulfilled) + return + } + assert.Nil(err) + assert.Equal(test.ShouldFulfill, fulfilled) + }) + } +} diff --git a/plugin/environment.go b/plugin/environment.go index 94e5f2646c..d64a92ea08 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -166,6 +166,16 @@ func (env *Environment) Activate(id string) (manifest *model.Manifest, activated env.activePlugins.Store(pluginInfo.Manifest.Id, activePlugin) }() + if pluginInfo.Manifest.MinServerVersion != "" { + fulfilled, err := pluginInfo.Manifest.MeetMinServerVersion(model.CurrentVersion) + if err != nil { + return nil, false, fmt.Errorf("%v: %v", err.Error(), id) + } + if !fulfilled { + return nil, false, fmt.Errorf("plugin requires Mattermost %v: %v", pluginInfo.Manifest.MinServerVersion, id) + } + } + componentActivated := false if pluginInfo.Manifest.HasWebapp() { diff --git a/vendor/github.com/blang/semver/.travis.yml b/vendor/github.com/blang/semver/.travis.yml new file mode 100644 index 0000000000..102fb9a691 --- /dev/null +++ b/vendor/github.com/blang/semver/.travis.yml @@ -0,0 +1,21 @@ +language: go +matrix: + include: + - go: 1.4.3 + - go: 1.5.4 + - go: 1.6.3 + - go: 1.7 + - go: tip + allow_failures: + - go: tip +install: +- go get golang.org/x/tools/cmd/cover +- go get github.com/mattn/goveralls +script: +- echo "Test and track coverage" ; $HOME/gopath/bin/goveralls -package "." -service=travis-ci + -repotoken $COVERALLS_TOKEN +- echo "Build examples" ; cd examples && go build +- echo "Check if gofmt'd" ; diff -u <(echo -n) <(gofmt -d -s .) +env: + global: + secure: HroGEAUQpVq9zX1b1VIkraLiywhGbzvNnTZq2TMxgK7JHP8xqNplAeF1izrR2i4QLL9nsY+9WtYss4QuPvEtZcVHUobw6XnL6radF7jS1LgfYZ9Y7oF+zogZ2I5QUMRLGA7rcxQ05s7mKq3XZQfeqaNts4bms/eZRefWuaFZbkw= diff --git a/vendor/github.com/blang/semver/LICENSE b/vendor/github.com/blang/semver/LICENSE new file mode 100644 index 0000000000..5ba5c86fcb --- /dev/null +++ b/vendor/github.com/blang/semver/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2014 Benedikt Lang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/github.com/blang/semver/README.md b/vendor/github.com/blang/semver/README.md new file mode 100644 index 0000000000..08b2e4a3d7 --- /dev/null +++ b/vendor/github.com/blang/semver/README.md @@ -0,0 +1,194 @@ +semver for golang [![Build Status](https://travis-ci.org/blang/semver.svg?branch=master)](https://travis-ci.org/blang/semver) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) [![Coverage Status](https://img.shields.io/coveralls/blang/semver.svg)](https://coveralls.io/r/blang/semver?branch=master) +====== + +semver is a [Semantic Versioning](http://semver.org/) library written in golang. It fully covers spec version `2.0.0`. + +Usage +----- +```bash +$ go get github.com/blang/semver +``` +Note: Always vendor your dependencies or fix on a specific version tag. + +```go +import github.com/blang/semver +v1, err := semver.Make("1.0.0-beta") +v2, err := semver.Make("2.0.0-beta") +v1.Compare(v2) +``` + +Also check the [GoDocs](http://godoc.org/github.com/blang/semver). + +Why should I use this lib? +----- + +- Fully spec compatible +- No reflection +- No regex +- Fully tested (Coverage >99%) +- Readable parsing/validation errors +- Fast (See [Benchmarks](#benchmarks)) +- Only Stdlib +- Uses values instead of pointers +- Many features, see below + + +Features +----- + +- Parsing and validation at all levels +- Comparator-like comparisons +- Compare Helper Methods +- InPlace manipulation +- Ranges `>=1.0.0 <2.0.0 || >=3.0.0 !3.0.1-beta.1` +- Wildcards `>=1.x`, `<=2.5.x` +- Sortable (implements sort.Interface) +- database/sql compatible (sql.Scanner/Valuer) +- encoding/json compatible (json.Marshaler/Unmarshaler) + +Ranges +------ + +A `Range` is a set of conditions which specify which versions satisfy the range. + +A condition is composed of an operator and a version. The supported operators are: + +- `<1.0.0` Less than `1.0.0` +- `<=1.0.0` Less than or equal to `1.0.0` +- `>1.0.0` Greater than `1.0.0` +- `>=1.0.0` Greater than or equal to `1.0.0` +- `1.0.0`, `=1.0.0`, `==1.0.0` Equal to `1.0.0` +- `!1.0.0`, `!=1.0.0` Not equal to `1.0.0`. Excludes version `1.0.0`. + +Note that spaces between the operator and the version will be gracefully tolerated. + +A `Range` can link multiple `Ranges` separated by space: + +Ranges can be linked by logical AND: + + - `>1.0.0 <2.0.0` would match between both ranges, so `1.1.1` and `1.8.7` but not `1.0.0` or `2.0.0` + - `>1.0.0 <3.0.0 !2.0.3-beta.2` would match every version between `1.0.0` and `3.0.0` except `2.0.3-beta.2` + +Ranges can also be linked by logical OR: + + - `<2.0.0 || >=3.0.0` would match `1.x.x` and `3.x.x` but not `2.x.x` + +AND has a higher precedence than OR. It's not possible to use brackets. + +Ranges can be combined by both AND and OR + + - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1` + +Range usage: + +``` +v, err := semver.Parse("1.2.3") +range, err := semver.ParseRange(">1.0.0 <2.0.0 || >=3.0.0") +if range(v) { + //valid +} + +``` + +Example +----- + +Have a look at full examples in [examples/main.go](examples/main.go) + +```go +import github.com/blang/semver + +v, err := semver.Make("0.0.1-alpha.preview+123.github") +fmt.Printf("Major: %d\n", v.Major) +fmt.Printf("Minor: %d\n", v.Minor) +fmt.Printf("Patch: %d\n", v.Patch) +fmt.Printf("Pre: %s\n", v.Pre) +fmt.Printf("Build: %s\n", v.Build) + +// Prerelease versions array +if len(v.Pre) > 0 { + fmt.Println("Prerelease versions:") + for i, pre := range v.Pre { + fmt.Printf("%d: %q\n", i, pre) + } +} + +// Build meta data array +if len(v.Build) > 0 { + fmt.Println("Build meta data:") + for i, build := range v.Build { + fmt.Printf("%d: %q\n", i, build) + } +} + +v001, err := semver.Make("0.0.1") +// Compare using helpers: v.GT(v2), v.LT, v.GTE, v.LTE +v001.GT(v) == true +v.LT(v001) == true +v.GTE(v) == true +v.LTE(v) == true + +// Or use v.Compare(v2) for comparisons (-1, 0, 1): +v001.Compare(v) == 1 +v.Compare(v001) == -1 +v.Compare(v) == 0 + +// Manipulate Version in place: +v.Pre[0], err = semver.NewPRVersion("beta") +if err != nil { + fmt.Printf("Error parsing pre release version: %q", err) +} + +fmt.Println("\nValidate versions:") +v.Build[0] = "?" + +err = v.Validate() +if err != nil { + fmt.Printf("Validation failed: %s\n", err) +} +``` + + +Benchmarks +----- + + BenchmarkParseSimple-4 5000000 390 ns/op 48 B/op 1 allocs/op + BenchmarkParseComplex-4 1000000 1813 ns/op 256 B/op 7 allocs/op + BenchmarkParseAverage-4 1000000 1171 ns/op 163 B/op 4 allocs/op + BenchmarkStringSimple-4 20000000 119 ns/op 16 B/op 1 allocs/op + BenchmarkStringLarger-4 10000000 206 ns/op 32 B/op 2 allocs/op + BenchmarkStringComplex-4 5000000 324 ns/op 80 B/op 3 allocs/op + BenchmarkStringAverage-4 5000000 273 ns/op 53 B/op 2 allocs/op + BenchmarkValidateSimple-4 200000000 9.33 ns/op 0 B/op 0 allocs/op + BenchmarkValidateComplex-4 3000000 469 ns/op 0 B/op 0 allocs/op + BenchmarkValidateAverage-4 5000000 256 ns/op 0 B/op 0 allocs/op + BenchmarkCompareSimple-4 100000000 11.8 ns/op 0 B/op 0 allocs/op + BenchmarkCompareComplex-4 50000000 30.8 ns/op 0 B/op 0 allocs/op + BenchmarkCompareAverage-4 30000000 41.5 ns/op 0 B/op 0 allocs/op + BenchmarkSort-4 3000000 419 ns/op 256 B/op 2 allocs/op + BenchmarkRangeParseSimple-4 2000000 850 ns/op 192 B/op 5 allocs/op + BenchmarkRangeParseAverage-4 1000000 1677 ns/op 400 B/op 10 allocs/op + BenchmarkRangeParseComplex-4 300000 5214 ns/op 1440 B/op 30 allocs/op + BenchmarkRangeMatchSimple-4 50000000 25.6 ns/op 0 B/op 0 allocs/op + BenchmarkRangeMatchAverage-4 30000000 56.4 ns/op 0 B/op 0 allocs/op + BenchmarkRangeMatchComplex-4 10000000 153 ns/op 0 B/op 0 allocs/op + +See benchmark cases at [semver_test.go](semver_test.go) + + +Motivation +----- + +I simply couldn't find any lib supporting the full spec. Others were just wrong or used reflection and regex which i don't like. + + +Contribution +----- + +Feel free to make a pull request. For bigger changes create a issue first to discuss about it. + + +License +----- + +See [LICENSE](LICENSE) file. diff --git a/vendor/github.com/blang/semver/json.go b/vendor/github.com/blang/semver/json.go new file mode 100644 index 0000000000..a74bf7c449 --- /dev/null +++ b/vendor/github.com/blang/semver/json.go @@ -0,0 +1,23 @@ +package semver + +import ( + "encoding/json" +) + +// MarshalJSON implements the encoding/json.Marshaler interface. +func (v Version) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +// UnmarshalJSON implements the encoding/json.Unmarshaler interface. +func (v *Version) UnmarshalJSON(data []byte) (err error) { + var versionString string + + if err = json.Unmarshal(data, &versionString); err != nil { + return + } + + *v, err = Parse(versionString) + + return +} diff --git a/vendor/github.com/blang/semver/package.json b/vendor/github.com/blang/semver/package.json new file mode 100644 index 0000000000..1cf8ebdd9c --- /dev/null +++ b/vendor/github.com/blang/semver/package.json @@ -0,0 +1,17 @@ +{ + "author": "blang", + "bugs": { + "URL": "https://github.com/blang/semver/issues", + "url": "https://github.com/blang/semver/issues" + }, + "gx": { + "dvcsimport": "github.com/blang/semver" + }, + "gxVersion": "0.10.0", + "language": "go", + "license": "MIT", + "name": "semver", + "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", + "version": "3.5.1" +} + diff --git a/vendor/github.com/blang/semver/range.go b/vendor/github.com/blang/semver/range.go new file mode 100644 index 0000000000..fca406d479 --- /dev/null +++ b/vendor/github.com/blang/semver/range.go @@ -0,0 +1,416 @@ +package semver + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +type wildcardType int + +const ( + noneWildcard wildcardType = iota + majorWildcard wildcardType = 1 + minorWildcard wildcardType = 2 + patchWildcard wildcardType = 3 +) + +func wildcardTypefromInt(i int) wildcardType { + switch i { + case 1: + return majorWildcard + case 2: + return minorWildcard + case 3: + return patchWildcard + default: + return noneWildcard + } +} + +type comparator func(Version, Version) bool + +var ( + compEQ comparator = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 0 + } + compNE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) != 0 + } + compGT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 1 + } + compGE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) >= 0 + } + compLT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == -1 + } + compLE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) <= 0 + } +) + +type versionRange struct { + v Version + c comparator +} + +// rangeFunc creates a Range from the given versionRange. +func (vr *versionRange) rangeFunc() Range { + return Range(func(v Version) bool { + return vr.c(v, vr.v) + }) +} + +// Range represents a range of versions. +// A Range can be used to check if a Version satisfies it: +// +// range, err := semver.ParseRange(">1.0.0 <2.0.0") +// range(semver.MustParse("1.1.1") // returns true +type Range func(Version) bool + +// OR combines the existing Range with another Range using logical OR. +func (rf Range) OR(f Range) Range { + return Range(func(v Version) bool { + return rf(v) || f(v) + }) +} + +// AND combines the existing Range with another Range using logical AND. +func (rf Range) AND(f Range) Range { + return Range(func(v Version) bool { + return rf(v) && f(v) + }) +} + +// ParseRange parses a range and returns a Range. +// If the range could not be parsed an error is returned. +// +// Valid ranges are: +// - "<1.0.0" +// - "<=1.0.0" +// - ">1.0.0" +// - ">=1.0.0" +// - "1.0.0", "=1.0.0", "==1.0.0" +// - "!1.0.0", "!=1.0.0" +// +// A Range can consist of multiple ranges separated by space: +// Ranges can be linked by logical AND: +// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" but not "1.0.0" or "2.0.0" +// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 except 2.0.3-beta.2 +// +// Ranges can also be linked by logical OR: +// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x" +// +// AND has a higher precedence than OR. It's not possible to use brackets. +// +// Ranges can be combined by both AND and OR +// +// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1` +func ParseRange(s string) (Range, error) { + parts := splitAndTrim(s) + orParts, err := splitORParts(parts) + if err != nil { + return nil, err + } + expandedParts, err := expandWildcardVersion(orParts) + if err != nil { + return nil, err + } + var orFn Range + for _, p := range expandedParts { + var andFn Range + for _, ap := range p { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + vr, err := buildVersionRange(opStr, vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse Range %q: %s", ap, err) + } + rf := vr.rangeFunc() + + // Set function + if andFn == nil { + andFn = rf + } else { // Combine with existing function + andFn = andFn.AND(rf) + } + } + if orFn == nil { + orFn = andFn + } else { + orFn = orFn.OR(andFn) + } + + } + return orFn, nil +} + +// splitORParts splits the already cleaned parts by '||'. +// Checks for invalid positions of the operator and returns an +// error if found. +func splitORParts(parts []string) ([][]string, error) { + var ORparts [][]string + last := 0 + for i, p := range parts { + if p == "||" { + if i == 0 { + return nil, fmt.Errorf("First element in range is '||'") + } + ORparts = append(ORparts, parts[last:i]) + last = i + 1 + } + } + if last == len(parts) { + return nil, fmt.Errorf("Last element in range is '||'") + } + ORparts = append(ORparts, parts[last:]) + return ORparts, nil +} + +// buildVersionRange takes a slice of 2: operator and version +// and builds a versionRange, otherwise an error. +func buildVersionRange(opStr, vStr string) (*versionRange, error) { + c := parseComparator(opStr) + if c == nil { + return nil, fmt.Errorf("Could not parse comparator %q in %q", opStr, strings.Join([]string{opStr, vStr}, "")) + } + v, err := Parse(vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse version %q in %q: %s", vStr, strings.Join([]string{opStr, vStr}, ""), err) + } + + return &versionRange{ + v: v, + c: c, + }, nil + +} + +// inArray checks if a byte is contained in an array of bytes +func inArray(s byte, list []byte) bool { + for _, el := range list { + if el == s { + return true + } + } + return false +} + +// splitAndTrim splits a range string by spaces and cleans whitespaces +func splitAndTrim(s string) (result []string) { + last := 0 + var lastChar byte + excludeFromSplit := []byte{'>', '<', '='} + for i := 0; i < len(s); i++ { + if s[i] == ' ' && !inArray(lastChar, excludeFromSplit) { + if last < i-1 { + result = append(result, s[last:i]) + } + last = i + 1 + } else if s[i] != ' ' { + lastChar = s[i] + } + } + if last < len(s)-1 { + result = append(result, s[last:]) + } + + for i, v := range result { + result[i] = strings.Replace(v, " ", "", -1) + } + + // parts := strings.Split(s, " ") + // for _, x := range parts { + // if s := strings.TrimSpace(x); len(s) != 0 { + // result = append(result, s) + // } + // } + return +} + +// splitComparatorVersion splits the comparator from the version. +// Input must be free of leading or trailing spaces. +func splitComparatorVersion(s string) (string, string, error) { + i := strings.IndexFunc(s, unicode.IsDigit) + if i == -1 { + return "", "", fmt.Errorf("Could not get version from string: %q", s) + } + return strings.TrimSpace(s[0:i]), s[i:], nil +} + +// getWildcardType will return the type of wildcard that the +// passed version contains +func getWildcardType(vStr string) wildcardType { + parts := strings.Split(vStr, ".") + nparts := len(parts) + wildcard := parts[nparts-1] + + possibleWildcardType := wildcardTypefromInt(nparts) + if wildcard == "x" { + return possibleWildcardType + } + + return noneWildcard +} + +// createVersionFromWildcard will convert a wildcard version +// into a regular version, replacing 'x's with '0's, handling +// special cases like '1.x.x' and '1.x' +func createVersionFromWildcard(vStr string) string { + // handle 1.x.x + vStr2 := strings.Replace(vStr, ".x.x", ".x", 1) + vStr2 = strings.Replace(vStr2, ".x", ".0", 1) + parts := strings.Split(vStr2, ".") + + // handle 1.x + if len(parts) == 2 { + return vStr2 + ".0" + } + + return vStr2 +} + +// incrementMajorVersion will increment the major version +// of the passed version +func incrementMajorVersion(vStr string) (string, error) { + parts := strings.Split(vStr, ".") + i, err := strconv.Atoi(parts[0]) + if err != nil { + return "", err + } + parts[0] = strconv.Itoa(i + 1) + + return strings.Join(parts, "."), nil +} + +// incrementMajorVersion will increment the minor version +// of the passed version +func incrementMinorVersion(vStr string) (string, error) { + parts := strings.Split(vStr, ".") + i, err := strconv.Atoi(parts[1]) + if err != nil { + return "", err + } + parts[1] = strconv.Itoa(i + 1) + + return strings.Join(parts, "."), nil +} + +// expandWildcardVersion will expand wildcards inside versions +// following these rules: +// +// * when dealing with patch wildcards: +// >= 1.2.x will become >= 1.2.0 +// <= 1.2.x will become < 1.3.0 +// > 1.2.x will become >= 1.3.0 +// < 1.2.x will become < 1.2.0 +// != 1.2.x will become < 1.2.0 >= 1.3.0 +// +// * when dealing with minor wildcards: +// >= 1.x will become >= 1.0.0 +// <= 1.x will become < 2.0.0 +// > 1.x will become >= 2.0.0 +// < 1.0 will become < 1.0.0 +// != 1.x will become < 1.0.0 >= 2.0.0 +// +// * when dealing with wildcards without +// version operator: +// 1.2.x will become >= 1.2.0 < 1.3.0 +// 1.x will become >= 1.0.0 < 2.0.0 +func expandWildcardVersion(parts [][]string) ([][]string, error) { + var expandedParts [][]string + for _, p := range parts { + var newParts []string + for _, ap := range p { + if strings.Index(ap, "x") != -1 { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + + versionWildcardType := getWildcardType(vStr) + flatVersion := createVersionFromWildcard(vStr) + + var resultOperator string + var shouldIncrementVersion bool + switch opStr { + case ">": + resultOperator = ">=" + shouldIncrementVersion = true + case ">=": + resultOperator = ">=" + case "<": + resultOperator = "<" + case "<=": + resultOperator = "<" + shouldIncrementVersion = true + case "", "=", "==": + newParts = append(newParts, ">="+flatVersion) + resultOperator = "<" + shouldIncrementVersion = true + case "!=", "!": + newParts = append(newParts, "<"+flatVersion) + resultOperator = ">=" + shouldIncrementVersion = true + } + + var resultVersion string + if shouldIncrementVersion { + switch versionWildcardType { + case patchWildcard: + resultVersion, _ = incrementMinorVersion(flatVersion) + case minorWildcard: + resultVersion, _ = incrementMajorVersion(flatVersion) + } + } else { + resultVersion = flatVersion + } + + ap = resultOperator + resultVersion + } + newParts = append(newParts, ap) + } + expandedParts = append(expandedParts, newParts) + } + + return expandedParts, nil +} + +func parseComparator(s string) comparator { + switch s { + case "==": + fallthrough + case "": + fallthrough + case "=": + return compEQ + case ">": + return compGT + case ">=": + return compGE + case "<": + return compLT + case "<=": + return compLE + case "!": + fallthrough + case "!=": + return compNE + } + + return nil +} + +// MustParseRange is like ParseRange but panics if the range cannot be parsed. +func MustParseRange(s string) Range { + r, err := ParseRange(s) + if err != nil { + panic(`semver: ParseRange(` + s + `): ` + err.Error()) + } + return r +} diff --git a/vendor/github.com/blang/semver/semver.go b/vendor/github.com/blang/semver/semver.go new file mode 100644 index 0000000000..8ee0842e6a --- /dev/null +++ b/vendor/github.com/blang/semver/semver.go @@ -0,0 +1,418 @@ +package semver + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + numbers string = "0123456789" + alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + alphanum = alphas + numbers +) + +// SpecVersion is the latest fully supported spec version of semver +var SpecVersion = Version{ + Major: 2, + Minor: 0, + Patch: 0, +} + +// Version represents a semver compatible version +type Version struct { + Major uint64 + Minor uint64 + Patch uint64 + Pre []PRVersion + Build []string //No Precendence +} + +// Version to string +func (v Version) String() string { + b := make([]byte, 0, 5) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + + if len(v.Pre) > 0 { + b = append(b, '-') + b = append(b, v.Pre[0].String()...) + + for _, pre := range v.Pre[1:] { + b = append(b, '.') + b = append(b, pre.String()...) + } + } + + if len(v.Build) > 0 { + b = append(b, '+') + b = append(b, v.Build[0]...) + + for _, build := range v.Build[1:] { + b = append(b, '.') + b = append(b, build...) + } + } + + return string(b) +} + +// Equals checks if v is equal to o. +func (v Version) Equals(o Version) bool { + return (v.Compare(o) == 0) +} + +// EQ checks if v is equal to o. +func (v Version) EQ(o Version) bool { + return (v.Compare(o) == 0) +} + +// NE checks if v is not equal to o. +func (v Version) NE(o Version) bool { + return (v.Compare(o) != 0) +} + +// GT checks if v is greater than o. +func (v Version) GT(o Version) bool { + return (v.Compare(o) == 1) +} + +// GTE checks if v is greater than or equal to o. +func (v Version) GTE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// GE checks if v is greater than or equal to o. +func (v Version) GE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// LT checks if v is less than o. +func (v Version) LT(o Version) bool { + return (v.Compare(o) == -1) +} + +// LTE checks if v is less than or equal to o. +func (v Version) LTE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// LE checks if v is less than or equal to o. +func (v Version) LE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// Compare compares Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v Version) Compare(o Version) int { + if v.Major != o.Major { + if v.Major > o.Major { + return 1 + } + return -1 + } + if v.Minor != o.Minor { + if v.Minor > o.Minor { + return 1 + } + return -1 + } + if v.Patch != o.Patch { + if v.Patch > o.Patch { + return 1 + } + return -1 + } + + // Quick comparison if a version has no prerelease versions + if len(v.Pre) == 0 && len(o.Pre) == 0 { + return 0 + } else if len(v.Pre) == 0 && len(o.Pre) > 0 { + return 1 + } else if len(v.Pre) > 0 && len(o.Pre) == 0 { + return -1 + } + + i := 0 + for ; i < len(v.Pre) && i < len(o.Pre); i++ { + if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { + continue + } else if comp == 1 { + return 1 + } else { + return -1 + } + } + + // If all pr versions are the equal but one has further prversion, this one greater + if i == len(v.Pre) && i == len(o.Pre) { + return 0 + } else if i == len(v.Pre) && i < len(o.Pre) { + return -1 + } else { + return 1 + } + +} + +// Validate validates v and returns error in case +func (v Version) Validate() error { + // Major, Minor, Patch already validated using uint64 + + for _, pre := range v.Pre { + if !pre.IsNum { //Numeric prerelease versions already uint64 + if len(pre.VersionStr) == 0 { + return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) + } + if !containsOnly(pre.VersionStr, alphanum) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) + } + } + } + + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, alphanum) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) + } + } + + return nil +} + +// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error +func New(s string) (vp *Version, err error) { + v, err := Parse(s) + vp = &v + return +} + +// Make is an alias for Parse, parses version string and returns a validated Version or error +func Make(s string) (Version, error) { + return Parse(s) +} + +// ParseTolerant allows for certain version specifications that do not strictly adhere to semver +// specs to be parsed by this library. It does so by normalizing versions before passing them to +// Parse(). It currently trims spaces, removes a "v" prefix, and adds a 0 patch number to versions +// with only major and minor components specified +func ParseTolerant(s string) (Version, error) { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "v") + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + if len(parts) < 3 { + if strings.ContainsAny(parts[len(parts)-1], "+-") { + return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") + } + for len(parts) < 3 { + parts = append(parts, "0") + } + s = strings.Join(parts, ".") + } + + return Parse(s) +} + +// Parse parses version string and returns a validated Version or error +func Parse(s string) (Version, error) { + if len(s) == 0 { + return Version{}, errors.New("Version string empty") + } + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + if len(parts) != 3 { + return Version{}, errors.New("No Major.Minor.Patch elements found") + } + + // Major + if !containsOnly(parts[0], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) + } + if hasLeadingZeroes(parts[0]) { + return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) + } + major, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return Version{}, err + } + + // Minor + if !containsOnly(parts[1], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) + } + if hasLeadingZeroes(parts[1]) { + return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) + } + minor, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return Version{}, err + } + + v := Version{} + v.Major = major + v.Minor = minor + + var build, prerelease []string + patchStr := parts[2] + + if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 { + build = strings.Split(patchStr[buildIndex+1:], ".") + patchStr = patchStr[:buildIndex] + } + + if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 { + prerelease = strings.Split(patchStr[preIndex+1:], ".") + patchStr = patchStr[:preIndex] + } + + if !containsOnly(patchStr, numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr) + } + if hasLeadingZeroes(patchStr) { + return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr) + } + patch, err := strconv.ParseUint(patchStr, 10, 64) + if err != nil { + return Version{}, err + } + + v.Patch = patch + + // Prerelease + for _, prstr := range prerelease { + parsedPR, err := NewPRVersion(prstr) + if err != nil { + return Version{}, err + } + v.Pre = append(v.Pre, parsedPR) + } + + // Build meta data + for _, str := range build { + if len(str) == 0 { + return Version{}, errors.New("Build meta data is empty") + } + if !containsOnly(str, alphanum) { + return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) + } + v.Build = append(v.Build, str) + } + + return v, nil +} + +// MustParse is like Parse but panics if the version cannot be parsed. +func MustParse(s string) Version { + v, err := Parse(s) + if err != nil { + panic(`semver: Parse(` + s + `): ` + err.Error()) + } + return v +} + +// PRVersion represents a PreRelease Version +type PRVersion struct { + VersionStr string + VersionNum uint64 + IsNum bool +} + +// NewPRVersion creates a new valid prerelease version +func NewPRVersion(s string) (PRVersion, error) { + if len(s) == 0 { + return PRVersion{}, errors.New("Prerelease is empty") + } + v := PRVersion{} + if containsOnly(s, numbers) { + if hasLeadingZeroes(s) { + return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) + } + num, err := strconv.ParseUint(s, 10, 64) + + // Might never be hit, but just in case + if err != nil { + return PRVersion{}, err + } + v.VersionNum = num + v.IsNum = true + } else if containsOnly(s, alphanum) { + v.VersionStr = s + v.IsNum = false + } else { + return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s) + } + return v, nil +} + +// IsNumeric checks if prerelease-version is numeric +func (v PRVersion) IsNumeric() bool { + return v.IsNum +} + +// Compare compares two PreRelease Versions v and o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v PRVersion) Compare(o PRVersion) int { + if v.IsNum && !o.IsNum { + return -1 + } else if !v.IsNum && o.IsNum { + return 1 + } else if v.IsNum && o.IsNum { + if v.VersionNum == o.VersionNum { + return 0 + } else if v.VersionNum > o.VersionNum { + return 1 + } else { + return -1 + } + } else { // both are Alphas + if v.VersionStr == o.VersionStr { + return 0 + } else if v.VersionStr > o.VersionStr { + return 1 + } else { + return -1 + } + } +} + +// PreRelease version to string +func (v PRVersion) String() string { + if v.IsNum { + return strconv.FormatUint(v.VersionNum, 10) + } + return v.VersionStr +} + +func containsOnly(s string, set string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !strings.ContainsRune(set, r) + }) == -1 +} + +func hasLeadingZeroes(s string) bool { + return len(s) > 1 && s[0] == '0' +} + +// NewBuildVersion creates a new valid build version +func NewBuildVersion(s string) (string, error) { + if len(s) == 0 { + return "", errors.New("Buildversion is empty") + } + if !containsOnly(s, alphanum) { + return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) + } + return s, nil +} diff --git a/vendor/github.com/blang/semver/sort.go b/vendor/github.com/blang/semver/sort.go new file mode 100644 index 0000000000..e18f880826 --- /dev/null +++ b/vendor/github.com/blang/semver/sort.go @@ -0,0 +1,28 @@ +package semver + +import ( + "sort" +) + +// Versions represents multiple versions. +type Versions []Version + +// Len returns length of version collection +func (s Versions) Len() int { + return len(s) +} + +// Swap swaps two versions inside the collection by its indices +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less checks if version at index i is less than version at index j +func (s Versions) Less(i, j int) bool { + return s[i].LT(s[j]) +} + +// Sort sorts a slice of versions +func Sort(versions []Version) { + sort.Sort(Versions(versions)) +} diff --git a/vendor/github.com/blang/semver/sql.go b/vendor/github.com/blang/semver/sql.go new file mode 100644 index 0000000000..eb4d802666 --- /dev/null +++ b/vendor/github.com/blang/semver/sql.go @@ -0,0 +1,30 @@ +package semver + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements the database/sql.Scanner interface. +func (v *Version) Scan(src interface{}) (err error) { + var str string + switch src := src.(type) { + case string: + str = src + case []byte: + str = string(src) + default: + return fmt.Errorf("Version.Scan: cannot convert %T to string.", src) + } + + if t, err := Parse(str); err == nil { + *v = t + } + + return +} + +// Value implements the database/sql/driver.Valuer interface. +func (v Version) Value() (driver.Value, error) { + return v.String(), nil +} From f6b1ccbcb1403bc0eb399b236bfc8c4040f49c09 Mon Sep 17 00:00:00 2001 From: Vaibhav Thakkar Date: Mon, 5 Nov 2018 19:20:08 +0530 Subject: [PATCH 07/23] Added GETEmojilist plugin api (#9750) * Add GetEmojiList plugin api * Fixed bug in getemojilist causing build test failure * Fix linting error * Add requested changes * Fix all conflicts --- app/plugin_api.go | 4 ++++ plugin/api.go | 7 +++++++ plugin/client_rpc_generated.go | 31 +++++++++++++++++++++++++++++++ plugin/plugintest/api.go | 25 +++++++++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/app/plugin_api.go b/app/plugin_api.go index 1821a661e3..b9b06a3458 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -385,6 +385,10 @@ func (api *PluginAPI) GetProfileImage(userId string) ([]byte, *model.AppError) { return data, err } +func (api *PluginAPI) GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) { + return api.app.GetEmojiList(page, perPage, sortBy) +} + func (api *PluginAPI) GetEmojiByName(name string) (*model.Emoji, *model.AppError) { return api.app.GetEmojiByName(name) } diff --git a/plugin/api.go b/plugin/api.go index ad970cb601..8f4c5dadeb 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -259,6 +259,13 @@ type API interface { // Minimum server version: 5.6 GetProfileImage(userId string) ([]byte, *model.AppError) + // GetEmojiList returns a page of custom emoji on the system. + // + // The sortBy parameter can be: "name". + // + // Minimum server version: 5.6 + GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) + // GetEmojiByName gets an emoji by it's name. // // Minimum server version: 5.6 diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 1263b14d30..5171f3ec66 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -2432,6 +2432,37 @@ func (s *apiRPCServer) GetProfileImage(args *Z_GetProfileImageArgs, returns *Z_G return nil } +type Z_GetEmojiListArgs struct { + A string + B int + C int +} + +type Z_GetEmojiListReturns struct { + A []*model.Emoji + B *model.AppError +} + +func (g *apiRPCClient) GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) { + _args := &Z_GetEmojiListArgs{sortBy, page, perPage} + _returns := &Z_GetEmojiListReturns{} + if err := g.client.Call("Plugin.GetEmojiList", _args, _returns); err != nil { + log.Printf("RPC call to GetEmojiList API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetEmojiList(args *Z_GetEmojiListArgs, returns *Z_GetEmojiListReturns) error { + if hook, ok := s.impl.(interface { + GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetEmojiList(args.A, args.B, args.C) + } else { + return encodableError(fmt.Errorf("API GetEmojiList called but not implemented.")) + } + return nil +} + type Z_GetEmojiByNameArgs struct { A string } diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index e3ce108193..6b9184156e 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -656,6 +656,31 @@ func (_m *API) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) { return r0, r1, r2 } +// GetEmojiList provides a mock function with given fields: sortBy, page, perPage +func (_m *API) GetEmojiList(sortBy string, page int, perPage int) ([]*model.Emoji, *model.AppError) { + ret := _m.Called(sortBy, page, perPage) + + var r0 []*model.Emoji + if rf, ok := ret.Get(0).(func(string, int, int) []*model.Emoji); ok { + r0 = rf(sortBy, page, perPage) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Emoji) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok { + r1 = rf(sortBy, page, perPage) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetFileInfo provides a mock function with given fields: fileId func (_m *API) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) { ret := _m.Called(fileId) From 1aa3ceccc23d90b11722b4cdd56734f63c5cdb05 Mon Sep 17 00:00:00 2001 From: Jason Mojica Date: Mon, 5 Nov 2018 14:55:31 +0000 Subject: [PATCH 08/23] MM-12355: Add CLI command "command create" revision (#9734) * Check for admin only setting and user admin status. If "EnableOnlyAdminIntegrations" is true, will only allow team admins to create slash commands * Add test for non-admin user * Simplify permissions check * Change error message * Fix test --- cmd/mattermost/commands/command.go | 17 +++++++++---- cmd/mattermost/commands/command_test.go | 32 +++++++++++++++---------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/cmd/mattermost/commands/command.go b/cmd/mattermost/commands/command.go index bd0bed1fd0..02bdd0c826 100644 --- a/cmd/mattermost/commands/command.go +++ b/cmd/mattermost/commands/command.go @@ -90,6 +90,18 @@ func createCommandCmdF(command *cobra.Command, args []string) error { return errors.New("unable to find team '" + args[0] + "'") } + // get the creator + creator, _ := command.Flags().GetString("creator") + user := getUserFromUserArg(a, creator) + if user == nil { + return errors.New("unable to find user '" + creator + "'") + } + + // check if creator has permission to create slash commands + if !a.HasPermissionToTeam(user.Id, team.Id, model.PERMISSION_MANAGE_SLASH_COMMANDS) { + return errors.New("the creator must be a user who has permissions to manage slash commands") + } + title, _ := command.Flags().GetString("title") description, _ := command.Flags().GetString("description") trigger, _ := command.Flags().GetString("trigger-word") @@ -102,11 +114,6 @@ func createCommandCmdF(command *cobra.Command, args []string) error { } url, _ := command.Flags().GetString("url") - creator, _ := command.Flags().GetString("creator") - user := getUserFromUserArg(a, creator) - if user == nil { - return errors.New("unable to find user '" + creator + "'") - } responseUsername, _ := command.Flags().GetString("response-username") icon, _ := command.Flags().GetString("icon") autocomplete, _ := command.Flags().GetBool("autocomplete") diff --git a/cmd/mattermost/commands/command_test.go b/cmd/mattermost/commands/command_test.go index 73ebacd083..bc6c7304ec 100644 --- a/cmd/mattermost/commands/command_test.go +++ b/cmd/mattermost/commands/command_test.go @@ -19,6 +19,7 @@ func TestCreateCommand(t *testing.T) { th.InitSystemAdmin() defer th.TearDown() team := th.BasicTeam + adminUser := th.TeamAdminUser user := th.BasicUser testCases := []struct { @@ -28,17 +29,17 @@ func TestCreateCommand(t *testing.T) { }{ { "nil error", - []string{"command", "create", team.Name, "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "", }, { "Team not specified", - []string{"command", "create", "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "Error: requires at least 1 arg(s), only received 0", }, { "Team not found", - []string{"command", "create", "fakeTeam", "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", "fakeTeam", "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "Error: unable to find team", }, { @@ -51,54 +52,59 @@ func TestCreateCommand(t *testing.T) { []string{"command", "create", team.Name, "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", "fakeuser"}, "unable to find user", }, + { + "Creator not team admin", + []string{"command", "create", team.Name, "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + "the creator must be a user who has permissions to manage slash commands", + }, { "Command not specified", - []string{"command", "", team.Name, "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "", team.Name, "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "Error: unknown flag: --trigger-word", }, { "Trigger not specified", - []string{"command", "create", team.Name, "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", team.Name, "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, `Error: required flag(s) "trigger-word" not set`, }, { "Blank trigger", - []string{"command", "create", team.Name, "--trigger-word", "", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-word", "", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "Invalid trigger", }, { "Trigger with space", - []string{"command", "create", team.Name, "--trigger-word", "test cmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-word", "test cmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "Error: a trigger word must not contain spaces", }, { "Trigger starting with /", - []string{"command", "create", team.Name, "--trigger-word", "/testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-word", "/testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "Error: a trigger word cannot begin with a /", }, { "URL not specified", - []string{"command", "create", team.Name, "--trigger-word", "testcmd", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-word", "testcmd", "--creator", adminUser.Username}, `Error: required flag(s) "url" not set`, }, { "Blank URL", - []string{"command", "create", team.Name, "--trigger-word", "testcmd2", "--url", "", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-word", "testcmd2", "--url", "", "--creator", adminUser.Username}, "Invalid URL", }, { "Invalid URL", - []string{"command", "create", team.Name, "--trigger-word", "testcmd2", "--url", "localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-word", "testcmd2", "--url", "localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "Invalid URL", }, { "Duplicate Command", - []string{"command", "create", team.Name, "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-word", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "This trigger word is already in use", }, { "Misspelled flag", - []string{"command", "create", team.Name, "--trigger-wor", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", user.Username}, + []string{"command", "create", team.Name, "--trigger-wor", "testcmd", "--url", "http://localhost:8000/my-slash-handler", "--creator", adminUser.Username}, "Error: unknown flag:", }, } From 46dd24333106aca13fe2abb1a564c18fd16ac4ac Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Mon, 5 Nov 2018 21:38:30 +0100 Subject: [PATCH 09/23] upgrade to 5.5 (#9798) * upgrade to 5.5 * add 5.6 upgrade db --- model/version.go | 1 + store/sqlstore/upgrade.go | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/model/version.go b/model/version.go index 000d754d81..1b09b0ad3b 100644 --- a/model/version.go +++ b/model/version.go @@ -13,6 +13,7 @@ import ( // It should be maintained in chronological order with most current // release at the front of the list. var versions = []string{ + "5.5.0", "5.4.0", "5.3.0", "5.2.0", diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 9898d5b322..26f0ed88db 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -15,6 +15,7 @@ import ( ) const ( + VERSION_5_6_0 = "5.6.0" VERSION_5_5_0 = "5.5.0" VERSION_5_4_0 = "5.4.0" VERSION_5_3_0 = "5.3.0" @@ -88,6 +89,7 @@ func UpgradeDatabase(sqlStore SqlStore) { UpgradeDatabaseToVersion53(sqlStore) UpgradeDatabaseToVersion54(sqlStore) UpgradeDatabaseToVersion55(sqlStore) + UpgradeDatabaseToVersion56(sqlStore) // If the SchemaVersion is empty this this is the first time it has ran // so lets set it to the current version. @@ -508,9 +510,15 @@ func UpgradeDatabaseToVersion54(sqlStore SqlStore) { } func UpgradeDatabaseToVersion55(sqlStore SqlStore) { - // TODO: Uncomment following condition when version 5.5.0 is released - // if shouldPerformUpgrade(sqlStore, VERSION_5_4_0, VERSION_5_5_0) { - sqlStore.CreateColumnIfNotExists("PluginKeyValueStore", "ExpireAt", "bigint(20)", "bigint", "0") - // saveSchemaVersion(sqlStore, VERSION_5_5_0) - // } + if shouldPerformUpgrade(sqlStore, VERSION_5_4_0, VERSION_5_5_0) { + saveSchemaVersion(sqlStore, VERSION_5_5_0) + } +} + +func UpgradeDatabaseToVersion56(sqlStore SqlStore) { + // TODO: Uncomment following condition when version 5.5.0 is released + //if shouldPerformUpgrade(sqlStore, VERSION_5_5_0, VERSION_5_6_0) { + sqlStore.CreateColumnIfNotExists("PluginKeyValueStore", "ExpireAt", "bigint(20)", "bigint", "0") + // saveSchemaVersion(sqlStore, VERSION_5_5_0) + //} } From 418a0ec10ee6410013ddd87042ebc4d50033fd8f Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Tue, 6 Nov 2018 00:28:55 -0800 Subject: [PATCH 10/23] Fixing formatting. (#9801) --- api4/terms_of_service.go | 3 +- api4/terms_of_service_test.go | 3 +- app/extension.go | 3 +- app/plugin_requests.go | 3 +- app/server_test.go | 17 ++++++----- app/webhook_test.go | 3 +- cmd/mattermost/commands/config_test.go | 29 +++++++++---------- cmd/mattermost/commands/webhook_test.go | 3 +- model/terms_of_service_test.go | 3 +- plugin/valid_test.go | 4 +-- store/sqlstore/terms_of_service_store.go | 3 +- store/sqlstore/terms_of_service_store_test.go | 3 +- store/storetest/terms_of_service_store.go | 3 +- utils/testutils/mocked_http_service.go | 3 +- 14 files changed, 46 insertions(+), 37 deletions(-) diff --git a/api4/terms_of_service.go b/api4/terms_of_service.go index de3f7499bf..8272d3c86e 100644 --- a/api4/terms_of_service.go +++ b/api4/terms_of_service.go @@ -4,9 +4,10 @@ package api4 import ( + "net/http" + "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" - "net/http" ) func (api *API) InitTermsOfService() { diff --git a/api4/terms_of_service_test.go b/api4/terms_of_service_test.go index c1709df2b6..d8745e975b 100644 --- a/api4/terms_of_service_test.go +++ b/api4/terms_of_service_test.go @@ -1,9 +1,10 @@ package api4 import ( + "testing" + "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/assert" - "testing" ) func TestGetTermsOfService(t *testing.T) { diff --git a/app/extension.go b/app/extension.go index d0226bba58..787537cf7c 100644 --- a/app/extension.go +++ b/app/extension.go @@ -4,9 +4,10 @@ package app import ( - "github.com/mattermost/mattermost-server/model" "html/template" "net/http" + + "github.com/mattermost/mattermost-server/model" ) func (a *App) isExtensionSupportEnabled() bool { diff --git a/app/plugin_requests.go b/app/plugin_requests.go index ec60910176..cf1033ce4a 100644 --- a/app/plugin_requests.go +++ b/app/plugin_requests.go @@ -9,12 +9,13 @@ import ( "strings" "bytes" + "io/ioutil" + "github.com/gorilla/mux" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/utils" - "io/ioutil" ) func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) { diff --git a/app/server_test.go b/app/server_test.go index 4a355e1134..b99e698513 100644 --- a/app/server_test.go +++ b/app/server_test.go @@ -5,13 +5,14 @@ package app import ( "crypto/tls" - "github.com/mattermost/mattermost-server/utils" "net/http" "path" "strconv" "strings" "testing" + "github.com/mattermost/mattermost-server/utils" + "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/require" ) @@ -24,7 +25,7 @@ func TestStartServerSuccess(t *testing.T) { serverErr := a.StartServer() client := &http.Client{} - checkEndpoint(t, client, "http://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + checkEndpoint(t, client, "http://localhost:"+strconv.Itoa(a.Srv.ListenAddr.Port)+"/", http.StatusNotFound) a.Shutdown() require.NoError(t, serverErr) @@ -77,7 +78,7 @@ func TestStartServerTLSSuccess(t *testing.T) { } client := &http.Client{Transport: tr} - checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(a.Srv.ListenAddr.Port)+"/", http.StatusNotFound) a.Shutdown() require.NoError(t, serverErr) @@ -100,12 +101,12 @@ func TestStartServerTLSVersion(t *testing.T) { tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, - MaxVersion: tls.VersionTLS11, + MaxVersion: tls.VersionTLS11, }, } client := &http.Client{Transport: tr} - err = checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + err = checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(a.Srv.ListenAddr.Port)+"/", http.StatusNotFound) if !strings.Contains(err.Error(), "remote error: tls: protocol version not supported") { t.Errorf("Expected protocol version error, got %s", err) @@ -117,7 +118,7 @@ func TestStartServerTLSVersion(t *testing.T) { }, } - err = checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + err = checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(a.Srv.ListenAddr.Port)+"/", http.StatusNotFound) if err != nil { t.Errorf("Expected nil, got %s", err) @@ -154,7 +155,7 @@ func TestStartServerTLSOverwriteCipher(t *testing.T) { } client := &http.Client{Transport: tr} - err = checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + err = checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(a.Srv.ListenAddr.Port)+"/", http.StatusNotFound) if !strings.Contains(err.Error(), "remote error: tls: handshake failure") { t.Errorf("Expected protocol version error, got %s", err) @@ -170,7 +171,7 @@ func TestStartServerTLSOverwriteCipher(t *testing.T) { }, } - err = checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + err = checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(a.Srv.ListenAddr.Port)+"/", http.StatusNotFound) if err != nil { t.Errorf("Expected nil, got %s", err) diff --git a/app/webhook_test.go b/app/webhook_test.go index 85c52b1448..d0cd9d3a0b 100644 --- a/app/webhook_test.go +++ b/app/webhook_test.go @@ -10,10 +10,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-server/model" "net/http" "net/http/httptest" "time" + + "github.com/mattermost/mattermost-server/model" ) func TestCreateIncomingWebhookForChannel(t *testing.T) { diff --git a/cmd/mattermost/commands/config_test.go b/cmd/mattermost/commands/config_test.go index 8982d1ce77..f4514924e9 100644 --- a/cmd/mattermost/commands/config_test.go +++ b/cmd/mattermost/commands/config_test.go @@ -56,19 +56,18 @@ type TestClientRequirements struct { type TestNewConfig struct { TestNewServiceSettings TestNewServiceSettings - TestNewTeamSettings TestNewTeamSettings + TestNewTeamSettings TestNewTeamSettings } -type TestNewServiceSettings struct{ - SiteUrl *string - UseLetsEncrypt *bool - TLSStrictTransportMaxAge *int64 - AllowedThemes []string +type TestNewServiceSettings struct { + SiteUrl *string + UseLetsEncrypt *bool + TLSStrictTransportMaxAge *int64 + AllowedThemes []string } - type TestNewTeamSettings struct { - SiteName *string + SiteName *string MaxUserPerTeam *int } @@ -469,16 +468,15 @@ func TestUpdateMap(t *testing.T) { }, } - // create a map of type map[string]interface configMap := configToMap(config) - cases := []struct{ - Name string + cases := []struct { + Name string configSettings []string - newVal []string - expected interface{} - } { + newVal []string + expected interface{} + }{ { Name: "check for Map and string", configSettings: []string{"TestNewServiceSettings", "SiteUrl"}, @@ -517,10 +515,9 @@ func TestUpdateMap(t *testing.T) { }, } - for _, test := range cases { - t.Run(test.Name, func(t *testing.T){ + t.Run(test.Name, func(t *testing.T) { err := UpdateMap(configMap, test.configSettings, test.newVal) if err != nil { diff --git a/cmd/mattermost/commands/webhook_test.go b/cmd/mattermost/commands/webhook_test.go index 6da960835e..fc4bf12281 100644 --- a/cmd/mattermost/commands/webhook_test.go +++ b/cmd/mattermost/commands/webhook_test.go @@ -4,11 +4,12 @@ package commands import ( - "github.com/stretchr/testify/require" "strconv" "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/mattermost/mattermost-server/api4" "github.com/mattermost/mattermost-server/model" ) diff --git a/model/terms_of_service_test.go b/model/terms_of_service_test.go index 134172a61d..b9f19d0c16 100644 --- a/model/terms_of_service_test.go +++ b/model/terms_of_service_test.go @@ -4,9 +4,10 @@ package model import ( - "github.com/stretchr/testify/assert" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestTermsOfServiceIsValid(t *testing.T) { diff --git a/plugin/valid_test.go b/plugin/valid_test.go index d47eeb58b1..d166fd49f2 100644 --- a/plugin/valid_test.go +++ b/plugin/valid_test.go @@ -18,8 +18,8 @@ func TestIsValid(t *testing.T) { "abc": true, "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij": true, "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij1": false, - "../path": false, - "/etc/passwd": false, + "../path": false, + "/etc/passwd": false, "com.mattermost.plugin_with_features-0.9": true, "PLUGINS-THAT-YELL-ARE-OK-2": true, } diff --git a/store/sqlstore/terms_of_service_store.go b/store/sqlstore/terms_of_service_store.go index dc9ce5b5c1..47557ee8ce 100644 --- a/store/sqlstore/terms_of_service_store.go +++ b/store/sqlstore/terms_of_service_store.go @@ -5,11 +5,12 @@ package sqlstore import ( "database/sql" + "net/http" + "github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/utils" - "net/http" ) type SqlTermsOfServiceStore struct { diff --git a/store/sqlstore/terms_of_service_store_test.go b/store/sqlstore/terms_of_service_store_test.go index c41fe72103..8f757aecbe 100644 --- a/store/sqlstore/terms_of_service_store_test.go +++ b/store/sqlstore/terms_of_service_store_test.go @@ -1,8 +1,9 @@ package sqlstore import ( - "github.com/mattermost/mattermost-server/store/storetest" "testing" + + "github.com/mattermost/mattermost-server/store/storetest" ) func TestTermsOfServiceStore(t *testing.T) { diff --git a/store/storetest/terms_of_service_store.go b/store/storetest/terms_of_service_store.go index 90af5c1ee5..9743721942 100644 --- a/store/storetest/terms_of_service_store.go +++ b/store/storetest/terms_of_service_store.go @@ -4,10 +4,11 @@ package storetest import ( + "testing" + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" "github.com/stretchr/testify/assert" - "testing" ) func TestTermsOfServiceStore(t *testing.T, ss store.Store) { diff --git a/utils/testutils/mocked_http_service.go b/utils/testutils/mocked_http_service.go index 8f9bc42be9..9b083ef083 100644 --- a/utils/testutils/mocked_http_service.go +++ b/utils/testutils/mocked_http_service.go @@ -4,9 +4,10 @@ package testutils import ( - "github.com/mattermost/mattermost-server/services/httpservice" "net/http" "net/http/httptest" + + "github.com/mattermost/mattermost-server/services/httpservice" ) type MockedHTTPService struct { From 0dcbecac8701b2c57357616b6469ca742bd7571a Mon Sep 17 00:00:00 2001 From: Wasim Thabraze Date: Tue, 6 Nov 2018 16:50:49 +0530 Subject: [PATCH 11/23] Added MinimumNArgs to required commands in channels.go and team.go (#9792) Removed arguments length comparison as MinimumNArgs would handle it. Addressed code review --- cmd/mattermost/commands/channel.go | 40 ++++++------------------------ cmd/mattermost/commands/team.go | 15 +++-------- 2 files changed, 11 insertions(+), 44 deletions(-) diff --git a/cmd/mattermost/commands/channel.go b/cmd/mattermost/commands/channel.go index 82fb1e966f..ff3e18af31 100644 --- a/cmd/mattermost/commands/channel.go +++ b/cmd/mattermost/commands/channel.go @@ -31,6 +31,7 @@ var ChannelRenameCmd = &cobra.Command{ Short: "Rename a channel", Long: `Rename a channel.`, Example: `" channel rename myteam:mychannel newchannelname --display_name "New Display Name"`, + Args: cobra.MinimumNArgs(2), RunE: renameChannelCmdF, } @@ -48,6 +49,7 @@ var AddChannelUsersCmd = &cobra.Command{ Short: "Add users to channel", Long: "Add some users to channel", Example: " channel add myteam:mychannel user@example.com username", + Args: cobra.MinimumNArgs(2), RunE: addChannelUsersCmdF, } @@ -58,6 +60,7 @@ var ArchiveChannelsCmd = &cobra.Command{ Archive a channel along with all related information including posts from the database. Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, Example: " channel archive myteam:mychannel", + Args: cobra.MinimumNArgs(1), RunE: archiveChannelsCmdF, } @@ -68,6 +71,7 @@ var DeleteChannelsCmd = &cobra.Command{ Permanently deletes a channel along with all related information including posts from the database. Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, Example: " channel delete myteam:mychannel", + Args: cobra.MinimumNArgs(1), RunE: deleteChannelsCmdF, } @@ -77,6 +81,7 @@ var ListChannelsCmd = &cobra.Command{ Long: `List all channels on specified teams. Archived channels are appended with ' (archived)'.`, Example: " channel list myteam", + Args: cobra.MinimumNArgs(1), RunE: listChannelsCmdF, } @@ -87,6 +92,7 @@ var MoveChannelsCmd = &cobra.Command{ Validates that all users in the channel belong to the target team. Incoming/Outgoing webhooks are moved along with the channel. Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, Example: " channel move newteam oldteam:mychannel --username myusername", + Args: cobra.MinimumNArgs(2), RunE: moveChannelsCmdF, } @@ -96,6 +102,7 @@ var RestoreChannelsCmd = &cobra.Command{ Long: `Restore a previously deleted channel Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, Example: " channel restore myteam:mychannel", + Args: cobra.MinimumNArgs(1), RunE: restoreChannelsCmdF, } @@ -105,6 +112,7 @@ var ModifyChannelCmd = &cobra.Command{ Long: `Change the public/private type of a channel. Channel can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, Example: " channel modify myteam:mychannel --private --username myusername", + Args: cobra.MinimumNArgs(1), RunE: modifyChannelCmdF, } @@ -252,10 +260,6 @@ func addChannelUsersCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 2 { - return errors.New("Not enough arguments.") - } - channel := getChannelFromChannelArg(a, args[0]) if channel == nil { return errors.New("Unable to find channel '" + args[0] + "'") @@ -286,10 +290,6 @@ func archiveChannelsCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 1 { - return errors.New("Enter at least one channel to archive.") - } - channels := getChannelsFromChannelArgs(a, args) for i, channel := range channels { if channel == nil { @@ -311,10 +311,6 @@ func deleteChannelsCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 1 { - return errors.New("Enter at least one channel to delete.") - } - confirmFlag, _ := command.Flags().GetBool("confirm") if !confirmFlag { var confirm string @@ -352,10 +348,6 @@ func moveChannelsCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 2 { - return errors.New("Enter the destination team and at least one channel to move.") - } - team := getTeamFromTeamArg(a, args[0]) if team == nil { return errors.New("Unable to find destination team '" + args[0] + "'") @@ -429,10 +421,6 @@ func listChannelsCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 1 { - return errors.New("Enter at least one team.") - } - teams := getTeamsFromTeamArgs(a, args) for i, team := range teams { if team == nil { @@ -464,10 +452,6 @@ func restoreChannelsCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 1 { - return errors.New("Enter at least one channel.") - } - channels := getChannelsFromChannelArgs(a, args) for i, channel := range channels { if channel == nil { @@ -489,10 +473,6 @@ func modifyChannelCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) != 1 { - return errors.New("Enter at one channel to modify.") - } - username, erru := command.Flags().GetString("username") if erru != nil || username == "" { return errors.New("Username is required.") @@ -535,10 +515,6 @@ func renameChannelCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 2 { - return errors.New("Not enough arguments.") - } - channel := getChannelFromChannelArg(a, args[0]) if channel == nil { return errors.New("Unable to find channel '" + args[0] + "'") diff --git a/cmd/mattermost/commands/team.go b/cmd/mattermost/commands/team.go index 151795f582..525ae76e3d 100644 --- a/cmd/mattermost/commands/team.go +++ b/cmd/mattermost/commands/team.go @@ -32,6 +32,7 @@ var RemoveUsersCmd = &cobra.Command{ Short: "Remove users from team", Long: "Remove some users from team", Example: " team remove myteam user@example.com username", + Args: cobra.MinimumNArgs(2), RunE: removeUsersCmdF, } @@ -40,6 +41,7 @@ var AddUsersCmd = &cobra.Command{ Short: "Add users to team", Long: "Add some users to team", Example: " team add myteam user@example.com username", + Args: cobra.MinimumNArgs(2), RunE: addUsersCmdF, } @@ -49,6 +51,7 @@ var DeleteTeamsCmd = &cobra.Command{ Long: `Permanently delete some teams. Permanently deletes a team along with all related information including posts from the database.`, Example: " team delete myteam", + Args: cobra.MinimumNArgs(1), RunE: deleteTeamsCmdF, } @@ -142,10 +145,6 @@ func removeUsersCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 2 { - return errors.New("Not enough arguments.") - } - team := getTeamFromTeamArg(a, args[0]) if team == nil { return errors.New("Unable to find team '" + args[0] + "'") @@ -176,10 +175,6 @@ func addUsersCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 2 { - return errors.New("Not enough arguments.") - } - team := getTeamFromTeamArg(a, args[0]) if team == nil { return errors.New("Unable to find team '" + args[0] + "'") @@ -210,10 +205,6 @@ func deleteTeamsCmdF(command *cobra.Command, args []string) error { } defer a.Shutdown() - if len(args) < 1 { - return errors.New("Not enough arguments.") - } - confirmFlag, _ := command.Flags().GetBool("confirm") if !confirmFlag { var confirm string From ecade2f1ecf457277a8b7e52fc80323dbfb90e08 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Wed, 7 Nov 2018 10:20:07 -0800 Subject: [PATCH 12/23] MM-12849 Moving all non request scoped items to Server struct (#9806) * Moving goroutine pool * Auto refactor * Moving plugins. * Auto refactor * Moving fields to server * Auto refactor * Removing siteurl duplication. * Moving reset of app fields * Auto refactor * Formatting * Moving niling of Server to after last use * Fixing unit tests. --- api4/user.go | 2 +- app/admin.go | 6 +- app/app.go | 178 ++++++++------------------- app/app_test.go | 12 +- app/channel.go | 26 ++-- app/cluster.go | 8 +- app/command_echo.go | 2 +- app/compliance.go | 2 +- app/config.go | 65 +++++----- app/config_test.go | 5 +- app/diagnostics.go | 2 +- app/email.go | 6 +- app/email_batching.go | 10 +- app/file.go | 2 +- app/job.go | 4 +- app/ldap.go | 6 +- app/license.go | 26 ++-- app/login.go | 6 +- app/notification.go | 4 +- app/notification_email.go | 2 +- app/notification_push.go | 12 +- app/oauth.go | 4 +- app/options.go | 8 +- app/plugin.go | 50 ++++---- app/plugin_api_test.go | 6 +- app/plugin_commands.go | 38 +++--- app/plugin_hooks_test.go | 2 +- app/plugin_install.go | 12 +- app/plugin_requests.go | 4 +- app/plugin_statuses.go | 4 +- app/post.go | 24 ++-- app/post_test.go | 4 +- app/reaction.go | 4 +- app/role.go | 2 +- app/scheme.go | 4 +- app/server.go | 79 ++++++++++++ app/session.go | 12 +- app/session_test.go | 6 +- app/team.go | 8 +- app/timezone.go | 4 +- app/user.go | 10 +- app/web_conn.go | 6 +- app/web_hub.go | 36 +++--- app/webhook.go | 4 +- app/websocket_router.go | 2 +- cmd/mattermost/commands/jobserver.go | 8 +- cmd/mattermost/commands/server.go | 18 +-- migrations/scheduler.go | 4 +- migrations/worker.go | 12 +- plugin/scheduler/scheduler.go | 2 +- plugin/scheduler/worker.go | 6 +- web/saml.go | 4 +- 52 files changed, 388 insertions(+), 385 deletions(-) diff --git a/api4/user.go b/api4/user.go index b827857a77..fe9d331c33 100644 --- a/api4/user.go +++ b/api4/user.go @@ -845,7 +845,7 @@ func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAuditWithUserId(user.Id, fmt.Sprintf("active=%v", active)) if isSelfDeactive { - c.App.Go(func() { + c.App.Srv.Go(func() { if err = c.App.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil { mlog.Error(err.Error()) } diff --git a/app/admin.go b/app/admin.go index 6055803a5b..ba4bd2581a 100644 --- a/app/admin.go +++ b/app/admin.go @@ -139,7 +139,7 @@ func (a *App) InvalidateAllCaches() *model.AppError { func (a *App) InvalidateAllCachesSkipSend() { mlog.Info("Purging all caches") - a.sessionCache.Purge() + a.Srv.sessionCache.Purge() ClearStatusCache() a.Srv.Store.Channel().ClearCaches() a.Srv.Store.User().ClearCaches() @@ -212,8 +212,8 @@ func (a *App) RecycleDatabaseConnection() { oldStore := a.Srv.Store mlog.Warn("Attempting to recycle the database connection.") - a.Srv.Store = a.newStore() - a.Jobs.Store = a.Srv.Store + a.Srv.Store = a.Srv.newStore() + a.Srv.Jobs.Store = a.Srv.Store if a.Srv.Store != oldStore { time.Sleep(20 * time.Second) diff --git a/app/app.go b/app/app.go index e3a919bba4..f14f915e94 100644 --- a/app/app.go +++ b/app/app.go @@ -4,19 +4,15 @@ package app import ( - "crypto/ecdsa" "fmt" "html/template" "net/http" "path" "reflect" "strconv" - "sync" - "sync/atomic" "github.com/gorilla/mux" "github.com/pkg/errors" - "github.com/throttled/throttled" "github.com/mattermost/mattermost-server/einterfaces" ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs" @@ -24,7 +20,6 @@ import ( tjobs "github.com/mattermost/mattermost-server/jobs/interfaces" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/services/httpservice" "github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/store/sqlstore" @@ -35,26 +30,10 @@ const ADVANCED_PERMISSIONS_MIGRATION_KEY = "AdvancedPermissionsMigrationComplete const EMOJIS_PERMISSIONS_MIGRATION_KEY = "EmojisPermissionsMigrationComplete" type App struct { - goroutineCount int32 - goroutineExitSignal chan struct{} - Srv *Server Log *mlog.Logger - Plugins *plugin.Environment - PluginConfigListenerId string - - EmailBatching *EmailBatchingJob - EmailRateLimiter *throttled.GCRARateLimiter - - Hubs []*Hub - HubsStopCheckingForDeadlock chan bool - - PushNotificationsHub PushNotificationsHub - - Jobs *jobs.JobServer - AccountMigration einterfaces.AccountMigrationInterface Cluster einterfaces.ClusterInterface Compliance einterfaces.ComplianceInterface @@ -66,42 +45,6 @@ type App struct { Mfa einterfaces.MfaInterface Saml einterfaces.SamlInterface - config atomic.Value - envConfig map[string]interface{} - configFile string - configListeners map[string]func(*model.Config, *model.Config) - clusterLeaderListeners sync.Map - - licenseValue atomic.Value - clientLicenseValue atomic.Value - licenseListeners map[string]func() - - timezones atomic.Value - - siteURL string - - newStore func() store.Store - - htmlTemplateWatcher *utils.HTMLTemplateWatcher - sessionCache *utils.Cache - configListenerId string - licenseListenerId string - logListenerId string - clusterLeaderListenerId string - disableConfigWatch bool - configWatcher *utils.ConfigWatcher - asymmetricSigningKey *ecdsa.PrivateKey - - pluginCommands []*PluginCommand - pluginCommandsLock sync.RWMutex - - clientConfig map[string]string - clientConfigHash string - limitedClientConfig map[string]string - diagnosticId string - - phase2PermissionsMigrationComplete bool - HTTPService httpservice.HTTPService } @@ -118,15 +61,15 @@ func New(options ...Option) (outApp *App, outErr error) { rootRouter := mux.NewRouter() app := &App{ - goroutineExitSignal: make(chan struct{}, 1), Srv: &Server{ - RootRouter: rootRouter, + goroutineExitSignal: make(chan struct{}, 1), + RootRouter: rootRouter, + configFile: "config.json", + configListeners: make(map[string]func(*model.Config, *model.Config)), + licenseListeners: map[string]func(){}, + sessionCache: utils.NewLru(model.SESSION_CACHE_SIZE), + clientConfig: make(map[string]string), }, - sessionCache: utils.NewLru(model.SESSION_CACHE_SIZE), - configFile: "config.json", - configListeners: make(map[string]func(*model.Config, *model.Config)), - clientConfig: make(map[string]string), - licenseListeners: map[string]func(){}, } app.HTTPService = httpservice.MakeHTTPService(app) @@ -151,7 +94,7 @@ func New(options ...Option) (outApp *App, outErr error) { } model.AppErrorInit(utils.T) - if err := app.LoadConfig(app.configFile); err != nil { + if err := app.LoadConfig(app.Srv.configFile); err != nil { return nil, err } @@ -164,7 +107,7 @@ func New(options ...Option) (outApp *App, outErr error) { // Use this app logger as the global logger (eventually remove all instances of global logging) mlog.InitGlobalLogger(app.Log) - app.logListenerId = app.AddConfigListener(func(_, after *model.Config) { + app.Srv.logListenerId = app.AddConfigListener(func(_, after *model.Config) { app.Log.ChangeLevels(utils.MloggerConfigFromLoggerConfig(&after.LogSettings)) }) @@ -176,22 +119,22 @@ func New(options ...Option) (outApp *App, outErr error) { return nil, errors.Wrapf(err, "unable to load Mattermost translation files") } - app.configListenerId = app.AddConfigListener(func(_, _ *model.Config) { + app.Srv.configListenerId = app.AddConfigListener(func(_, _ *model.Config) { app.configOrLicenseListener() message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CONFIG_CHANGED, "", "", "", nil) message.Add("config", app.ClientConfigWithComputed()) - app.Go(func() { + app.Srv.Go(func() { app.Publish(message) }) }) - app.licenseListenerId = app.AddLicenseListener(func() { + app.Srv.licenseListenerId = app.AddLicenseListener(func() { app.configOrLicenseListener() message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LICENSE_CHANGED, "", "", "", nil) message.Add("license", app.GetSanitizedClientLicense()) - app.Go(func() { + app.Srv.Go(func() { app.Publish(message) }) @@ -205,8 +148,8 @@ func New(options ...Option) (outApp *App, outErr error) { app.initEnterprise() - if app.newStore == nil { - app.newStore = func() store.Store { + if app.Srv.newStore == nil { + app.Srv.newStore = func() store.Store { return store.NewLayeredStore(sqlstore.NewSqlSupplier(app.Config().SqlSettings, app.Metrics), app.Metrics, app.Cluster) } } @@ -214,10 +157,10 @@ func New(options ...Option) (outApp *App, outErr error) { if htmlTemplateWatcher, err := utils.NewHTMLTemplateWatcher("templates"); err != nil { mlog.Error(fmt.Sprintf("Failed to parse server templates %v", err)) } else { - app.htmlTemplateWatcher = htmlTemplateWatcher + app.Srv.htmlTemplateWatcher = htmlTemplateWatcher } - app.Srv.Store = app.newStore() + app.Srv.Store = app.Srv.newStore() if err := app.ensureAsymmetricSigningKey(); err != nil { return nil, errors.Wrapf(err, "unable to ensure asymmetric signing key") @@ -235,9 +178,9 @@ func New(options ...Option) (outApp *App, outErr error) { app.initJobs() }) - app.clusterLeaderListenerId = app.AddClusterLeaderChangedListener(func() { + app.Srv.clusterLeaderListenerId = app.AddClusterLeaderChangedListener(func() { mlog.Info("Cluster leader changed. Determining if job schedulers should be running:", mlog.Bool("isLeader", app.IsLeader())) - app.Jobs.Schedulers.HandleClusterLeaderChange(app.IsLeader()) + app.Srv.Jobs.Schedulers.HandleClusterLeaderChange(app.IsLeader()) }) subpath, err := utils.GetSubpathFromConfig(app.Config()) @@ -279,26 +222,26 @@ func (a *App) Shutdown() { a.StopPushNotificationsHubWorkers() a.ShutDownPlugins() - a.WaitForGoroutines() + a.Srv.WaitForGoroutines() if a.Srv.Store != nil { a.Srv.Store.Close() } - a.Srv = nil - if a.htmlTemplateWatcher != nil { - a.htmlTemplateWatcher.Close() + if a.Srv.htmlTemplateWatcher != nil { + a.Srv.htmlTemplateWatcher.Close() } - a.RemoveConfigListener(a.configListenerId) - a.RemoveLicenseListener(a.licenseListenerId) - a.RemoveConfigListener(a.logListenerId) - a.RemoveClusterLeaderChangedListener(a.clusterLeaderListenerId) + a.RemoveConfigListener(a.Srv.configListenerId) + a.RemoveLicenseListener(a.Srv.licenseListenerId) + a.RemoveConfigListener(a.Srv.logListenerId) + a.RemoveClusterLeaderChangedListener(a.Srv.clusterLeaderListenerId) mlog.Info("Server stopped") a.DisableConfigWatch() a.HTTPService.Close() + a.Srv = nil } var accountMigrationInterface func(*App) einterfaces.AccountMigrationInterface @@ -439,39 +382,39 @@ func (a *App) initEnterprise() { } func (a *App) initJobs() { - a.Jobs = jobs.NewJobServer(a, a.Srv.Store) + a.Srv.Jobs = jobs.NewJobServer(a, a.Srv.Store) if jobsDataRetentionJobInterface != nil { - a.Jobs.DataRetentionJob = jobsDataRetentionJobInterface(a) + a.Srv.Jobs.DataRetentionJob = jobsDataRetentionJobInterface(a) } if jobsMessageExportJobInterface != nil { - a.Jobs.MessageExportJob = jobsMessageExportJobInterface(a) + a.Srv.Jobs.MessageExportJob = jobsMessageExportJobInterface(a) } if jobsElasticsearchAggregatorInterface != nil { - a.Jobs.ElasticsearchAggregator = jobsElasticsearchAggregatorInterface(a) + a.Srv.Jobs.ElasticsearchAggregator = jobsElasticsearchAggregatorInterface(a) } if jobsElasticsearchIndexerInterface != nil { - a.Jobs.ElasticsearchIndexer = jobsElasticsearchIndexerInterface(a) + a.Srv.Jobs.ElasticsearchIndexer = jobsElasticsearchIndexerInterface(a) } if jobsLdapSyncInterface != nil { - a.Jobs.LdapSync = jobsLdapSyncInterface(a) + a.Srv.Jobs.LdapSync = jobsLdapSyncInterface(a) } if jobsMigrationsInterface != nil { - a.Jobs.Migrations = jobsMigrationsInterface(a) + a.Srv.Jobs.Migrations = jobsMigrationsInterface(a) } - a.Jobs.Workers = a.Jobs.InitWorkers() - a.Jobs.Schedulers = a.Jobs.InitSchedulers() + a.Srv.Jobs.Workers = a.Srv.Jobs.InitWorkers() + a.Srv.Jobs.Schedulers = a.Srv.Jobs.InitSchedulers() } func (a *App) DiagnosticId() string { - return a.diagnosticId + return a.Srv.diagnosticId } func (a *App) SetDiagnosticId(id string) { - a.diagnosticId = id + a.Srv.diagnosticId = id } func (a *App) EnsureDiagnosticId() { - if a.diagnosticId != "" { + if a.Srv.diagnosticId != "" { return } if result := <-a.Srv.Store.System().Get(); result.Err == nil { @@ -484,36 +427,13 @@ func (a *App) EnsureDiagnosticId() { <-a.Srv.Store.System().Save(systemId) } - a.diagnosticId = id - } -} - -// Go creates a goroutine, but maintains a record of it to ensure that execution completes before -// the app is destroyed. -func (a *App) Go(f func()) { - atomic.AddInt32(&a.goroutineCount, 1) - - go func() { - f() - - atomic.AddInt32(&a.goroutineCount, -1) - select { - case a.goroutineExitSignal <- struct{}{}: - default: - } - }() -} - -// WaitForGoroutines blocks until all goroutines created by App.Go exit. -func (a *App) WaitForGoroutines() { - for atomic.LoadInt32(&a.goroutineCount) != 0 { - <-a.goroutineExitSignal + a.Srv.diagnosticId = id } } func (a *App) HTMLTemplates() *template.Template { - if a.htmlTemplateWatcher != nil { - return a.htmlTemplateWatcher.Templates() + if a.Srv.htmlTemplateWatcher != nil { + return a.Srv.htmlTemplateWatcher.Templates() } return nil @@ -596,7 +516,7 @@ func (a *App) SetPhase2PermissionsMigrationStatus(isComplete bool) error { return res.Err } } - a.phase2PermissionsMigrationComplete = isComplete + a.Srv.phase2PermissionsMigrationComplete = isComplete return nil } @@ -670,7 +590,7 @@ func (a *App) DoEmojisPermissionsMigration() { } func (a *App) StartElasticsearch() { - a.Go(func() { + a.Srv.Go(func() { if err := a.Elasticsearch.Start(); err != nil { mlog.Error(err.Error()) } @@ -678,19 +598,19 @@ func (a *App) StartElasticsearch() { a.AddConfigListener(func(oldConfig *model.Config, newConfig *model.Config) { if !*oldConfig.ElasticsearchSettings.EnableIndexing && *newConfig.ElasticsearchSettings.EnableIndexing { - a.Go(func() { + a.Srv.Go(func() { if err := a.Elasticsearch.Start(); err != nil { mlog.Error(err.Error()) } }) } else if *oldConfig.ElasticsearchSettings.EnableIndexing && !*newConfig.ElasticsearchSettings.EnableIndexing { - a.Go(func() { + a.Srv.Go(func() { if err := a.Elasticsearch.Stop(); err != nil { mlog.Error(err.Error()) } }) } else if *oldConfig.ElasticsearchSettings.Password != *newConfig.ElasticsearchSettings.Password || *oldConfig.ElasticsearchSettings.Username != *newConfig.ElasticsearchSettings.Username || *oldConfig.ElasticsearchSettings.ConnectionUrl != *newConfig.ElasticsearchSettings.ConnectionUrl || *oldConfig.ElasticsearchSettings.Sniff != *newConfig.ElasticsearchSettings.Sniff { - a.Go(func() { + a.Srv.Go(func() { if *oldConfig.ElasticsearchSettings.EnableIndexing { if err := a.Elasticsearch.Stop(); err != nil { mlog.Error(err.Error()) @@ -705,13 +625,13 @@ func (a *App) StartElasticsearch() { a.AddLicenseListener(func() { if a.License() != nil { - a.Go(func() { + a.Srv.Go(func() { if err := a.Elasticsearch.Start(); err != nil { mlog.Error(err.Error()) } }) } else { - a.Go(func() { + a.Srv.Go(func() { if err := a.Elasticsearch.Stop(); err != nil { mlog.Error(err.Error()) } diff --git a/app/app_test.go b/app/app_test.go index de9378d1a4..0092940198 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -246,8 +246,12 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { restrictPrivateChannel := *th.App.Config().TeamSettings.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.DEPRECATED_DO_NOT_USE_RestrictPublicChannelManagement = restrictPublicChannel }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement = restrictPrivateChannel }) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.DEPRECATED_DO_NOT_USE_RestrictPublicChannelManagement = restrictPublicChannel + }) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement = restrictPrivateChannel + }) }() th.App.UpdateConfig(func(cfg *model.Config) { @@ -433,8 +437,8 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { postEditTimeLimit := *th.App.Config().ServiceSettings.PostEditTimeLimit defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.DEPRECATED_DO_NOT_USE_AllowEditPost = allowEditPost}) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostEditTimeLimit = postEditTimeLimit}) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.DEPRECATED_DO_NOT_USE_AllowEditPost = allowEditPost }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostEditTimeLimit = postEditTimeLimit }) }() th.App.UpdateConfig(func(cfg *model.Config) { diff --git a/app/channel.go b/app/channel.go index dee856b94d..14cd74d87b 100644 --- a/app/channel.go +++ b/app/channel.go @@ -212,9 +212,9 @@ func (a *App) CreateChannel(channel *model.Channel, addMember bool) (*model.Chan } if a.PluginsReady() { - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.ChannelHasBeenCreated(pluginContext, sc) return true }, plugin.ChannelHasBeenCreatedId) @@ -239,9 +239,9 @@ func (a *App) CreateDirectChannel(userId string, otherUserId string) (*model.Cha a.InvalidateCacheForUser(otherUserId) if a.PluginsReady() { - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.ChannelHasBeenCreated(pluginContext, channel) return true }, plugin.ChannelHasBeenCreatedId) @@ -854,9 +854,9 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques } if a.PluginsReady() { - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor) return true }, plugin.UserHasJoinedChannelId) @@ -866,7 +866,7 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques if userRequestorId == "" || userId == userRequestorId { a.postJoinChannelMessage(user, channel) } else { - a.Go(func() { + a.Srv.Go(func() { a.PostAddToChannelMessage(userRequestor, user, channel, postRootId) }) } @@ -1244,9 +1244,9 @@ func (a *App) JoinChannel(channel *model.Channel, userId string) *model.AppError } if a.PluginsReady() { - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedChannel(pluginContext, cm, nil) return true }, plugin.UserHasJoinedChannelId) @@ -1336,7 +1336,7 @@ func (a *App) LeaveChannel(channelId string, userId string) *model.AppError { return nil } - a.Go(func() { + a.Srv.Go(func() { a.postLeaveChannelMessage(user, channel) }) @@ -1451,9 +1451,9 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string, actorUser, _ = a.GetUser(removerUserId) } - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.UserHasLeftChannel(pluginContext, cm, actorUser) return true }, plugin.UserHasLeftChannelId) @@ -1489,7 +1489,7 @@ func (a *App) RemoveUserFromChannel(userIdToRemove string, removerUserId string, if userIdToRemove == removerUserId { a.postLeaveChannelMessage(user, channel) } else { - a.Go(func() { + a.Srv.Go(func() { a.postRemoveFromChannelMessage(removerUserId, user, channel) }) } diff --git a/app/cluster.go b/app/cluster.go index 22b9843b8c..e166521f16 100644 --- a/app/cluster.go +++ b/app/cluster.go @@ -13,19 +13,19 @@ import ( // be called. func (a *App) AddClusterLeaderChangedListener(listener func()) string { id := model.NewId() - a.clusterLeaderListeners.Store(id, listener) + a.Srv.clusterLeaderListeners.Store(id, listener) return id } // Removes a listener function by the unique ID returned when AddConfigListener was called func (a *App) RemoveClusterLeaderChangedListener(id string) { - a.clusterLeaderListeners.Delete(id) + a.Srv.clusterLeaderListeners.Delete(id) } func (a *App) InvokeClusterLeaderChangedListeners() { mlog.Info("Cluster leader changed. Invoking ClusterLeaderChanged listeners.") - a.Go(func() { - a.clusterLeaderListeners.Range(func(_, listener interface{}) bool { + a.Srv.Go(func() { + a.Srv.clusterLeaderListeners.Range(func(_, listener interface{}) bool { listener.(func())() return true }) diff --git a/app/command_echo.go b/app/command_echo.go index f0851964bc..14b6e11b9f 100644 --- a/app/command_echo.go +++ b/app/command_echo.go @@ -78,7 +78,7 @@ func (me *EchoProvider) DoCommand(a *App, args *model.CommandArgs, message strin } echoSem <- true - a.Go(func() { + a.Srv.Go(func() { defer func() { <-echoSem }() post := &model.Post{} post.ChannelId = args.ChannelId diff --git a/app/compliance.go b/app/compliance.go index d46e75b245..2d8dd2b517 100644 --- a/app/compliance.go +++ b/app/compliance.go @@ -36,7 +36,7 @@ func (a *App) SaveComplianceReport(job *model.Compliance) (*model.Compliance, *m } job = result.Data.(*model.Compliance) - a.Go(func() { + a.Srv.Go(func() { a.Compliance.RunComplianceJob(job) }) diff --git a/app/config.go b/app/config.go index 34f4dc7519..450e205268 100644 --- a/app/config.go +++ b/app/config.go @@ -28,15 +28,15 @@ const ( ) func (a *App) Config() *model.Config { - if cfg := a.config.Load(); cfg != nil { + if cfg := a.Srv.config.Load(); cfg != nil { return cfg.(*model.Config) } return &model.Config{} } func (a *App) EnvironmentConfig() map[string]interface{} { - if a.envConfig != nil { - return a.envConfig + if a.Srv.envConfig != nil { + return a.Srv.envConfig } return map[string]interface{}{} } @@ -45,7 +45,7 @@ func (a *App) UpdateConfig(f func(*model.Config)) { old := a.Config() updated := old.Clone() f(updated) - a.config.Store(updated) + a.Srv.config.Store(updated) a.InvokeConfigListeners(old, updated) } @@ -62,11 +62,10 @@ func (a *App) LoadConfig(configFile string) *model.AppError { return err } *cfg.ServiceSettings.SiteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/") - a.config.Store(cfg) + a.Srv.config.Store(cfg) - a.configFile = configPath - a.envConfig = envConfig - a.siteURL = *cfg.ServiceSettings.SiteURL + a.Srv.configFile = configPath + a.Srv.envConfig = envConfig a.InvokeConfigListeners(old, cfg) return nil @@ -74,7 +73,7 @@ func (a *App) LoadConfig(configFile string) *model.AppError { func (a *App) ReloadConfig() *model.AppError { debug.FreeOSMemory() - if err := a.LoadConfig(a.configFile); err != nil { + if err := a.LoadConfig(a.Srv.configFile); err != nil { return err } @@ -84,37 +83,37 @@ func (a *App) ReloadConfig() *model.AppError { } func (a *App) ConfigFileName() string { - return a.configFile + return a.Srv.configFile } func (a *App) ClientConfig() map[string]string { - return a.clientConfig + return a.Srv.clientConfig } func (a *App) ClientConfigHash() string { - return a.clientConfigHash + return a.Srv.clientConfigHash } func (a *App) LimitedClientConfig() map[string]string { - return a.limitedClientConfig + return a.Srv.limitedClientConfig } func (a *App) EnableConfigWatch() { - if a.configWatcher == nil && !a.disableConfigWatch { + if a.Srv.configWatcher == nil && !a.Srv.disableConfigWatch { configWatcher, err := utils.NewConfigWatcher(a.ConfigFileName(), func() { a.ReloadConfig() }) if err != nil { mlog.Error(fmt.Sprint(err)) } - a.configWatcher = configWatcher + a.Srv.configWatcher = configWatcher } } func (a *App) DisableConfigWatch() { - if a.configWatcher != nil { - a.configWatcher.Close() - a.configWatcher = nil + if a.Srv.configWatcher != nil { + a.Srv.configWatcher.Close() + a.Srv.configWatcher = nil } } @@ -123,17 +122,17 @@ func (a *App) DisableConfigWatch() { // 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 + a.Srv.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) + delete(a.Srv.configListeners, id) } func (a *App) InvokeConfigListeners(old, current *model.Config) { - for _, listener := range a.configListeners { + for _, listener := range a.Srv.configListeners { listener(old, current) } } @@ -141,7 +140,7 @@ func (a *App) InvokeConfigListeners(old, current *model.Config) { // EnsureAsymmetricSigningKey ensures that an asymmetric signing key exists and future calls to // AsymmetricSigningKey will always return a valid signing key. func (a *App) ensureAsymmetricSigningKey() error { - if a.asymmetricSigningKey != nil { + if a.Srv.asymmetricSigningKey != nil { return nil } @@ -202,7 +201,7 @@ func (a *App) ensureAsymmetricSigningKey() error { default: return fmt.Errorf("unknown curve: " + key.ECDSAKey.Curve) } - a.asymmetricSigningKey = &ecdsa.PrivateKey{ + a.Srv.asymmetricSigningKey = &ecdsa.PrivateKey{ PublicKey: ecdsa.PublicKey{ Curve: curve, X: key.ECDSAKey.X, @@ -240,31 +239,31 @@ func (a *App) ensureInstallationDate() error { // AsymmetricSigningKey will return a private key that can be used for asymmetric signing. func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey { - return a.asymmetricSigningKey + return a.Srv.asymmetricSigningKey } func (a *App) regenerateClientConfig() { - a.clientConfig = utils.GenerateClientConfig(a.Config(), a.DiagnosticId(), a.License()) + a.Srv.clientConfig = utils.GenerateClientConfig(a.Config(), a.DiagnosticId(), a.License()) - if a.clientConfig["EnableCustomTermsOfService"] == "true" { + if a.Srv.clientConfig["EnableCustomTermsOfService"] == "true" { termsOfService, err := a.GetLatestTermsOfService() if err != nil { mlog.Err(err) } else { - a.clientConfig["CustomTermsOfServiceId"] = termsOfService.Id + a.Srv.clientConfig["CustomTermsOfServiceId"] = termsOfService.Id } } - a.limitedClientConfig = utils.GenerateLimitedClientConfig(a.Config(), a.DiagnosticId(), a.License()) + a.Srv.limitedClientConfig = utils.GenerateLimitedClientConfig(a.Config(), a.DiagnosticId(), a.License()) if key := a.AsymmetricSigningKey(); key != nil { der, _ := x509.MarshalPKIXPublicKey(&key.PublicKey) - a.clientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der) - a.limitedClientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der) + a.Srv.clientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der) + a.Srv.limitedClientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der) } - clientConfigJSON, _ := json.Marshal(a.clientConfig) - a.clientConfigHash = fmt.Sprintf("%x", md5.Sum(clientConfigJSON)) + clientConfigJSON, _ := json.Marshal(a.Srv.clientConfig) + a.Srv.clientConfigHash = fmt.Sprintf("%x", md5.Sum(clientConfigJSON)) } func (a *App) Desanitize(cfg *model.Config) { @@ -322,7 +321,7 @@ func (a *App) GetCookieDomain() string { } func (a *App) GetSiteURL() string { - return a.siteURL + return *a.Config().ServiceSettings.SiteURL } // ClientConfigWithComputed gets the configuration in a format suitable for sending to the client. diff --git a/app/config_test.go b/app/config_test.go index 1c1811b94b..a885eb62f3 100644 --- a/app/config_test.go +++ b/app/config_test.go @@ -35,11 +35,12 @@ func TestLoadConfig(t *testing.T) { require.Nil(t, err) tempConfig.Close() - a := App{} + a := App{ + Srv: &Server{}, + } appErr := a.LoadConfig(tempConfig.Name()) require.Nil(t, appErr) - assert.Equal(t, "http://localhost:8065", a.siteURL) assert.Equal(t, "http://localhost:8065", *a.GetConfig().ServiceSettings.SiteURL) } diff --git a/app/diagnostics.go b/app/diagnostics.go index bc2684f9a4..fdbf9cab7c 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -590,7 +590,7 @@ func (a *App) trackPlugins() { settingsCount := 0 pluginStates := a.Config().PluginSettings.PluginStates - plugins, _ := a.Plugins.Available() + plugins, _ := a.Srv.Plugins.Available() if pluginStates != nil && plugins != nil { for _, plugin := range plugins { diff --git a/app/email.go b/app/email.go index 143d4a052f..ed9a71a1d5 100644 --- a/app/email.go +++ b/app/email.go @@ -42,7 +42,7 @@ func (a *App) SetupInviteEmailRateLimiting() error { return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.") } - a.EmailRateLimiter = rateLimiter + a.Srv.EmailRateLimiter = rateLimiter return nil } @@ -286,11 +286,11 @@ func (a *App) SendMfaChangeEmail(email string, activated bool, locale, siteURL s } func (a *App) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string) { - if a.EmailRateLimiter == nil { + if a.Srv.EmailRateLimiter == nil { a.Log.Error("Email invite not sent, rate limiting could not be setup.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id)) return } - rateLimited, result, err := a.EmailRateLimiter.RateLimit(senderUserId, len(invites)) + rateLimited, result, err := a.Srv.EmailRateLimiter.RateLimit(senderUserId, len(invites)) if err != nil { a.Log.Error("Error rate limiting invite email.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id), mlog.Err(err)) return diff --git a/app/email_batching.go b/app/email_batching.go index ceee47f324..6adb2b33b2 100644 --- a/app/email_batching.go +++ b/app/email_batching.go @@ -25,13 +25,13 @@ const ( func (a *App) InitEmailBatching() { if *a.Config().EmailSettings.EnableEmailBatching { - if a.EmailBatching == nil { - a.EmailBatching = NewEmailBatchingJob(a, *a.Config().EmailSettings.EmailBatchingBufferSize) + if a.Srv.EmailBatching == nil { + a.Srv.EmailBatching = NewEmailBatchingJob(a, *a.Config().EmailSettings.EmailBatchingBufferSize) } // note that we don't support changing EmailBatchingBufferSize without restarting the server - a.EmailBatching.Start() + a.Srv.EmailBatching.Start() } } @@ -40,7 +40,7 @@ func (a *App) AddNotificationEmailToBatch(user *model.User, post *model.Post, te return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "", http.StatusNotImplemented) } - if !a.EmailBatching.Add(user, post, team) { + if !a.Srv.EmailBatching.Add(user, post, team) { mlog.Error("Email batching job's receiving channel was full. Please increase the EmailBatchingBufferSize.") return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.channel_full.app_error", nil, "", http.StatusInternalServerError) } @@ -188,7 +188,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu // send the email notification if it's been long enough if now.Sub(time.Unix(batchStartTime/1000, 0)) > time.Duration(interval)*time.Second { - job.app.Go(func(userId string, notifications []*batchedNotification) func() { + job.app.Srv.Go(func(userId string, notifications []*batchedNotification) func() { return func() { handler(userId, notifications) } diff --git a/app/file.go b/app/file.go index 8d2a7fc15b..1901a8beeb 100644 --- a/app/file.go +++ b/app/file.go @@ -444,7 +444,7 @@ func (a *App) DoUploadFileExpectModification(now time.Time, rawTeamId string, ra if a.PluginsReady() { var rejectionError *model.AppError pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { var newBytes bytes.Buffer replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes) if rejectionReason != "" { diff --git a/app/job.go b/app/job.go index 782301de45..50ce346f09 100644 --- a/app/job.go +++ b/app/job.go @@ -40,9 +40,9 @@ func (a *App) GetJobsByType(jobType string, offset int, limit int) ([]*model.Job } func (a *App) CreateJob(job *model.Job) (*model.Job, *model.AppError) { - return a.Jobs.CreateJob(job.Type, job.Data) + return a.Srv.Jobs.CreateJob(job.Type, job.Data) } func (a *App) CancelJob(jobId string) *model.AppError { - return a.Jobs.RequestCancellation(jobId) + return a.Srv.Jobs.RequestCancellation(jobId) } diff --git a/app/ldap.go b/app/ldap.go index 544905b702..d254c656cd 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -13,7 +13,7 @@ import ( ) func (a *App) SyncLdap() { - a.Go(func() { + a.Srv.Go(func() { if license := a.License(); license != nil && *license.Features.LDAP && *a.Config().LdapSettings.EnableSync { if ldapI := a.Ldap; ldapI != nil { @@ -67,7 +67,7 @@ func (a *App) SwitchEmailToLdap(email, password, code, ldapLoginId, ldapPassword return "", err } - a.Go(func() { + a.Srv.Go(func() { if err := a.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, a.GetSiteURL()); err != nil { mlog.Error(err.Error()) } @@ -113,7 +113,7 @@ func (a *App) SwitchLdapToEmail(ldapPassword, code, email, newPassword string) ( T := utils.GetUserTranslations(user.Locale) - a.Go(func() { + a.Srv.Go(func() { if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil { mlog.Error(err.Error()) } diff --git a/app/license.go b/app/license.go index ec18ec3186..ea7f7d4b0c 100644 --- a/app/license.go +++ b/app/license.go @@ -93,10 +93,10 @@ func (a *App) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) // doesn't start until the server is restarted, which prevents the 'run job now' buttons in system console from // functioning as expected if *a.Config().JobSettings.RunJobs { - a.Jobs.StartWorkers() + a.Srv.Jobs.StartWorkers() } if *a.Config().JobSettings.RunScheduler { - a.Jobs.StartSchedulers() + a.Srv.Jobs.StartSchedulers() } return license, nil @@ -104,13 +104,13 @@ func (a *App) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) // License returns the currently active license or nil if the application is unlicensed. func (a *App) License() *model.License { - license, _ := a.licenseValue.Load().(*model.License) + license, _ := a.Srv.licenseValue.Load().(*model.License) return license } func (a *App) SetLicense(license *model.License) bool { defer func() { - for _, listener := range a.licenseListeners { + for _, listener := range a.Srv.licenseListeners { listener() } }() @@ -119,14 +119,14 @@ func (a *App) SetLicense(license *model.License) bool { license.Features.SetDefaults() if !license.IsExpired() { - a.licenseValue.Store(license) - a.clientLicenseValue.Store(utils.GetClientLicense(license)) + a.Srv.licenseValue.Store(license) + a.Srv.clientLicenseValue.Store(utils.GetClientLicense(license)) return true } } - a.licenseValue.Store((*model.License)(nil)) - a.clientLicenseValue.Store(map[string]string(nil)) + a.Srv.licenseValue.Store((*model.License)(nil)) + a.Srv.clientLicenseValue.Store(map[string]string(nil)) return false } @@ -141,18 +141,18 @@ func (a *App) ValidateAndSetLicenseBytes(b []byte) { } func (a *App) SetClientLicense(m map[string]string) { - a.clientLicenseValue.Store(m) + a.Srv.clientLicenseValue.Store(m) } func (a *App) ClientLicense() map[string]string { - if clientLicense, _ := a.clientLicenseValue.Load().(map[string]string); clientLicense != nil { + if clientLicense, _ := a.Srv.clientLicenseValue.Load().(map[string]string); clientLicense != nil { return clientLicense } return map[string]string{"IsLicensed": "false"} } func (a *App) RemoveLicense() *model.AppError { - if license, _ := a.licenseValue.Load().(*model.License); license == nil { + if license, _ := a.Srv.licenseValue.Load().(*model.License); license == nil { return nil } @@ -174,12 +174,12 @@ func (a *App) RemoveLicense() *model.AppError { func (a *App) AddLicenseListener(listener func()) string { id := model.NewId() - a.licenseListeners[id] = listener + a.Srv.licenseListeners[id] = listener return id } func (a *App) RemoveLicenseListener(id string) { - delete(a.licenseListeners, id) + delete(a.Srv.licenseListeners, id) } func (a *App) GetClientLicenseEtag(useSanitized bool) string { diff --git a/app/login.go b/app/login.go index 01cdde3863..28f207f75f 100644 --- a/app/login.go +++ b/app/login.go @@ -69,7 +69,7 @@ func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken string, l if a.PluginsReady() { var rejectionReason string pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { rejectionReason = hooks.UserWillLogIn(pluginContext, user) return rejectionReason == "" }, plugin.UserWillLogInId) @@ -78,9 +78,9 @@ func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken string, l return nil, model.NewAppError("AuthenticateUserForLogin", "Login rejected by plugin: "+rejectionReason, nil, "", http.StatusBadRequest) } - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.UserHasLoggedIn(pluginContext, user) return true }, plugin.UserHasLoggedInId) diff --git a/app/notification.go b/app/notification.go index 54f1f470da..4501b311d1 100644 --- a/app/notification.go +++ b/app/notification.go @@ -78,7 +78,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod } if post.Type != model.POST_AUTO_RESPONDER { - a.Go(func() { + a.Srv.Go(func() { a.SendAutoResponse(channel, otherUser) }) } @@ -127,7 +127,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod 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() { + a.Srv.Go(func() { a.sendOutOfChannelMentions(sender, post, outOfChannelMentions) }) } diff --git a/app/notification_email.go b/app/notification_email.go index 9eb842084d..8333ee8098 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -103,7 +103,7 @@ func (a *App) sendNotificationEmail(notification *postNotification, user *model. teamURL := a.GetSiteURL() + "/" + team.Name var bodyText = a.getNotificationEmailBody(user, post, channel, channelName, senderName, team.Name, teamURL, emailNotificationContentsType, useMilitaryTime, translateFunc) - a.Go(func() { + a.Srv.Go(func() { if err := a.SendMail(user.Email, html.UnescapeString(subjectText), bodyText); err != nil { mlog.Error(fmt.Sprint("Error to send the email", user.Email, err)) } diff --git a/app/notification_push.go b/app/notification_push.go index a17ccb3755..eb392bc220 100644 --- a/app/notification_push.go +++ b/app/notification_push.go @@ -132,7 +132,7 @@ func (a *App) sendPushNotification(notification *postNotification, user *model.U channelName := notification.GetChannelName(nameFormat, user.Id) senderName := notification.GetSenderName(nameFormat, cfg.ServiceSettings.EnablePostUsernameOverride) - c := a.PushNotificationsHub.GetGoChannelFromUserId(user.Id) + c := a.Srv.PushNotificationsHub.GetGoChannelFromUserId(user.Id) c <- PushNotification{ notificationType: NOTIFICATION_TYPE_MESSAGE, post: post, @@ -217,7 +217,7 @@ func (a *App) ClearPushNotificationSync(userId string, channelId string) { } func (a *App) ClearPushNotification(userId string, channelId string) { - channel := a.PushNotificationsHub.GetGoChannelFromUserId(userId) + channel := a.Srv.PushNotificationsHub.GetGoChannelFromUserId(userId) channel <- PushNotification{ notificationType: NOTIFICATION_TYPE_CLEAR, userId: userId, @@ -232,7 +232,7 @@ func (a *App) CreatePushNotificationsHub() { for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ { hub.Channels = append(hub.Channels, make(chan PushNotification, PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER)) } - a.PushNotificationsHub = hub + a.Srv.PushNotificationsHub = hub } func (a *App) pushNotificationWorker(notifications chan PushNotification) { @@ -259,13 +259,13 @@ func (a *App) pushNotificationWorker(notifications chan PushNotification) { func (a *App) StartPushNotificationsHubWorkers() { for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ { - channel := a.PushNotificationsHub.Channels[x] - a.Go(func() { a.pushNotificationWorker(channel) }) + channel := a.Srv.PushNotificationsHub.Channels[x] + a.Srv.Go(func() { a.pushNotificationWorker(channel) }) } } func (a *App) StopPushNotificationsHubWorkers() { - for _, channel := range a.PushNotificationsHub.Channels { + for _, channel := range a.Srv.PushNotificationsHub.Channels { close(channel) } } diff --git a/app/oauth.go b/app/oauth.go index 645d502f5e..e1e02f929b 100644 --- a/app/oauth.go +++ b/app/oauth.go @@ -601,7 +601,7 @@ func (a *App) CompleteSwitchWithOAuth(service string, userData io.ReadCloser, em return nil, result.Err } - a.Go(func() { + a.Srv.Go(func() { if err := a.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, a.GetSiteURL()); err != nil { mlog.Error(err.Error()) } @@ -859,7 +859,7 @@ func (a *App) SwitchOAuthToEmail(email, password, requesterId string) (string, * T := utils.GetUserTranslations(user.Locale) - a.Go(func() { + a.Srv.Go(func() { if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil { mlog.Error(err.Error()) } diff --git a/app/options.go b/app/options.go index 4645660242..d490ecb06c 100644 --- a/app/options.go +++ b/app/options.go @@ -17,11 +17,11 @@ func StoreOverride(override interface{}) Option { return func(a *App) { switch o := override.(type) { case store.Store: - a.newStore = func() store.Store { + a.Srv.newStore = func() store.Store { return o } case func(*App) store.Store: - a.newStore = func() store.Store { + a.Srv.newStore = func() store.Store { return o(a) } default: @@ -32,10 +32,10 @@ func StoreOverride(override interface{}) Option { func ConfigFile(file string) Option { return func(a *App) { - a.configFile = file + a.Srv.configFile = file } } func DisableConfigWatch(a *App) { - a.disableConfigWatch = true + a.Srv.disableConfigWatch = true } diff --git a/app/plugin.go b/app/plugin.go index 2c18c6cecf..2c1eb3862a 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -16,21 +16,21 @@ import ( ) func (a *App) SyncPluginsActiveState() { - if a.Plugins == nil { + if a.Srv.Plugins == nil { return } config := a.Config().PluginSettings if *config.Enable { - availablePlugins, err := a.Plugins.Available() + availablePlugins, err := a.Srv.Plugins.Available() if err != nil { a.Log.Error("Unable to get available plugins", mlog.Err(err)) return } // Deactivate any plugins that have been disabled. - for _, plugin := range a.Plugins.Active() { + for _, plugin := range a.Srv.Plugins.Active() { // Determine if plugin is enabled pluginId := plugin.Manifest.Id pluginEnabled := false @@ -40,7 +40,7 @@ func (a *App) SyncPluginsActiveState() { // If it's not enabled we need to deactivate it if !pluginEnabled { - deactivated := a.Plugins.Deactivate(pluginId) + deactivated := a.Srv.Plugins.Deactivate(pluginId) if deactivated && plugin.Manifest.HasClient() { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DISABLED, "", "", "", nil) message.Add("manifest", plugin.Manifest.ClientManifest()) @@ -65,7 +65,7 @@ func (a *App) SyncPluginsActiveState() { // Activate plugin if enabled if pluginEnabled { - updatedManifest, activated, err := a.Plugins.Activate(pluginId) + updatedManifest, activated, err := a.Srv.Plugins.Activate(pluginId) if err != nil { plugin.WrapLogger(a.Log).Error("Unable to activate plugin", mlog.Err(err)) continue @@ -79,7 +79,7 @@ func (a *App) SyncPluginsActiveState() { } } } else { // If plugins are disabled, shutdown plugins. - a.Plugins.Shutdown() + a.Srv.Plugins.Shutdown() } if err := a.notifyPluginStatusesChanged(); err != nil { @@ -92,7 +92,7 @@ func (a *App) NewPluginAPI(manifest *model.Manifest) plugin.API { } func (a *App) InitPlugins(pluginDir, webappPluginDir string) { - if a.Plugins != nil || !*a.Config().PluginSettings.Enable { + if a.Srv.Plugins != nil || !*a.Config().PluginSettings.Enable { a.SyncPluginsActiveState() return } @@ -113,7 +113,7 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) { mlog.Error("Failed to start up plugins", mlog.Err(err)) return } else { - a.Plugins = env + a.Srv.Plugins = env } prepackagedPluginsDir, found := utils.FindDir("prepackaged_plugins") @@ -136,10 +136,10 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) { } // Sync plugin active state when config changes. Also notify plugins. - a.RemoveConfigListener(a.PluginConfigListenerId) - a.PluginConfigListenerId = a.AddConfigListener(func(*model.Config, *model.Config) { + a.RemoveConfigListener(a.Srv.PluginConfigListenerId) + a.Srv.PluginConfigListenerId = a.AddConfigListener(func(*model.Config, *model.Config) { a.SyncPluginsActiveState() - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.OnConfigurationChange() return true }, plugin.OnConfigurationChangeId) @@ -150,25 +150,25 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) { } func (a *App) ShutDownPlugins() { - if a.Plugins == nil { + if a.Srv.Plugins == nil { return } mlog.Info("Shutting down plugins") - a.Plugins.Shutdown() + a.Srv.Plugins.Shutdown() - a.RemoveConfigListener(a.PluginConfigListenerId) - a.PluginConfigListenerId = "" - a.Plugins = nil + a.RemoveConfigListener(a.Srv.PluginConfigListenerId) + a.Srv.PluginConfigListenerId = "" + a.Srv.Plugins = nil } func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) { - if a.Plugins == nil || !*a.Config().PluginSettings.Enable { + if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - plugins := a.Plugins.Active() + plugins := a.Srv.Plugins.Active() manifests := make([]*model.Manifest, len(plugins)) for i, plugin := range plugins { @@ -181,11 +181,11 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) { // EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous // activation if inactive anywhere in the cluster. func (a *App) EnablePlugin(id string) *model.AppError { - if a.Plugins == nil || !*a.Config().PluginSettings.Enable { + if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - plugins, err := a.Plugins.Available() + plugins, err := a.Srv.Plugins.Available() if err != nil { return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -221,11 +221,11 @@ func (a *App) EnablePlugin(id string) *model.AppError { // DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active. func (a *App) DisablePlugin(id string) *model.AppError { - if a.Plugins == nil || !*a.Config().PluginSettings.Enable { + if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - plugins, err := a.Plugins.Available() + plugins, err := a.Srv.Plugins.Available() if err != nil { return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -256,7 +256,7 @@ func (a *App) DisablePlugin(id string) *model.AppError { } func (a *App) PluginsReady() bool { - return a.Plugins != nil && *a.Config().PluginSettings.Enable + return a.Srv.Plugins != nil && *a.Config().PluginSettings.Enable } func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) { @@ -264,7 +264,7 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) { return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - availablePlugins, err := a.Plugins.Available() + availablePlugins, err := a.Srv.Plugins.Available() if err != nil { return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -278,7 +278,7 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) { Manifest: *plugin.Manifest, } - if a.Plugins.IsActive(plugin.Manifest.Id) { + if a.Srv.Plugins.IsActive(plugin.Manifest.Id) { resp.Active = append(resp.Active, info) } else { resp.Inactive = append(resp.Inactive, info) diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index d95ae38fb9..6c51189dee 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -33,7 +33,7 @@ func setupPluginApiTest(t *testing.T, pluginCode string, pluginManifest string, ioutil.WriteFile(filepath.Join(pluginDir, pluginId, "plugin.json"), []byte(pluginManifest), 0600) env.Activate(pluginId) - app.Plugins = env + app.Srv.Plugins = env } func TestPluginAPIUpdateUserStatus(t *testing.T) { @@ -120,7 +120,7 @@ func TestPluginAPILoadPluginConfiguration(t *testing.T) { } ] }}`, "testloadpluginconfig", th.App) - hooks, err := th.App.Plugins.HooksForPlugin("testloadpluginconfig") + hooks, err := th.App.Srv.Plugins.HooksForPlugin("testloadpluginconfig") assert.NoError(t, err) _, ret := hooks.MessageWillBePosted(nil, nil) assert.Equal(t, "str32true", ret) @@ -194,7 +194,7 @@ func TestPluginAPILoadPluginConfigurationDefaults(t *testing.T) { } ] }}`, "testloadpluginconfig", th.App) - hooks, err := th.App.Plugins.HooksForPlugin("testloadpluginconfig") + hooks, err := th.App.Srv.Plugins.HooksForPlugin("testloadpluginconfig") assert.NoError(t, err) _, ret := hooks.MessageWillBePosted(nil, nil) assert.Equal(t, "override35true", ret) diff --git a/app/plugin_commands.go b/app/plugin_commands.go index daa92ce5ac..cbaeb507c9 100644 --- a/app/plugin_commands.go +++ b/app/plugin_commands.go @@ -31,10 +31,10 @@ func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) err DisplayName: command.DisplayName, } - a.pluginCommandsLock.Lock() - defer a.pluginCommandsLock.Unlock() + a.Srv.pluginCommandsLock.Lock() + defer a.Srv.pluginCommandsLock.Unlock() - for _, pc := range a.pluginCommands { + for _, pc := range a.Srv.pluginCommands { if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId { if pc.PluginId == pluginId { pc.Command = command @@ -43,7 +43,7 @@ func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) err } } - a.pluginCommands = append(a.pluginCommands, &PluginCommand{ + a.Srv.pluginCommands = append(a.Srv.pluginCommands, &PluginCommand{ Command: command, PluginId: pluginId, }) @@ -53,37 +53,37 @@ func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) err func (a *App) UnregisterPluginCommand(pluginId, teamId, trigger string) { trigger = strings.ToLower(trigger) - a.pluginCommandsLock.Lock() - defer a.pluginCommandsLock.Unlock() + a.Srv.pluginCommandsLock.Lock() + defer a.Srv.pluginCommandsLock.Unlock() var remaining []*PluginCommand - for _, pc := range a.pluginCommands { + for _, pc := range a.Srv.pluginCommands { if pc.Command.TeamId != teamId || pc.Command.Trigger != trigger { remaining = append(remaining, pc) } } - a.pluginCommands = remaining + a.Srv.pluginCommands = remaining } func (a *App) UnregisterPluginCommands(pluginId string) { - a.pluginCommandsLock.Lock() - defer a.pluginCommandsLock.Unlock() + a.Srv.pluginCommandsLock.Lock() + defer a.Srv.pluginCommandsLock.Unlock() var remaining []*PluginCommand - for _, pc := range a.pluginCommands { + for _, pc := range a.Srv.pluginCommands { if pc.PluginId != pluginId { remaining = append(remaining, pc) } } - a.pluginCommands = remaining + a.Srv.pluginCommands = remaining } func (a *App) PluginCommandsForTeam(teamId string) []*model.Command { - a.pluginCommandsLock.RLock() - defer a.pluginCommandsLock.RUnlock() + a.Srv.pluginCommandsLock.RLock() + defer a.Srv.pluginCommandsLock.RUnlock() var commands []*model.Command - for _, pc := range a.pluginCommands { + for _, pc := range a.Srv.pluginCommands { if pc.Command.TeamId == "" || pc.Command.TeamId == teamId { commands = append(commands, pc.Command) } @@ -96,12 +96,12 @@ func (a *App) ExecutePluginCommand(args *model.CommandArgs) (*model.Command, *mo trigger := parts[0][1:] trigger = strings.ToLower(trigger) - a.pluginCommandsLock.RLock() - defer a.pluginCommandsLock.RUnlock() + a.Srv.pluginCommandsLock.RLock() + defer a.Srv.pluginCommandsLock.RUnlock() - for _, pc := range a.pluginCommands { + for _, pc := range a.Srv.pluginCommands { if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger { - pluginHooks, err := a.Plugins.HooksForPlugin(pc.PluginId) + pluginHooks, err := a.Srv.Plugins.HooksForPlugin(pc.PluginId) if err != nil { return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } diff --git a/app/plugin_hooks_test.go b/app/plugin_hooks_test.go index 766385af41..9016b84e46 100644 --- a/app/plugin_hooks_test.go +++ b/app/plugin_hooks_test.go @@ -44,7 +44,7 @@ func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, a env, err := plugin.NewEnvironment(apiFunc, pluginDir, webappPluginDir, app.Log) require.NoError(t, err) - app.Plugins = env + app.Srv.Plugins = env pluginIds := []string{} activationErrors := []error{} for _, code := range pluginCode { diff --git a/app/plugin_install.go b/app/plugin_install.go index 65e6973562..aaa6e46a53 100644 --- a/app/plugin_install.go +++ b/app/plugin_install.go @@ -22,7 +22,7 @@ func (a *App) InstallPlugin(pluginFile io.Reader, replace bool) (*model.Manifest } func (a *App) installPlugin(pluginFile io.Reader, replace bool) (*model.Manifest, *model.AppError) { - if a.Plugins == nil || !*a.Config().PluginSettings.Enable { + if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -55,7 +55,7 @@ func (a *App) installPlugin(pluginFile io.Reader, replace bool) (*model.Manifest return nil, model.NewAppError("installPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidIdRegex}, "", http.StatusBadRequest) } - bundles, err := a.Plugins.Available() + bundles, err := a.Srv.Plugins.Available() if err != nil { return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -91,11 +91,11 @@ func (a *App) RemovePlugin(id string) *model.AppError { } func (a *App) removePlugin(id string) *model.AppError { - if a.Plugins == nil || !*a.Config().PluginSettings.Enable { + if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - plugins, err := a.Plugins.Available() + plugins, err := a.Srv.Plugins.Available() if err != nil { return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) } @@ -114,13 +114,13 @@ func (a *App) removePlugin(id string) *model.AppError { return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) } - if a.Plugins.IsActive(id) && manifest.HasClient() { + if a.Srv.Plugins.IsActive(id) && manifest.HasClient() { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DISABLED, "", "", "", nil) message.Add("manifest", manifest.ClientManifest()) a.Publish(message) } - a.Plugins.Deactivate(id) + a.Srv.Plugins.Deactivate(id) a.UnregisterPluginCommands(id) err = os.RemoveAll(pluginPath) diff --git a/app/plugin_requests.go b/app/plugin_requests.go index cf1033ce4a..4070a060f4 100644 --- a/app/plugin_requests.go +++ b/app/plugin_requests.go @@ -19,7 +19,7 @@ import ( ) func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) { - if a.Plugins == nil || !*a.Config().PluginSettings.Enable { + if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented) a.Log.Error(err.Error()) w.WriteHeader(err.StatusCode) @@ -29,7 +29,7 @@ func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) { } params := mux.Vars(r) - hooks, err := a.Plugins.HooksForPlugin(params["plugin_id"]) + hooks, err := a.Srv.Plugins.HooksForPlugin(params["plugin_id"]) if err != nil { a.Log.Error("Access to route for non-existent plugin", mlog.String("missing_plugin_id", params["plugin_id"]), mlog.Err(err)) http.NotFound(w, r) diff --git a/app/plugin_statuses.go b/app/plugin_statuses.go index 4922f0fc1c..df6477f89b 100644 --- a/app/plugin_statuses.go +++ b/app/plugin_statuses.go @@ -11,11 +11,11 @@ import ( // GetPluginStatuses returns the status for plugins installed on this server. func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { - if a.Plugins == nil || !*a.Config().PluginSettings.Enable { + if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - pluginStatuses, err := a.Plugins.Statuses() + pluginStatuses, err := a.Srv.Plugins.Statuses() if err != nil { return nil, model.NewAppError("GetPluginStatuses", "app.plugin.get_statuses.app_error", nil, err.Error(), http.StatusInternalServerError) } diff --git a/app/post.go b/app/post.go index c882fc0562..019cf207f5 100644 --- a/app/post.go +++ b/app/post.go @@ -150,7 +150,7 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo if a.PluginsReady() { var rejectionError *model.AppError pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { replacementPost, rejectionReason := hooks.MessageWillBePosted(pluginContext, post) if rejectionReason != "" { rejectionError = model.NewAppError("createPost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest) @@ -174,9 +174,9 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo rpost := result.Data.(*model.Post) if a.PluginsReady() { - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.MessageHasBeenPosted(pluginContext, rpost) return true }, plugin.MessageHasBeenPostedId) @@ -185,7 +185,7 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo esInterface := a.Elasticsearch if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { - a.Go(func() { + a.Srv.Go(func() { esInterface.IndexPost(rpost, channel.TeamId) }) } @@ -277,7 +277,7 @@ func (a *App) handlePostEvents(post *model.Post, user *model.User, channel *mode } if triggerWebhooks { - a.Go(func() { + a.Srv.Go(func() { if err := a.handleWebhookEvents(post, team, channel, user); err != nil { mlog.Error(err.Error()) } @@ -362,7 +362,7 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model if a.PluginsReady() { var rejectionReason string pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost, oldPost) return post != nil }, plugin.MessageWillBeUpdatedId) @@ -378,9 +378,9 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model rpost := result.Data.(*model.Post) if a.PluginsReady() { - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.MessageHasBeenUpdated(pluginContext, newPost, oldPost) return true }, plugin.MessageHasBeenUpdatedId) @@ -389,7 +389,7 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model esInterface := a.Elasticsearch if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { - a.Go(func() { + a.Srv.Go(func() { rchannel := <-a.Srv.Store.Channel().GetForPost(rpost.Id) if rchannel.Err != nil { mlog.Error(fmt.Sprintf("Couldn't get channel %v for post %v for Elasticsearch indexing.", rpost.ChannelId, rpost.Id)) @@ -567,16 +567,16 @@ func (a *App) DeletePost(postId, deleteByID string) (*model.Post, *model.AppErro message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Publish(message) - a.Go(func() { + a.Srv.Go(func() { a.DeletePostFiles(post) }) - a.Go(func() { + a.Srv.Go(func() { a.DeleteFlaggedPosts(post.Id) }) esInterface := a.Elasticsearch if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing { - a.Go(func() { + a.Srv.Go(func() { esInterface.DeletePost(post) }) } diff --git a/app/post_test.go b/app/post_test.go index 5d93d3f0f5..3b5bc910cd 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -648,9 +648,9 @@ func TestMaxPostSize(t *testing.T) { app := App{ Srv: &Server{ - Store: mockStore, + Store: mockStore, + config: atomic.Value{}, }, - config: atomic.Value{}, } assert.Equal(t, testCase.ExpectedMaxPostSize, app.MaxPostSize()) diff --git a/app/reaction.go b/app/reaction.go index 41fc7fca41..9b30e610fc 100644 --- a/app/reaction.go +++ b/app/reaction.go @@ -42,7 +42,7 @@ func (a *App) SaveReactionForPost(reaction *model.Reaction) (*model.Reaction, *m reaction = result.Data.(*model.Reaction) - a.Go(func() { + a.Srv.Go(func() { a.sendReactionEvent(model.WEBSOCKET_EVENT_REACTION_ADDED, reaction, post, true) }) @@ -92,7 +92,7 @@ func (a *App) DeleteReactionForPost(reaction *model.Reaction) *model.AppError { return result.Err } - a.Go(func() { + a.Srv.Go(func() { a.sendReactionEvent(model.WEBSOCKET_EVENT_REACTION_REMOVED, reaction, post, hasReactions) }) diff --git a/app/role.go b/app/role.go index 8a0e9decea..8141575076 100644 --- a/app/role.go +++ b/app/role.go @@ -104,7 +104,7 @@ func (a *App) sendUpdatedRoleEvent(role *model.Role) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_ROLE_UPDATED, "", "", "", nil) message.Add("role", role.ToJson()) - a.Go(func() { + a.Srv.Go(func() { a.Publish(message) }) } diff --git a/app/scheme.go b/app/scheme.go index 0fa2647b2e..b3bbb2614c 100644 --- a/app/scheme.go +++ b/app/scheme.go @@ -152,7 +152,7 @@ func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) } func (a *App) IsPhase2MigrationCompleted() *model.AppError { - if a.phase2PermissionsMigrationComplete { + if a.Srv.phase2PermissionsMigrationComplete { return nil } @@ -160,7 +160,7 @@ func (a *App) IsPhase2MigrationCompleted() *model.AppError { return model.NewAppError("App.IsPhase2MigrationCompleted", "app.schemes.is_phase_2_migration_completed.not_completed.app_error", nil, result.Err.Error(), http.StatusNotImplemented) } - a.phase2PermissionsMigrationComplete = true + a.Srv.phase2PermissionsMigrationComplete = true return nil } diff --git a/app/server.go b/app/server.go index b95059c840..53e5e039da 100644 --- a/app/server.go +++ b/app/server.go @@ -5,6 +5,7 @@ package app import ( "context" + "crypto/ecdsa" "crypto/tls" "fmt" "io" @@ -14,16 +15,21 @@ import ( "net/url" "os" "strings" + "sync" + "sync/atomic" "time" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/rs/cors" + "github.com/throttled/throttled" "golang.org/x/crypto/acme/autocert" + "github.com/mattermost/mattermost-server/jobs" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/utils" ) @@ -44,6 +50,79 @@ type Server struct { RateLimiter *RateLimiter didFinishListen chan struct{} + + goroutineCount int32 + goroutineExitSignal chan struct{} + + Plugins *plugin.Environment + PluginConfigListenerId string + + EmailBatching *EmailBatchingJob + EmailRateLimiter *throttled.GCRARateLimiter + + Hubs []*Hub + HubsStopCheckingForDeadlock chan bool + + PushNotificationsHub PushNotificationsHub + + Jobs *jobs.JobServer + + config atomic.Value + envConfig map[string]interface{} + configFile string + configListeners map[string]func(*model.Config, *model.Config) + clusterLeaderListeners sync.Map + + licenseValue atomic.Value + clientLicenseValue atomic.Value + licenseListeners map[string]func() + + timezones atomic.Value + + newStore func() store.Store + + htmlTemplateWatcher *utils.HTMLTemplateWatcher + sessionCache *utils.Cache + configListenerId string + licenseListenerId string + logListenerId string + clusterLeaderListenerId string + disableConfigWatch bool + configWatcher *utils.ConfigWatcher + asymmetricSigningKey *ecdsa.PrivateKey + + pluginCommands []*PluginCommand + pluginCommandsLock sync.RWMutex + + clientConfig map[string]string + clientConfigHash string + limitedClientConfig map[string]string + diagnosticId string + + phase2PermissionsMigrationComplete bool +} + +// Go creates a goroutine, but maintains a record of it to ensure that execution completes before +// the app is destroyed. +func (s *Server) Go(f func()) { + atomic.AddInt32(&s.goroutineCount, 1) + + go func() { + f() + + atomic.AddInt32(&s.goroutineCount, -1) + select { + case s.goroutineExitSignal <- struct{}{}: + default: + } + }() +} + +// WaitForGoroutines blocks until all goroutines created by App.Go exit. +func (s *Server) WaitForGoroutines() { + for atomic.LoadInt32(&s.goroutineCount) != 0 { + <-s.goroutineExitSignal + } } var corsAllowedMethods = []string{ diff --git a/app/session.go b/app/session.go index 6d46f3d3a9..e30e03bc1e 100644 --- a/app/session.go +++ b/app/session.go @@ -29,7 +29,7 @@ func (a *App) GetSession(token string) (*model.Session, *model.AppError) { metrics := a.Metrics var session *model.Session - if ts, ok := a.sessionCache.Get(token); ok { + if ts, ok := a.Srv.sessionCache.Get(token); ok { session = ts.(*model.Session) if metrics != nil { metrics.IncrementMemCacheHitCounterSession() @@ -137,13 +137,13 @@ func (a *App) ClearSessionCacheForUser(userId string) { } func (a *App) ClearSessionCacheForUserSkipClusterSend(userId string) { - keys := a.sessionCache.Keys() + keys := a.Srv.sessionCache.Keys() for _, key := range keys { - if ts, ok := a.sessionCache.Get(key); ok { + if ts, ok := a.Srv.sessionCache.Get(key); ok { session := ts.(*model.Session) if session.UserId == userId { - a.sessionCache.Remove(key) + a.Srv.sessionCache.Remove(key) if a.Metrics != nil { a.Metrics.IncrementMemCacheInvalidationCounterSession() } @@ -155,11 +155,11 @@ func (a *App) ClearSessionCacheForUserSkipClusterSend(userId string) { } func (a *App) AddSessionToCache(session *model.Session) { - a.sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*a.Config().ServiceSettings.SessionCacheInMinutes*60)) + a.Srv.sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*a.Config().ServiceSettings.SessionCacheInMinutes*60)) } func (a *App) SessionCacheLength() int { - return a.sessionCache.Len() + return a.Srv.sessionCache.Len() } func (a *App) RevokeSessionsForDeviceId(userId string, deviceId string, currentSessionId string) *model.AppError { diff --git a/app/session_test.go b/app/session_test.go index 8349a4cecf..4eb6999151 100644 --- a/app/session_test.go +++ b/app/session_test.go @@ -22,16 +22,16 @@ func TestCache(t *testing.T) { UserId: model.NewId(), } - th.App.sessionCache.AddWithExpiresInSecs(session.Token, session, 5*60) + th.App.Srv.sessionCache.AddWithExpiresInSecs(session.Token, session, 5*60) - keys := th.App.sessionCache.Keys() + keys := th.App.Srv.sessionCache.Keys() if len(keys) <= 0 { t.Fatal("should have items") } th.App.ClearSessionCacheForUser(session.UserId) - rkeys := th.App.sessionCache.Keys() + rkeys := th.App.Srv.sessionCache.Keys() if len(rkeys) != len(keys)-1 { t.Fatal("should have one less") } diff --git a/app/team.go b/app/team.go index 34216b7fc1..6180611048 100644 --- a/app/team.go +++ b/app/team.go @@ -466,9 +466,9 @@ func (a *App) JoinUserToTeam(team *model.Team, user *model.User, userRequestorId actor, _ = a.GetUser(userRequestorId) } - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedTeam(pluginContext, tm, actor) return true }, plugin.UserHasJoinedTeamId) @@ -789,9 +789,9 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) actor, _ = a.GetUser(requestorId) } - a.Go(func() { + a.Srv.Go(func() { pluginContext := &plugin.Context{} - a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.Srv.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool { hooks.UserHasLeftTeam(pluginContext, teamMember, actor) return true }, plugin.UserHasLeftTeamId) diff --git a/app/timezone.go b/app/timezone.go index 84d912da68..e014eb025c 100644 --- a/app/timezone.go +++ b/app/timezone.go @@ -9,7 +9,7 @@ import ( ) func (a *App) Timezones() model.SupportedTimezones { - if cfg := a.timezones.Load(); cfg != nil { + if cfg := a.Srv.timezones.Load(); cfg != nil { return cfg.(model.SupportedTimezones) } return model.SupportedTimezones{} @@ -24,5 +24,5 @@ func (a *App) LoadTimezones() { timezoneCfg := utils.LoadTimezones(timezonePath) - a.timezones.Store(timezoneCfg) + a.Srv.timezones.Store(timezoneCfg) } diff --git a/app/user.go b/app/user.go index e86cbbf3ee..92269e618f 100644 --- a/app/user.go +++ b/app/user.go @@ -1034,14 +1034,14 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, if sendNotifications { if rusers[0].Email != rusers[1].Email { - a.Go(func() { + a.Srv.Go(func() { if err := a.SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, a.GetSiteURL()); err != nil { mlog.Error(err.Error()) } }) if a.Config().EmailSettings.RequireEmailVerification { - a.Go(func() { + a.Srv.Go(func() { if err := a.SendEmailVerification(rusers[0]); err != nil { mlog.Error(err.Error()) } @@ -1050,7 +1050,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, } if rusers[0].Username != rusers[1].Username { - a.Go(func() { + a.Srv.Go(func() { if err := a.SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, a.GetSiteURL()); err != nil { mlog.Error(err.Error()) } @@ -1090,7 +1090,7 @@ func (a *App) UpdateMfa(activate bool, userId, token string) *model.AppError { } } - a.Go(func() { + a.Srv.Go(func() { user, err := a.GetUser(userId) if err != nil { mlog.Error(err.Error()) @@ -1133,7 +1133,7 @@ func (a *App) UpdatePasswordSendEmail(user *model.User, newPassword, method stri return err } - a.Go(func() { + a.Srv.Go(func() { if err := a.SendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil { mlog.Error(err.Error()) } diff --git a/app/web_conn.go b/app/web_conn.go index 084480bbd6..7eea5d3bb3 100644 --- a/app/web_conn.go +++ b/app/web_conn.go @@ -45,7 +45,7 @@ type WebConn struct { func (a *App) NewWebConn(ws *websocket.Conn, session model.Session, t goi18n.TranslateFunc, locale string) *WebConn { if len(session.UserId) > 0 { - a.Go(func() { + a.Srv.Go(func() { a.SetStatusOnline(session.UserId, false) a.UpdateLastActivityAtIfNeeded(session) }) @@ -126,7 +126,7 @@ func (c *WebConn) readPump() { c.WebSocket.SetPongHandler(func(string) error { c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) if c.IsAuthenticated() { - c.App.Go(func() { + c.App.Srv.Go(func() { c.App.SetStatusAwayIfNeeded(c.UserId, false) }) } @@ -212,7 +212,7 @@ func (c *WebConn) writePump() { } if c.App.Metrics != nil { - c.App.Go(func() { + c.App.Srv.Go(func() { c.App.Metrics.IncrementWebSocketBroadcast(msg.EventType()) }) } diff --git a/app/web_hub.go b/app/web_hub.go index 5bb86ee382..f65bec931d 100644 --- a/app/web_hub.go +++ b/app/web_hub.go @@ -62,7 +62,7 @@ func (a *App) NewWebHub() *Hub { func (a *App) TotalWebsocketConnections() int { count := int64(0) - for _, hub := range a.Hubs { + for _, hub := range a.Srv.Hubs { count = count + atomic.LoadInt64(&hub.connectionCount) } @@ -74,13 +74,13 @@ func (a *App) HubStart() { numberOfHubs := runtime.NumCPU() * 2 mlog.Info(fmt.Sprintf("Starting %v websocket hubs", numberOfHubs)) - a.Hubs = make([]*Hub, numberOfHubs) - a.HubsStopCheckingForDeadlock = make(chan bool, 1) + a.Srv.Hubs = make([]*Hub, numberOfHubs) + a.Srv.HubsStopCheckingForDeadlock = make(chan bool, 1) - for i := 0; i < len(a.Hubs); i++ { - a.Hubs[i] = a.NewWebHub() - a.Hubs[i].connectionIndex = i - a.Hubs[i].Start() + for i := 0; i < len(a.Srv.Hubs); i++ { + a.Srv.Hubs[i] = a.NewWebHub() + a.Srv.Hubs[i].connectionIndex = i + a.Srv.Hubs[i].Start() } go func() { @@ -93,7 +93,7 @@ func (a *App) HubStart() { for { select { case <-ticker.C: - for _, hub := range a.Hubs { + for _, hub := range a.Srv.Hubs { if len(hub.broadcast) >= DEADLOCK_WARN { mlog.Error(fmt.Sprintf("Hub processing might be deadlock on hub %v goroutine %v with %v events in the buffer", hub.connectionIndex, hub.goroutineId, len(hub.broadcast))) buf := make([]byte, 1<<16) @@ -109,7 +109,7 @@ func (a *App) HubStart() { } } - case <-a.HubsStopCheckingForDeadlock: + case <-a.Srv.HubsStopCheckingForDeadlock: return } } @@ -120,27 +120,27 @@ func (a *App) HubStop() { mlog.Info("stopping websocket hub connections") select { - case a.HubsStopCheckingForDeadlock <- true: + case a.Srv.HubsStopCheckingForDeadlock <- true: default: mlog.Warn("We appear to have already sent the stop checking for deadlocks command") } - for _, hub := range a.Hubs { + for _, hub := range a.Srv.Hubs { hub.Stop() } - a.Hubs = []*Hub{} + a.Srv.Hubs = []*Hub{} } func (a *App) GetHubForUserId(userId string) *Hub { - if len(a.Hubs) == 0 { + if len(a.Srv.Hubs) == 0 { return nil } hash := fnv.New32a() hash.Write([]byte(userId)) - index := hash.Sum32() % uint32(len(a.Hubs)) - return a.Hubs[index] + index := hash.Sum32() % uint32(len(a.Srv.Hubs)) + return a.Srv.Hubs[index] } func (a *App) HubRegister(webConn *WebConn) { @@ -190,7 +190,7 @@ func (a *App) PublishSkipClusterSend(message *model.WebSocketEvent) { hub.Broadcast(message) } } else { - for _, hub := range a.Hubs { + for _, hub := range a.Srv.Hubs { hub.Broadcast(message) } } @@ -416,7 +416,7 @@ func (h *Hub) Start() { conns := connections.ForUser(webCon.UserId) if len(conns) == 0 { - h.app.Go(func() { + h.app.Srv.Go(func() { h.app.SetStatusOffline(webCon.UserId, false) }) } else { @@ -427,7 +427,7 @@ func (h *Hub) Start() { } } if h.app.IsUserAway(latestActivity) { - h.app.Go(func() { + h.app.Srv.Go(func() { h.app.SetStatusLastActivityAt(webCon.UserId, latestActivity) }) } diff --git a/app/webhook.go b/app/webhook.go index 2557a99e6f..0d62947dfd 100644 --- a/app/webhook.go +++ b/app/webhook.go @@ -80,7 +80,7 @@ func (a *App) handleWebhookEvents(post *model.Post, team *model.Team, channel *m TriggerWord: triggerWord, FileIds: strings.Join(post.FileIds, ","), } - a.Go(func(hook *model.OutgoingWebhook) func() { + a.Srv.Go(func(hook *model.OutgoingWebhook) func() { return func() { a.TriggerWebhook(payload, hook, post, channel) } @@ -102,7 +102,7 @@ func (a *App) TriggerWebhook(payload *model.OutgoingWebhookPayload, hook *model. } for _, url := range hook.CallbackURLs { - a.Go(func(url string) func() { + a.Srv.Go(func(url string) func() { return func() { req, _ := http.NewRequest("POST", url, body) req.Header.Set("Content-Type", contentType) diff --git a/app/websocket_router.go b/app/websocket_router.go index c7c9943089..38937d0fa4 100644 --- a/app/websocket_router.go +++ b/app/websocket_router.go @@ -55,7 +55,7 @@ func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketReque return } - wr.app.Go(func() { + wr.app.Srv.Go(func() { wr.app.SetStatusOnline(session.UserId, false) wr.app.UpdateLastActivityAtIfNeeded(*session) }) diff --git a/cmd/mattermost/commands/jobserver.go b/cmd/mattermost/commands/jobserver.go index 253ada9323..238fcaca35 100644 --- a/cmd/mattermost/commands/jobserver.go +++ b/cmd/mattermost/commands/jobserver.go @@ -44,12 +44,12 @@ func jobserverCmdF(command *cobra.Command, args []string) { defer mlog.Info("Stopped Mattermost job server") if !noJobs { - a.Jobs.StartWorkers() - defer a.Jobs.StopWorkers() + a.Srv.Jobs.StartWorkers() + defer a.Srv.Jobs.StopWorkers() } if !noSchedule { - a.Jobs.StartSchedulers() - defer a.Jobs.StopSchedulers() + a.Srv.Jobs.StartSchedulers() + defer a.Srv.Jobs.StopSchedulers() } signalChan := make(chan os.Signal, 1) diff --git a/cmd/mattermost/commands/server.go b/cmd/mattermost/commands/server.go index e996e6a2e0..dbced49b7b 100644 --- a/cmd/mattermost/commands/server.go +++ b/cmd/mattermost/commands/server.go @@ -147,19 +147,19 @@ func runServer(configFileLocation string, disableConfigWatch bool, usedPlatform manualtesting.Init(api) } - a.Go(func() { + a.Srv.Go(func() { runSecurityJob(a) }) - a.Go(func() { + a.Srv.Go(func() { runDiagnosticsJob(a) }) - a.Go(func() { + a.Srv.Go(func() { runSessionCleanupJob(a) }) - a.Go(func() { + a.Srv.Go(func() { runTokenCleanupJob(a) }) - a.Go(func() { + a.Srv.Go(func() { runCommandWebhookCleanupJob(a) }) @@ -181,12 +181,12 @@ func runServer(configFileLocation string, disableConfigWatch bool, usedPlatform } if *a.Config().JobSettings.RunJobs { - a.Jobs.StartWorkers() - defer a.Jobs.StopWorkers() + a.Srv.Jobs.StartWorkers() + defer a.Srv.Jobs.StopWorkers() } if *a.Config().JobSettings.RunScheduler { - a.Jobs.StartSchedulers() - defer a.Jobs.StopSchedulers() + a.Srv.Jobs.StartSchedulers() + defer a.Srv.Jobs.StopSchedulers() } notifyReady() diff --git a/migrations/scheduler.go b/migrations/scheduler.go index 5778c5cb3b..9baa56e30a 100644 --- a/migrations/scheduler.go +++ b/migrations/scheduler.go @@ -61,7 +61,7 @@ func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, las // Check the migration job isn't wedged. if job != nil && job.LastActivityAt < model.GetMillis()-MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS && job.CreateAt < model.GetMillis()-MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS { mlog.Warn("Job appears to be wedged. Rescheduling another instance.", mlog.String("scheduler", scheduler.Name()), mlog.String("wedged_job_id", job.Id), mlog.String("migration_key", key)) - if err := scheduler.App.Jobs.SetJobError(job, nil); err != nil { + if err := scheduler.App.Srv.Jobs.SetJobError(job, nil); err != nil { mlog.Error("Worker: Failed to set job error", mlog.String("scheduler", scheduler.Name()), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) } return scheduler.createJob(key, job, scheduler.App.Srv.Store) @@ -102,7 +102,7 @@ func (scheduler *Scheduler) createJob(migrationKey string, lastJob *model.Job, s JOB_DATA_KEY_MIGRATION_LAST_DONE: lastDone, } - if job, err := scheduler.App.Jobs.CreateJob(model.JOB_TYPE_MIGRATIONS, data); err != nil { + if job, err := scheduler.App.Srv.Jobs.CreateJob(model.JOB_TYPE_MIGRATIONS, data); err != nil { return nil, err } else { return job, nil diff --git a/migrations/worker.go b/migrations/worker.go index 7a64dd6093..e50b515782 100644 --- a/migrations/worker.go +++ b/migrations/worker.go @@ -33,7 +33,7 @@ func (m *MigrationsJobInterfaceImpl) MakeWorker() model.Worker { stop: make(chan bool, 1), stopped: make(chan bool, 1), jobs: make(chan model.Job), - jobServer: m.App.Jobs, + jobServer: m.App.Srv.Jobs, app: m.App, } @@ -83,7 +83,7 @@ func (worker *Worker) DoJob(job *model.Job) { cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background()) cancelWatcherChan := make(chan interface{}, 1) - go worker.app.Jobs.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan) + go worker.app.Srv.Jobs.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan) defer cancelCancelWatcher() @@ -111,7 +111,7 @@ func (worker *Worker) DoJob(job *model.Job) { return } else { job.Data[JOB_DATA_KEY_MIGRATION_LAST_DONE] = progress - if err := worker.app.Jobs.UpdateInProgressJobData(job); err != nil { + if err := worker.app.Srv.Jobs.UpdateInProgressJobData(job); err != nil { mlog.Error("Worker: Failed to update migration status data for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) worker.setJobError(job, err) return @@ -122,20 +122,20 @@ func (worker *Worker) DoJob(job *model.Job) { } func (worker *Worker) setJobSuccess(job *model.Job) { - if err := worker.app.Jobs.SetJobSuccess(job); err != nil { + if err := worker.app.Srv.Jobs.SetJobSuccess(job); err != nil { mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) worker.setJobError(job, err) } } func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) { - if err := worker.app.Jobs.SetJobError(job, appError); err != nil { + if err := worker.app.Srv.Jobs.SetJobError(job, appError); err != nil { mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) } } func (worker *Worker) setJobCanceled(job *model.Job) { - if err := worker.app.Jobs.SetJobCanceled(job); err != nil { + if err := worker.app.Srv.Jobs.SetJobCanceled(job); err != nil { mlog.Error("Worker: Failed to mark job as canceled", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) } } diff --git a/plugin/scheduler/scheduler.go b/plugin/scheduler/scheduler.go index 7214d6cfdf..fade4fdc17 100644 --- a/plugin/scheduler/scheduler.go +++ b/plugin/scheduler/scheduler.go @@ -39,7 +39,7 @@ func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, p func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) { mlog.Debug("Scheduling Job", mlog.String("scheduler", scheduler.Name())) - if job, err := scheduler.App.Jobs.CreateJob(model.JOB_TYPE_PLUGINS, nil); err != nil { + if job, err := scheduler.App.Srv.Jobs.CreateJob(model.JOB_TYPE_PLUGINS, nil); err != nil { return nil, err } else { return job, nil diff --git a/plugin/scheduler/worker.go b/plugin/scheduler/worker.go index 252e100fab..ec293defea 100644 --- a/plugin/scheduler/worker.go +++ b/plugin/scheduler/worker.go @@ -25,7 +25,7 @@ func (m *PluginsJobInterfaceImpl) MakeWorker() model.Worker { stop: make(chan bool, 1), stopped: make(chan bool, 1), jobs: make(chan model.Job), - jobServer: m.App.Jobs, + jobServer: m.App.Srv.Jobs, app: m.App, } @@ -86,14 +86,14 @@ func (worker *Worker) DoJob(job *model.Job) { } func (worker *Worker) setJobSuccess(job *model.Job) { - if err := worker.app.Jobs.SetJobSuccess(job); err != nil { + if err := worker.app.Srv.Jobs.SetJobSuccess(job); err != nil { mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) worker.setJobError(job, err) } } func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) { - if err := worker.app.Jobs.SetJobError(job, appError); err != nil { + if err := worker.app.Srv.Jobs.SetJobError(job, appError); err != nil { mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) } } diff --git a/web/saml.go b/web/saml.go index 773b349808..a1dc9569c7 100644 --- a/web/saml.go +++ b/web/saml.go @@ -115,7 +115,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { case model.OAUTH_ACTION_SIGNUP: teamId := relayProps["team_id"] if len(teamId) > 0 { - c.App.Go(func() { + c.App.Srv.Go(func() { if err := c.App.AddUserToTeamByTeamId(teamId, user); err != nil { mlog.Error(err.Error()) } else { @@ -129,7 +129,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { return } c.LogAuditWithUserId(user.Id, "Revoked all sessions for user") - c.App.Go(func() { + c.App.Srv.Go(func() { if err := c.App.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil { mlog.Error(err.Error()) } From 6d4421d18a21318e0cc357b11d7a5acff8d9f770 Mon Sep 17 00:00:00 2001 From: Christian Claus Date: Wed, 7 Nov 2018 20:23:02 +0100 Subject: [PATCH 13/23] Add GetTeamsUnreadForUser to plugin api (#9659) * Add GetTeamsUnreadForUser to plugin api * Remove teamIdToExclude from plugin method GetTeamsUnreadForUser * Add minimum server version to plugin API doc of GetTeamsUnreadForUser --- app/plugin_api.go | 4 ++++ plugin/api.go | 5 +++++ plugin/client_rpc_generated.go | 29 +++++++++++++++++++++++++++++ plugin/plugintest/api.go | 25 +++++++++++++++++++++++++ 4 files changed, 63 insertions(+) diff --git a/app/plugin_api.go b/app/plugin_api.go index b9b06a3458..ef297694f3 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -107,6 +107,10 @@ func (api *PluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) return api.app.GetTeamByName(name) } +func (api *PluginAPI) GetTeamsUnreadForUser(userId string) ([]*model.TeamUnread, *model.AppError) { + return api.app.GetTeamsUnreadForUser("", userId) +} + func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) { return api.app.UpdateTeam(team) } diff --git a/plugin/api.go b/plugin/api.go index 8f4c5dadeb..7d95006efa 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -106,6 +106,11 @@ type API interface { // GetTeamByName gets a team by its name. GetTeamByName(name string) (*model.Team, *model.AppError) + // GetTeamsUnreadForUser gets the unread message and mention counts for each team to which the given user belongs. + // + // Minimum server version: 5.6 + GetTeamsUnreadForUser(userId string) ([]*model.TeamUnread, *model.AppError) + // UpdateTeam updates a team. UpdateTeam(team *model.Team) (*model.Team, *model.AppError) diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 5171f3ec66..864d60a89c 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -1209,6 +1209,35 @@ func (s *apiRPCServer) GetTeamByName(args *Z_GetTeamByNameArgs, returns *Z_GetTe return nil } +type Z_GetTeamsUnreadForUserArgs struct { + A string +} + +type Z_GetTeamsUnreadForUserReturns struct { + A []*model.TeamUnread + B *model.AppError +} + +func (g *apiRPCClient) GetTeamsUnreadForUser(userId string) ([]*model.TeamUnread, *model.AppError) { + _args := &Z_GetTeamsUnreadForUserArgs{userId} + _returns := &Z_GetTeamsUnreadForUserReturns{} + if err := g.client.Call("Plugin.GetTeamsUnreadForUser", _args, _returns); err != nil { + log.Printf("RPC call to GetTeamsUnreadForUser API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetTeamsUnreadForUser(args *Z_GetTeamsUnreadForUserArgs, returns *Z_GetTeamsUnreadForUserReturns) error { + if hook, ok := s.impl.(interface { + GetTeamsUnreadForUser(userId string) ([]*model.TeamUnread, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetTeamsUnreadForUser(args.A) + } else { + return encodableError(fmt.Errorf("API GetTeamsUnreadForUser called but not implemented.")) + } + return nil +} + type Z_UpdateTeamArgs struct { A *model.Team } diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index 6b9184156e..9b03ed0b7b 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -1193,6 +1193,31 @@ func (_m *API) GetTeamsForUser(userId string) ([]*model.Team, *model.AppError) { return r0, r1 } +// GetTeamsUnreadForUser provides a mock function with given fields: userId +func (_m *API) GetTeamsUnreadForUser(userId string) ([]*model.TeamUnread, *model.AppError) { + ret := _m.Called(userId) + + var r0 []*model.TeamUnread + if rf, ok := ret.Get(0).(func(string) []*model.TeamUnread); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.TeamUnread) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(userId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetUser provides a mock function with given fields: userId func (_m *API) GetUser(userId string) (*model.User, *model.AppError) { ret := _m.Called(userId) From 93e581d642def809949000804be1e2e213a816b2 Mon Sep 17 00:00:00 2001 From: Christian Claus Date: Wed, 7 Nov 2018 21:24:54 +0100 Subject: [PATCH 14/23] Add plugin API for UploadFile method (#9684) * Add plugin API for UploadFile method * Add minimum server version documentation to plugin API UploadFile function * Reorganize some imports --- app/file.go | 19 +++++++++++++++++++ app/file_test.go | 24 ++++++++++++++++++++++++ app/plugin_api.go | 4 ++++ model/file_info.go | 2 +- plugin/api.go | 5 +++++ plugin/client_rpc_generated.go | 31 +++++++++++++++++++++++++++++++ plugin/plugintest/api.go | 25 +++++++++++++++++++++++++ 7 files changed, 109 insertions(+), 1 deletion(-) diff --git a/app/file.go b/app/file.go index 1901a8beeb..410e3ae9b3 100644 --- a/app/file.go +++ b/app/file.go @@ -397,6 +397,25 @@ func (a *App) UploadFiles(teamId string, channelId string, userId string, files return resStruct, nil } +// UploadFile uploads a single file in form of a completely constructed byte array for a channel. +func (a *App) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { + info, _, appError := a.DoUploadFileExpectModification(time.Now(), "noteam", channelId, "nouser", filename, data) + + if appError != nil { + return nil, appError + } + + if info.PreviewPath != "" || info.ThumbnailPath != "" { + previewPathList := []string{info.PreviewPath} + thumbnailPathList := []string{info.ThumbnailPath} + imageDataList := [][]byte{data} + + a.HandleImages(previewPathList, thumbnailPathList, imageDataList) + } + + return info, nil +} + func (a *App) DoUploadFile(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) { info, _, err := a.DoUploadFileExpectModification(now, rawTeamId, rawChannelId, rawUserId, rawFilename, data) return info, err diff --git a/app/file_test.go b/app/file_test.go index c736328cfa..ee1319dca2 100644 --- a/app/file_test.go +++ b/app/file_test.go @@ -107,6 +107,30 @@ func TestDoUploadFile(t *testing.T) { } } +func TestUploadFile(t *testing.T) { + th := Setup() + defer th.TearDown() + + channelId := model.NewId() + filename := "test" + data := []byte("abcd") + + info1, err := th.App.UploadFile(data, channelId, filename) + if err != nil { + t.Fatal(err) + } else { + defer func() { + <-th.App.Srv.Store.FileInfo().PermanentDelete(info1.Id) + th.App.RemoveFile(info1.Path) + }() + } + + if info1.Path != fmt.Sprintf("%v/teams/noteam/channels/%v/users/nouser/%v/%v", + time.Now().Format("20060102"), channelId, info1.Id, filename) { + t.Fatal("stored file at incorrect path", info1.Path) + } +} + func TestGetInfoForFilename(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/plugin_api.go b/app/plugin_api.go index ef297694f3..a0c449b874 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -430,6 +430,10 @@ func (api *PluginAPI) ReadFile(path string) ([]byte, *model.AppError) { return api.app.ReadFile(path) } +func (api *PluginAPI) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { + return api.app.UploadFile(data, channelId, filename) +} + func (api *PluginAPI) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) { return api.app.GetEmojiImage(emojiId) } diff --git a/model/file_info.go b/model/file_info.go index e0bbfcfc48..7a3e54a187 100644 --- a/model/file_info.go +++ b/model/file_info.go @@ -85,7 +85,7 @@ func (o *FileInfo) IsValid() *AppError { return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "", http.StatusBadRequest) } - if len(o.CreatorId) != 26 { + if len(o.CreatorId) != 26 && o.CreatorId != "nouser" { return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+o.Id, http.StatusBadRequest) } diff --git a/plugin/api.go b/plugin/api.go index 7d95006efa..2589fa9e01 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -309,6 +309,11 @@ type API interface { // Minimum server version: 5.6 GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) + // UploadFile will upload a file to a channel using a multipart request, to be later attached to a post. + // + // Minimum server version: 5.6 + UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) + // KVSet will store a key-value pair, unique per plugin. KVSet(key string, value []byte) *model.AppError diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 864d60a89c..6b20dd8679 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -2697,6 +2697,37 @@ func (s *apiRPCServer) GetEmojiImage(args *Z_GetEmojiImageArgs, returns *Z_GetEm return nil } +type Z_UploadFileArgs struct { + A []byte + B string + C string +} + +type Z_UploadFileReturns struct { + A *model.FileInfo + B *model.AppError +} + +func (g *apiRPCClient) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { + _args := &Z_UploadFileArgs{data, channelId, filename} + _returns := &Z_UploadFileReturns{} + if err := g.client.Call("Plugin.UploadFile", _args, _returns); err != nil { + log.Printf("RPC call to UploadFile API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) UploadFile(args *Z_UploadFileArgs, returns *Z_UploadFileReturns) error { + if hook, ok := s.impl.(interface { + UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) + }); ok { + returns.A, returns.B = hook.UploadFile(args.A, args.B, args.C) + } else { + return encodableError(fmt.Errorf("API UploadFile called but not implemented.")) + } + return nil +} + type Z_KVSetArgs struct { A string B []byte diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index 9b03ed0b7b..14628095d2 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -1950,3 +1950,28 @@ func (_m *API) UpdateUserStatus(userId string, status string) (*model.Status, *m return r0, r1 } + +// UploadFile provides a mock function with given fields: data, channelId, filename +func (_m *API) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { + ret := _m.Called(data, channelId, filename) + + var r0 *model.FileInfo + if rf, ok := ret.Get(0).(func([]byte, string, string) *model.FileInfo); ok { + r0 = rf(data, channelId, filename) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.FileInfo) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func([]byte, string, string) *model.AppError); ok { + r1 = rf(data, channelId, filename) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} From e67d89b9a8715c8bd6ef9cf05e7edf729b75deca Mon Sep 17 00:00:00 2001 From: Hanzei <16541325+hanzei@users.noreply.github.com> Date: Thu, 8 Nov 2018 19:17:07 +0100 Subject: [PATCH 15/23] Add plugin methods to plugin API (#9744) --- app/plugin_api.go | 35 ++++++++ app/plugin_api_test.go | 64 ++++++++++++++- app/plugin_statuses.go | 21 +++++ model/client4.go | 6 +- plugin/api.go | 29 +++++++ plugin/client_rpc_generated.go | 141 +++++++++++++++++++++++++++++++++ plugin/plugintest/api.go | 98 +++++++++++++++++++++++ 7 files changed, 390 insertions(+), 4 deletions(-) diff --git a/app/plugin_api.go b/app/plugin_api.go index a0c449b874..6aae2e618c 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -438,6 +438,41 @@ func (api *PluginAPI) GetEmojiImage(emojiId string) ([]byte, string, *model.AppE return api.app.GetEmojiImage(emojiId) } +// Plugin Section + +func (api *PluginAPI) GetPlugins() ([]*model.Manifest, *model.AppError) { + plugins, err := api.app.GetPlugins() + if err != nil { + return nil, err + } + var manifests []*model.Manifest + for _, manifest := range plugins.Active { + manifests = append(manifests, &manifest.Manifest) + } + for _, manifest := range plugins.Inactive { + manifests = append(manifests, &manifest.Manifest) + } + return manifests, nil +} + +func (api *PluginAPI) EnablePlugin(id string) *model.AppError { + return api.app.EnablePlugin(id) +} + +func (api *PluginAPI) DisablePlugin(id string) *model.AppError { + return api.app.DisablePlugin(id) +} + +func (api *PluginAPI) RemovePlugin(id string) *model.AppError { + return api.app.RemovePlugin(id) +} + +func (api *PluginAPI) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { + return api.app.GetPluginStatus(id) +} + +// KV Store Section + func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError { return api.app.SetPluginKey(api.id, key, value) } diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 6c51189dee..7311348990 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -5,6 +5,7 @@ package app import ( "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -31,7 +32,10 @@ func setupPluginApiTest(t *testing.T, pluginCode string, pluginManifest string, compileGo(t, pluginCode, backend) ioutil.WriteFile(filepath.Join(pluginDir, pluginId, "plugin.json"), []byte(pluginManifest), 0600) - env.Activate(pluginId) + manifest, activated, reterr := env.Activate(pluginId) + require.Nil(t, reterr) + require.NotNil(t, manifest) + require.True(t, activated) app.Srv.Plugins = env } @@ -215,3 +219,61 @@ func TestPluginAPIGetProfileImage(t *testing.T) { require.NotNil(t, err) require.Nil(t, data) } + +func TestPluginAPIGetPlugins(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + pluginCode := ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + ` + + pluginDir, err := ioutil.TempDir("", "") + require.NoError(t, err) + webappPluginDir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(pluginDir) + defer os.RemoveAll(webappPluginDir) + + env, err := plugin.NewEnvironment(th.App.NewPluginAPI, pluginDir, webappPluginDir, th.App.Log) + require.NoError(t, err) + + pluginIDs := []string{"pluginid1", "pluginid2", "pluginid3"} + var pluginManifests []*model.Manifest + for _, pluginID := range pluginIDs { + backend := filepath.Join(pluginDir, pluginID, "backend.exe") + compileGo(t, pluginCode, backend) + + ioutil.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(fmt.Sprintf(`{"id": "%s", "server": {"executable": "backend.exe"}}`, pluginID)), 0600) + manifest, activated, reterr := env.Activate(pluginID) + + require.Nil(t, reterr) + require.NotNil(t, manifest) + require.True(t, activated) + pluginManifests = append(pluginManifests, manifest) + } + th.App.Srv.Plugins = env + + // Decative the last one for testing + sucess := env.Deactivate(pluginIDs[len(pluginIDs)-1]) + require.True(t, sucess) + + // check existing user first + plugins, err := api.GetPlugins() + assert.Nil(t, err) + assert.NotEmpty(t, plugins) + assert.Equal(t, pluginManifests, plugins) +} diff --git a/app/plugin_statuses.go b/app/plugin_statuses.go index df6477f89b..fc5561bcfc 100644 --- a/app/plugin_statuses.go +++ b/app/plugin_statuses.go @@ -9,6 +9,27 @@ import ( "github.com/mattermost/mattermost-server/model" ) +// GetPluginStatus returns the status for a plugin installed on this server. +func (a *App) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { + if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { + return nil, model.NewAppError("GetPluginStatus", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + pluginStatuses, err := a.Srv.Plugins.Statuses() + if err != nil { + return nil, model.NewAppError("GetPluginStatus", "app.plugin.get_statuses.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // Add our cluster ID + for _, status := range pluginStatuses { + if status.PluginId == id { + status.ClusterId = a.GetClusterId() + return status, nil + } + } + return nil, model.NewAppError("GetPluginStatus", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) +} + // GetPluginStatuses returns the status for plugins installed on this server. func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { if a.Srv.Plugins == nil || !*a.Config().PluginSettings.Enable { diff --git a/model/client4.go b/model/client4.go index cce3b62ef9..484694416b 100644 --- a/model/client4.go +++ b/model/client4.go @@ -3771,7 +3771,7 @@ func (c *Client4) GetPluginStatuses() (PluginStatuses, *Response) { } } -// RemovePlugin will deactivate and delete a plugin. +// RemovePlugin will disable and delete a plugin. // WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. func (c *Client4) RemovePlugin(id string) (bool, *Response) { if r, err := c.DoApiDelete(c.GetPluginRoute(id)); err != nil { @@ -3793,7 +3793,7 @@ func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response) { } } -// ActivatePlugin will activate an plugin installed. +// EnablePlugin will enable an plugin installed. // WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. func (c *Client4) EnablePlugin(id string) (bool, *Response) { if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/enable", ""); err != nil { @@ -3804,7 +3804,7 @@ func (c *Client4) EnablePlugin(id string) (bool, *Response) { } } -// DeactivatePlugin will deactivate an active plugin. +// DisablePlugin will disable an enabled plugin. // WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. func (c *Client4) DisablePlugin(id string) (bool, *Response) { if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/disable", ""); err != nil { diff --git a/plugin/api.go b/plugin/api.go index 2589fa9e01..33d07c40ac 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -314,6 +314,35 @@ type API interface { // Minimum server version: 5.6 UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) + // Plugin Section + + // GetPlugins will return a list of plugin manifests for currently active plugins. + // + // Minimum server version: 5.6 + GetPlugins() ([]*model.Manifest, *model.AppError) + + // EnablePlugin will enable an plugin installed. + // + // Minimum server version: 5.6 + EnablePlugin(id string) *model.AppError + + // DisablePlugin will disable an enabled plugin. + // + // Minimum server version: 5.6 + DisablePlugin(id string) *model.AppError + + // RemovePlugin will disable and delete a plugin. + // + // Minimum server version: 5.6 + RemovePlugin(id string) *model.AppError + + // GetPluginStatus will return the status of a plugin. + // + // Minimum server version: 5.6 + GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) + + // KV Store Section + // KVSet will store a key-value pair, unique per plugin. KVSet(key string, value []byte) *model.AppError diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 6b20dd8679..2e18934715 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -2728,6 +2728,147 @@ func (s *apiRPCServer) UploadFile(args *Z_UploadFileArgs, returns *Z_UploadFileR return nil } +type Z_GetPluginsArgs struct { +} + +type Z_GetPluginsReturns struct { + A []*model.Manifest + B *model.AppError +} + +func (g *apiRPCClient) GetPlugins() ([]*model.Manifest, *model.AppError) { + _args := &Z_GetPluginsArgs{} + _returns := &Z_GetPluginsReturns{} + if err := g.client.Call("Plugin.GetPlugins", _args, _returns); err != nil { + log.Printf("RPC call to GetPlugins API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetPlugins(args *Z_GetPluginsArgs, returns *Z_GetPluginsReturns) error { + if hook, ok := s.impl.(interface { + GetPlugins() ([]*model.Manifest, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetPlugins() + } else { + return encodableError(fmt.Errorf("API GetPlugins called but not implemented.")) + } + return nil +} + +type Z_GetPluginStatusArgs struct { + A string +} + +type Z_GetPluginStatusReturns struct { + A *model.PluginStatus + B *model.AppError +} + +func (g *apiRPCClient) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { + _args := &Z_GetPluginStatusArgs{id} + _returns := &Z_GetPluginStatusReturns{} + if err := g.client.Call("Plugin.GetPluginStatus", _args, _returns); err != nil { + log.Printf("RPC call to GetPluginStatus API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetPluginStatus(args *Z_GetPluginStatusArgs, returns *Z_GetPluginStatusReturns) error { + if hook, ok := s.impl.(interface { + GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetPluginStatus(args.A) + } else { + return encodableError(fmt.Errorf("API GetPluginStatus called but not implemented.")) + } + return nil +} + +type Z_EnablePluginArgs struct { + A string +} + +type Z_EnablePluginReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) EnablePlugin(id string) *model.AppError { + _args := &Z_EnablePluginArgs{id} + _returns := &Z_EnablePluginReturns{} + if err := g.client.Call("Plugin.EnablePlugin", _args, _returns); err != nil { + log.Printf("RPC call to EnablePlugin API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) EnablePlugin(args *Z_EnablePluginArgs, returns *Z_EnablePluginReturns) error { + if hook, ok := s.impl.(interface { + EnablePlugin(id string) *model.AppError + }); ok { + returns.A = hook.EnablePlugin(args.A) + } else { + return encodableError(fmt.Errorf("API EnablePlugin called but not implemented.")) + } + return nil +} + +type Z_DisablePluginArgs struct { + A string +} + +type Z_DisablePluginReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) DisablePlugin(id string) *model.AppError { + _args := &Z_DisablePluginArgs{id} + _returns := &Z_DisablePluginReturns{} + if err := g.client.Call("Plugin.DisablePlugin", _args, _returns); err != nil { + log.Printf("RPC call to DisablePlugin API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) DisablePlugin(args *Z_DisablePluginArgs, returns *Z_DisablePluginReturns) error { + if hook, ok := s.impl.(interface { + DisablePlugin(id string) *model.AppError + }); ok { + returns.A = hook.DisablePlugin(args.A) + } else { + return encodableError(fmt.Errorf("API DisablePlugin called but not implemented.")) + } + return nil +} + +type Z_RemovePluginArgs struct { + A string +} + +type Z_RemovePluginReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) RemovePlugin(id string) *model.AppError { + _args := &Z_RemovePluginArgs{id} + _returns := &Z_RemovePluginReturns{} + if err := g.client.Call("Plugin.RemovePlugin", _args, _returns); err != nil { + log.Printf("RPC call to RemovePlugin API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) RemovePlugin(args *Z_RemovePluginArgs, returns *Z_RemovePluginReturns) error { + if hook, ok := s.impl.(interface { + RemovePlugin(id string) *model.AppError + }); ok { + returns.A = hook.RemovePlugin(args.A) + } else { + return encodableError(fmt.Errorf("API RemovePlugin called but not implemented.")) + } + return nil +} + type Z_KVSetArgs struct { A string B []byte diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index 14628095d2..d192fe713f 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -333,6 +333,38 @@ func (_m *API) DeleteUser(userId string) *model.AppError { return r0 } +// DisablePlugin provides a mock function with given fields: id +func (_m *API) DisablePlugin(id string) *model.AppError { + ret := _m.Called(id) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + +// EnablePlugin provides a mock function with given fields: id +func (_m *API) EnablePlugin(id string) *model.AppError { + ret := _m.Called(id) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // GetChannel provides a mock function with given fields: channelId func (_m *API) GetChannel(channelId string) (*model.Channel, *model.AppError) { ret := _m.Called(channelId) @@ -779,6 +811,56 @@ func (_m *API) GetLDAPUserAttributes(userId string, attributes []string) (map[st return r0, r1 } +// GetPluginStatus provides a mock function with given fields: id +func (_m *API) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { + ret := _m.Called(id) + + var r0 *model.PluginStatus + if rf, ok := ret.Get(0).(func(string) *model.PluginStatus); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.PluginStatus) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(id) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetPlugins provides a mock function with given fields: +func (_m *API) GetPlugins() ([]*model.Manifest, *model.AppError) { + ret := _m.Called() + + var r0 []*model.Manifest + if rf, ok := ret.Get(0).(func() []*model.Manifest); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Manifest) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func() *model.AppError); ok { + r1 = rf() + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetPost provides a mock function with given fields: postId func (_m *API) GetPost(postId string) (*model.Post, *model.AppError) { ret := _m.Called(postId) @@ -1664,6 +1746,22 @@ func (_m *API) RegisterCommand(command *model.Command) error { return r0 } +// RemovePlugin provides a mock function with given fields: id +func (_m *API) RemovePlugin(id string) *model.AppError { + ret := _m.Called(id) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // RemoveReaction provides a mock function with given fields: reaction func (_m *API) RemoveReaction(reaction *model.Reaction) *model.AppError { ret := _m.Called(reaction) From 8d56fcf568308fc3b29d241a856dc55654d2ed54 Mon Sep 17 00:00:00 2001 From: Daniel Fiori Date: Thu, 8 Nov 2018 13:23:07 -0500 Subject: [PATCH 16/23] Add library and command for human-readable logs (#9809) * Update logrus to 1.2 and add as a direct dependency * Create an mlog/human package for pretty-printing logs This package can read JSON logs from mattermost.log, and output the data to either logrus or a custom formatter, to make the logs more human readable. * Create a command for outputting human-readable logs This command will read JSON data from mattermost.log or stdin, and output in a human readable format. An optional argument can be used to activate logrus output (which includes color support). * Reorganize code in mlog/human and improve logrus timestamp formatting --- Gopkg.lock | 7 +- Gopkg.toml | 4 + cmd/mattermost/commands/logs.go | 52 +++++ mlog/human/entry.go | 51 +++++ mlog/human/logrus_writer.go | 76 ++++++++ mlog/human/parser.go | 180 ++++++++++++++++++ mlog/human/process.go | 23 +++ mlog/human/simple_writer.go | 23 +++ vendor/github.com/sirupsen/logrus/.gitignore | 1 + .../github.com/sirupsen/logrus/CHANGELOG.md | 12 ++ vendor/github.com/sirupsen/logrus/README.md | 36 +++- vendor/github.com/sirupsen/logrus/entry.go | 134 +++++++++++-- vendor/github.com/sirupsen/logrus/exported.go | 21 ++ .../github.com/sirupsen/logrus/formatter.go | 33 +++- vendor/github.com/sirupsen/logrus/go.mod | 3 +- vendor/github.com/sirupsen/logrus/go.sum | 3 + .../sirupsen/logrus/json_formatter.go | 23 +-- vendor/github.com/sirupsen/logrus/logger.go | 62 +++++- vendor/github.com/sirupsen/logrus/logrus.go | 30 ++- .../sirupsen/logrus/terminal_appengine.go | 13 -- .../sirupsen/logrus/terminal_bsd.go | 17 -- .../sirupsen/logrus/terminal_linux.go | 21 -- .../sirupsen/logrus/terminal_notwindows.go | 8 + .../sirupsen/logrus/text_formatter.go | 40 +++- vendor/github.com/sirupsen/logrus/writer.go | 2 + 25 files changed, 773 insertions(+), 102 deletions(-) create mode 100644 cmd/mattermost/commands/logs.go create mode 100644 mlog/human/entry.go create mode 100644 mlog/human/logrus_writer.go create mode 100644 mlog/human/parser.go create mode 100644 mlog/human/process.go create mode 100644 mlog/human/simple_writer.go delete mode 100644 vendor/github.com/sirupsen/logrus/terminal_appengine.go delete mode 100644 vendor/github.com/sirupsen/logrus/terminal_bsd.go delete mode 100644 vendor/github.com/sirupsen/logrus/terminal_linux.go create mode 100644 vendor/github.com/sirupsen/logrus/terminal_notwindows.go diff --git a/Gopkg.lock b/Gopkg.lock index 245ade8e6f..cb5ea9f042 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -641,12 +641,12 @@ revision = "204274ad699c0983a70203a566887f17a717fef4" [[projects]] - digest = "1:dc2d85c13ac22c22a1f3170a41a8e1b897fa05134aaf533f16df44f66a25b4a1" + digest = "1:69b1cc331fca23d702bd72f860c6a647afd0aa9fcbc1d0659b1365e26546dd70" name = "github.com/sirupsen/logrus" packages = ["."] pruneopts = "UT" - revision = "a67f783a3814b8729bd2dac5780b5f78f8dbd64d" - version = "v1.1.0" + revision = "bcd833dfe83d3cebad139e4a29ed79cb2318bf95" + version = "v1.2.0" [[projects]] digest = "1:6a4a11ba764a56d2758899ec6f3848d24698d48442ebce85ee7a3f63284526cd" @@ -1031,6 +1031,7 @@ "github.com/rs/cors", "github.com/rwcarlsen/goexif/exif", "github.com/segmentio/analytics-go", + "github.com/sirupsen/logrus", "github.com/spf13/cobra", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/mock", diff --git a/Gopkg.toml b/Gopkg.toml index ac196bb50a..0321ceddfd 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -61,3 +61,7 @@ [prune] go-tests = true unused-packages = true + +[[constraint]] + name = "github.com/sirupsen/logrus" + version = "1.2.0" diff --git a/cmd/mattermost/commands/logs.go b/cmd/mattermost/commands/logs.go new file mode 100644 index 0000000000..f5e2df3573 --- /dev/null +++ b/cmd/mattermost/commands/logs.go @@ -0,0 +1,52 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "github.com/mattermost/mattermost-server/mlog/human" + "github.com/spf13/cobra" + "io" + "os" +) + +var LogsCmd = &cobra.Command{ + Use: "logs", + Short: "Display logs in a human-readable format", + RunE: logsCmdF, +} + +func init() { + LogsCmd.Flags().Bool("logrus", false, "Use logrus for formatting.") + RootCmd.AddCommand(LogsCmd) +} + +func logsCmdF(command *cobra.Command, args []string) error { + // check stdin to see if we have a pipe + fi, err := os.Stdin.Stat() + if err != nil { + return err + } + + var input io.Reader + if fi.Size() == 0 && fi.Mode()&os.ModeNamedPipe == 0 { + file, err := os.Open("mattermost.log") + if err != nil { + return err + } + defer file.Close() + input = file + } else { + input = os.Stdin + } + var writer human.LogWriter + + if flag, _ := command.Flags().GetBool("logrus"); flag { + writer = human.NewLogrusWriter(os.Stdout) + } else { + writer = human.NewSimpleWriter(os.Stdout) + } + human.ProcessLogs(input, writer) + + return nil +} diff --git a/mlog/human/entry.go b/mlog/human/entry.go new file mode 100644 index 0000000000..048e5e5a19 --- /dev/null +++ b/mlog/human/entry.go @@ -0,0 +1,51 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package human + +import ( + "fmt" + "github.com/mattermost/mattermost-server/mlog" + "strings" + "time" +) + +type LogEntry struct { + Time time.Time + Level string + Message string + Caller string + Fields []mlog.Field +} + +// Provide default string representation. Used by SimpleWriter +func (f LogEntry) String() string { + var sb strings.Builder + if !f.Time.IsZero() { + sb.WriteString(f.Time.Format(time.RFC3339Nano)) + sb.WriteRune(' ') + } + if f.Level != "" { + sb.WriteString(f.Level) + sb.WriteRune(' ') + } + if f.Caller != "" { + sb.WriteString(f.Caller) + sb.WriteRune(' ') + } + for _, field := range f.Fields { + sb.WriteString(field.Key) + sb.WriteRune('=') + sb.WriteString(fmt.Sprint(field.Interface)) + sb.WriteRune(' ') + } + if f.Message != "" { + // If the message is multiple lines, start the whole message on a new line + if strings.ContainsRune(f.Message, '\n') { + sb.WriteRune('\n') + } + sb.WriteString(f.Message) + } + + return sb.String() +} diff --git a/mlog/human/logrus_writer.go b/mlog/human/logrus_writer.go new file mode 100644 index 0000000000..67192d8742 --- /dev/null +++ b/mlog/human/logrus_writer.go @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package human + +import ( + "fmt" + "github.com/sirupsen/logrus" + "io" + "time" +) + +type LogrusWriter struct { + logger *logrus.Logger +} + +func (w *LogrusWriter) Write(e LogEntry) { + if e.Level == "" { + fmt.Fprintln(w.logger.Out, e.Message) + return + } + + lvl, err := logrus.ParseLevel(e.Level) + if err != nil { + fmt.Fprintln(w.logger.Out, err) + lvl = logrus.TraceLevel + 1 // will invoke Println + } + + logger := w.logger.WithTime(e.Time) + + if e.Caller != "" { + // logrus has a system of reporting the caller, but there's no easy way to override it + logger = logger.WithField("caller", e.Caller) + } + + for _, field := range e.Fields { + logger = logger.WithField(field.Key, field.Interface) + } + + switch lvl { + case logrus.PanicLevel: + // Prevent panic from causing us to exit + defer func() { + recover() + }() + logger.Panic(e.Message) + case logrus.FatalLevel: + logger.Fatal(e.Message) + case logrus.ErrorLevel: + logger.Error(e.Message) + case logrus.WarnLevel: + logger.Warn(e.Message) + case logrus.InfoLevel: + logger.Info(e.Message) + case logrus.DebugLevel: + logger.Debug(e.Message) + case logrus.TraceLevel: + logger.Trace(e.Message) + default: + logger.Println(e.Message) + } +} + +func NewLogrusWriter(output io.Writer) *LogrusWriter { + w := new(LogrusWriter) + w.logger = logrus.New() + w.logger.SetLevel(logrus.TraceLevel) // don't filter any logs + w.logger.ExitFunc = func(int) {} // prevent Fatal from causing us to exit + w.logger.SetReportCaller(false) + w.logger.SetOutput(output) + var tf logrus.TextFormatter + tf.FullTimestamp = true + tf.TimestampFormat = time.RFC3339Nano + w.logger.SetFormatter(&tf) + return w +} diff --git a/mlog/human/parser.go b/mlog/human/parser.go new file mode 100644 index 0000000000..6071baf16b --- /dev/null +++ b/mlog/human/parser.go @@ -0,0 +1,180 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package human + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/mattermost/mattermost-server/mlog" + "io" + "strconv" + "strings" + "time" +) + +func ParseLogMessage(msg string) LogEntry { + result, err := parseLogMessage(msg) + if err != nil { + // If failed to parse, just output a LogEntry where all fields are blank, but Message is the original string + var result2 LogEntry + result2.Message = msg + return result2 + } + return result +} + +func parseLogMessage(msg string) (result LogEntry, err error) { + + // Note: This implementation uses a custom json decoding loop. + // The primary advantage of this versus decoding directly into a map is to + // preserve the order of the fields. This can be simplified if we end up + // having the formatter sort fields alphabetically (logrus does by default) + + dec := json.NewDecoder(strings.NewReader(msg)) + + // look for an initial "{" + if token, err := dec.Token(); err != nil { + return result, err + } else { + d, ok := token.(json.Delim) + if !ok || d != '{' { + return result, errors.New(fmt.Sprintf("input is not a JSON object, found: %v", token)) + } + } + + // read all key-value pairs + for dec.More() { + key, err := dec.Token() + if err != nil { + return result, err + } + if skey, ok := key.(string); !ok { + return result, errors.New("key is not a value string") + } else { + if !dec.More() { + return result, errors.New("missing value pair") + } + + switch skey { + case "ts": + var ts json.Number + if err := dec.Decode(&ts); err != nil { + return result, err + } + if time, err := numberToTime(ts); err != nil { + return result, err + } else { + result.Time = time + } + + case "level": + if s, err := decodeAsString(dec); err != nil { + return result, err + } else { + result.Level = s + } + + case "msg": + if s, err := decodeAsString(dec); err != nil { + return result, err + } else { + result.Message = s + } + + case "caller": + if s, err := decodeAsString(dec); err != nil { + return result, err + } else { + result.Caller = s + } + + default: + var p interface{} + if err := dec.Decode(&p); err != nil { + return result, err + } + var f mlog.Field + f.Key = skey + f.Interface = p + result.Fields = append(result.Fields, f) + } + } + } + + // read the "}" + if token, err := dec.Token(); err != nil { + return result, err + } else { + d, ok := token.(json.Delim) + if !ok || d != '}' { + return result, errors.New(fmt.Sprintf("failed to read '}', read: %v", token)) + } + } + + // make sure nothing else trailing + if token, err := dec.Token(); err != io.EOF { + return result, err + } else if token != nil { + return result, errors.New("found trailing data") + } + + return result, nil +} + +// Translate a number into a time +func numberToTime(v json.Number) (time.Time, error) { + // Using floating point math to extract the nanoseconds leads to a time that doesn't exactly match the input + // Instead, parse out the components from the string representation + + var t time.Time + + // First make sure it is a number... + flt, err := v.Float64() + if err != nil { + return t, err + } + + s := v.String() + + if strings.ContainsAny(s, "eE") { + // input is in scientific notation. Convert to standard decimal notation + s = strconv.FormatFloat(flt, 'f', -1, 64) + } + + // extract the seconds and nanoseconds separately + var nanos, sec int64 + + parts := strings.SplitN(s, ".", 2) + sec, err = strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return t, err + } + + if len(parts) == 2 { + nanosText := parts[1] + "000000000" + nanosText = nanosText[:9] + nanos, err = strconv.ParseInt(nanosText, 10, 64) + if err != nil { + return t, err + } + } + + t = time.Unix(sec, nanos) + return t, nil +} + +// Decodes a value from JSON, coercing it to a string value as necessary +func decodeAsString(dec *json.Decoder) (s string, err error) { + var v interface{} + if err = dec.Decode(&v); err != nil { + return s, err + } + var ok bool + if s, ok = v.(string); ok { + return s, err + } + s = fmt.Sprint(v) + return s, err +} diff --git a/mlog/human/process.go b/mlog/human/process.go new file mode 100644 index 0000000000..71633c3e0a --- /dev/null +++ b/mlog/human/process.go @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package human + +import ( + "bufio" + "io" +) + +type LogWriter interface { + Write(e LogEntry) +} + +// Read JSON logs from input and write formatted logs to the output +func ProcessLogs(reader io.Reader, writer LogWriter) { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + s := scanner.Text() + e := ParseLogMessage(s) + writer.Write(e) + } +} diff --git a/mlog/human/simple_writer.go b/mlog/human/simple_writer.go new file mode 100644 index 0000000000..3ffba9a20d --- /dev/null +++ b/mlog/human/simple_writer.go @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package human + +import ( + "fmt" + "io" +) + +type SimpleWriter struct { + out io.Writer +} + +func (w *SimpleWriter) Write(e LogEntry) { + fmt.Fprintln(w.out, e) +} + +func NewSimpleWriter(out io.Writer) *SimpleWriter { + w := new(SimpleWriter) + w.out = out + return w +} diff --git a/vendor/github.com/sirupsen/logrus/.gitignore b/vendor/github.com/sirupsen/logrus/.gitignore index 66be63a005..6b7d7d1e8b 100644 --- a/vendor/github.com/sirupsen/logrus/.gitignore +++ b/vendor/github.com/sirupsen/logrus/.gitignore @@ -1 +1,2 @@ logrus +vendor diff --git a/vendor/github.com/sirupsen/logrus/CHANGELOG.md b/vendor/github.com/sirupsen/logrus/CHANGELOG.md index 1702696087..cb85d9f9f6 100644 --- a/vendor/github.com/sirupsen/logrus/CHANGELOG.md +++ b/vendor/github.com/sirupsen/logrus/CHANGELOG.md @@ -1,3 +1,15 @@ +# 1.2.0 +This new release introduces: + * A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued + * A new trace level named `Trace` whose level is below `Debug` + * A configurable exit function to be called upon a Fatal trace + * The `Level` object now implements `encoding.TextUnmarshaler` interface + +# 1.1.1 +This is a bug fix release. + * fix the build break on Solaris + * don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized + # 1.1.0 This new release introduces: * several fixes: diff --git a/vendor/github.com/sirupsen/logrus/README.md b/vendor/github.com/sirupsen/logrus/README.md index 072e99be31..093bb13f83 100644 --- a/vendor/github.com/sirupsen/logrus/README.md +++ b/vendor/github.com/sirupsen/logrus/README.md @@ -56,8 +56,39 @@ time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4 time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009 time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true -exit status 1 ``` +To ensure this behaviour even if a TTY is attached, set your formatter as follows: + +```go + log.SetFormatter(&log.TextFormatter{ + DisableColors: true, + FullTimestamp: true, + }) +``` + +#### Logging Method Name + +If you wish to add the calling method as a field, instruct the logger via: +```go +log.SetReportCaller(true) +``` +This adds the caller as 'method' like so: + +```json +{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by", +"time":"2014-03-10 19:57:38.562543129 -0400 EDT"} +``` + +```text +time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin +``` +Note that this does add measurable overhead - the cost will depend on the version of Go, but is +between 20 and 40% in recent tests with 1.6 and 1.7. You can validate this in your +environment via benchmarks: +``` +go test -bench=.*CallerTracing +``` + #### Case-sensitivity @@ -246,9 +277,10 @@ A list of currently known of service hook can be found in this wiki [page](https #### Level logging -Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic. +Logrus has seven logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic. ```go +log.Trace("Something very low level.") log.Debug("Useful debugging information.") log.Info("Something noteworthy happened!") log.Warn("You should probably take a look at this.") diff --git a/vendor/github.com/sirupsen/logrus/entry.go b/vendor/github.com/sirupsen/logrus/entry.go index 4efedddfea..cc85d3aab4 100644 --- a/vendor/github.com/sirupsen/logrus/entry.go +++ b/vendor/github.com/sirupsen/logrus/entry.go @@ -4,11 +4,30 @@ import ( "bytes" "fmt" "os" + "reflect" + "runtime" + "strings" "sync" "time" ) -var bufferPool *sync.Pool +var ( + bufferPool *sync.Pool + + // qualified package name, cached at first use + logrusPackage string + + // Positions in the call stack when tracing to report the calling method + minimumCallerDepth int + + // Used for caller information initialisation + callerInitOnce sync.Once +) + +const ( + maximumCallerDepth int = 25 + knownLogrusFrames int = 4 +) func init() { bufferPool = &sync.Pool{ @@ -16,15 +35,18 @@ func init() { return new(bytes.Buffer) }, } + + // start at the bottom of the stack before the package-name cache is primed + minimumCallerDepth = 1 } // Defines the key when adding errors using WithError. var ErrorKey = "error" // An entry is the final or intermediate Logrus logging entry. It contains all -// the fields passed with WithField{,s}. It's finally logged when Debug, Info, -// Warn, Error, Fatal or Panic is called on it. These objects can be reused and -// passed around as much as you wish to avoid field duplication. +// the fields passed with WithField{,s}. It's finally logged when Trace, Debug, +// Info, Warn, Error, Fatal or Panic is called on it. These objects can be +// reused and passed around as much as you wish to avoid field duplication. type Entry struct { Logger *Logger @@ -34,22 +56,28 @@ type Entry struct { // Time at which the log entry was created Time time.Time - // Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic + // Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic // This field will be set on entry firing and the value will be equal to the one in Logger struct field. Level Level - // Message passed to Debug, Info, Warn, Error, Fatal or Panic + // Calling method, with package name + Caller *runtime.Frame + + // Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic Message string // When formatter is called in entry.log(), a Buffer may be set to entry Buffer *bytes.Buffer + + // err may contain a field formatting error + err string } func NewEntry(logger *Logger) *Entry { return &Entry{ Logger: logger, - // Default is five fields, give a little extra room - Data: make(Fields, 5), + // Default is three fields, plus one optional. Give a little extra room. + Data: make(Fields, 6), } } @@ -80,10 +108,18 @@ func (entry *Entry) WithFields(fields Fields) *Entry { for k, v := range entry.Data { data[k] = v } + var field_err string for k, v := range fields { - data[k] = v + if t := reflect.TypeOf(v); t != nil && t.Kind() == reflect.Func { + field_err = fmt.Sprintf("can not add field %q", k) + if entry.err != "" { + field_err = entry.err + ", " + field_err + } + } else { + data[k] = v + } } - return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time} + return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: field_err} } // Overrides the time of the Entry. @@ -91,6 +127,57 @@ func (entry *Entry) WithTime(t time.Time) *Entry { return &Entry{Logger: entry.Logger, Data: entry.Data, Time: t} } +// getPackageName reduces a fully qualified function name to the package name +// There really ought to be to be a better way... +func getPackageName(f string) string { + for { + lastPeriod := strings.LastIndex(f, ".") + lastSlash := strings.LastIndex(f, "/") + if lastPeriod > lastSlash { + f = f[:lastPeriod] + } else { + break + } + } + + return f +} + +// getCaller retrieves the name of the first non-logrus calling function +func getCaller() *runtime.Frame { + // Restrict the lookback frames to avoid runaway lookups + pcs := make([]uintptr, maximumCallerDepth) + depth := runtime.Callers(minimumCallerDepth, pcs) + frames := runtime.CallersFrames(pcs[:depth]) + + // cache this package's fully-qualified name + callerInitOnce.Do(func() { + logrusPackage = getPackageName(runtime.FuncForPC(pcs[0]).Name()) + + // now that we have the cache, we can skip a minimum count of known-logrus functions + // XXX this is dubious, the number of frames may vary store an entry in a logger interface + minimumCallerDepth = knownLogrusFrames + }) + + for f, again := frames.Next(); again; f, again = frames.Next() { + pkg := getPackageName(f.Function) + + // If the caller isn't part of this package, we're done + if pkg != logrusPackage { + return &f + } + } + + // if we got here, we failed to find the caller's context + return nil +} + +func (entry Entry) HasCaller() (has bool) { + return entry.Logger != nil && + entry.Logger.ReportCaller && + entry.Caller != nil +} + // This function is not declared with a pointer value because otherwise // race conditions will occur when using multiple goroutines func (entry Entry) log(level Level, msg string) { @@ -107,6 +194,9 @@ func (entry Entry) log(level Level, msg string) { entry.Level = level entry.Message = msg + if entry.Logger.ReportCaller { + entry.Caller = getCaller() + } entry.fireHooks() @@ -150,6 +240,12 @@ func (entry *Entry) write() { } } +func (entry *Entry) Trace(args ...interface{}) { + if entry.Logger.IsLevelEnabled(TraceLevel) { + entry.log(TraceLevel, fmt.Sprint(args...)) + } +} + func (entry *Entry) Debug(args ...interface{}) { if entry.Logger.IsLevelEnabled(DebugLevel) { entry.log(DebugLevel, fmt.Sprint(args...)) @@ -186,7 +282,7 @@ func (entry *Entry) Fatal(args ...interface{}) { if entry.Logger.IsLevelEnabled(FatalLevel) { entry.log(FatalLevel, fmt.Sprint(args...)) } - Exit(1) + entry.Logger.Exit(1) } func (entry *Entry) Panic(args ...interface{}) { @@ -198,6 +294,12 @@ func (entry *Entry) Panic(args ...interface{}) { // Entry Printf family functions +func (entry *Entry) Tracef(format string, args ...interface{}) { + if entry.Logger.IsLevelEnabled(TraceLevel) { + entry.Trace(fmt.Sprintf(format, args...)) + } +} + func (entry *Entry) Debugf(format string, args ...interface{}) { if entry.Logger.IsLevelEnabled(DebugLevel) { entry.Debug(fmt.Sprintf(format, args...)) @@ -234,7 +336,7 @@ func (entry *Entry) Fatalf(format string, args ...interface{}) { if entry.Logger.IsLevelEnabled(FatalLevel) { entry.Fatal(fmt.Sprintf(format, args...)) } - Exit(1) + entry.Logger.Exit(1) } func (entry *Entry) Panicf(format string, args ...interface{}) { @@ -245,6 +347,12 @@ func (entry *Entry) Panicf(format string, args ...interface{}) { // Entry Println family functions +func (entry *Entry) Traceln(args ...interface{}) { + if entry.Logger.IsLevelEnabled(TraceLevel) { + entry.Trace(entry.sprintlnn(args...)) + } +} + func (entry *Entry) Debugln(args ...interface{}) { if entry.Logger.IsLevelEnabled(DebugLevel) { entry.Debug(entry.sprintlnn(args...)) @@ -281,7 +389,7 @@ func (entry *Entry) Fatalln(args ...interface{}) { if entry.Logger.IsLevelEnabled(FatalLevel) { entry.Fatal(entry.sprintlnn(args...)) } - Exit(1) + entry.Logger.Exit(1) } func (entry *Entry) Panicln(args ...interface{}) { diff --git a/vendor/github.com/sirupsen/logrus/exported.go b/vendor/github.com/sirupsen/logrus/exported.go index fb2a7a1f07..7342613c37 100644 --- a/vendor/github.com/sirupsen/logrus/exported.go +++ b/vendor/github.com/sirupsen/logrus/exported.go @@ -24,6 +24,12 @@ func SetFormatter(formatter Formatter) { std.SetFormatter(formatter) } +// SetReportCaller sets whether the standard logger will include the calling +// method as a field. +func SetReportCaller(include bool) { + std.SetReportCaller(include) +} + // SetLevel sets the standard logger level. func SetLevel(level Level) { std.SetLevel(level) @@ -77,6 +83,11 @@ func WithTime(t time.Time) *Entry { return std.WithTime(t) } +// Trace logs a message at level Trace on the standard logger. +func Trace(args ...interface{}) { + std.Trace(args...) +} + // Debug logs a message at level Debug on the standard logger. func Debug(args ...interface{}) { std.Debug(args...) @@ -117,6 +128,11 @@ func Fatal(args ...interface{}) { std.Fatal(args...) } +// Tracef logs a message at level Trace on the standard logger. +func Tracef(format string, args ...interface{}) { + std.Tracef(format, args...) +} + // Debugf logs a message at level Debug on the standard logger. func Debugf(format string, args ...interface{}) { std.Debugf(format, args...) @@ -157,6 +173,11 @@ func Fatalf(format string, args ...interface{}) { std.Fatalf(format, args...) } +// Traceln logs a message at level Trace on the standard logger. +func Traceln(args ...interface{}) { + std.Traceln(args...) +} + // Debugln logs a message at level Debug on the standard logger. func Debugln(args ...interface{}) { std.Debugln(args...) diff --git a/vendor/github.com/sirupsen/logrus/formatter.go b/vendor/github.com/sirupsen/logrus/formatter.go index 83c74947be..408883773e 100644 --- a/vendor/github.com/sirupsen/logrus/formatter.go +++ b/vendor/github.com/sirupsen/logrus/formatter.go @@ -2,7 +2,16 @@ package logrus import "time" -const defaultTimestampFormat = time.RFC3339 +// Default key names for the default fields +const ( + defaultTimestampFormat = time.RFC3339 + FieldKeyMsg = "msg" + FieldKeyLevel = "level" + FieldKeyTime = "time" + FieldKeyLogrusError = "logrus_error" + FieldKeyFunc = "func" + FieldKeyFile = "file" +) // The Formatter interface is used to implement a custom Formatter. It takes an // `Entry`. It exposes all the fields, including the default ones: @@ -18,7 +27,7 @@ type Formatter interface { Format(*Entry) ([]byte, error) } -// This is to not silently overwrite `time`, `msg` and `level` fields when +// This is to not silently overwrite `time`, `msg`, `func` and `level` fields when // dumping it. If this code wasn't there doing: // // logrus.WithField("level", 1).Info("hello") @@ -30,7 +39,7 @@ type Formatter interface { // // It's not exported because it's still using Data in an opinionated way. It's to // avoid code duplication between the two default formatters. -func prefixFieldClashes(data Fields, fieldMap FieldMap) { +func prefixFieldClashes(data Fields, fieldMap FieldMap, reportCaller bool) { timeKey := fieldMap.resolve(FieldKeyTime) if t, ok := data[timeKey]; ok { data["fields."+timeKey] = t @@ -48,4 +57,22 @@ func prefixFieldClashes(data Fields, fieldMap FieldMap) { data["fields."+levelKey] = l delete(data, levelKey) } + + logrusErrKey := fieldMap.resolve(FieldKeyLogrusError) + if l, ok := data[logrusErrKey]; ok { + data["fields."+logrusErrKey] = l + delete(data, logrusErrKey) + } + + // If reportCaller is not set, 'func' will not conflict. + if reportCaller { + funcKey := fieldMap.resolve(FieldKeyFunc) + if l, ok := data[funcKey]; ok { + data["fields."+funcKey] = l + } + fileKey := fieldMap.resolve(FieldKeyFile) + if l, ok := data[fileKey]; ok { + data["fields."+fileKey] = l + } + } } diff --git a/vendor/github.com/sirupsen/logrus/go.mod b/vendor/github.com/sirupsen/logrus/go.mod index f4fed02fb8..94574cc635 100644 --- a/vendor/github.com/sirupsen/logrus/go.mod +++ b/vendor/github.com/sirupsen/logrus/go.mod @@ -2,8 +2,9 @@ module github.com/sirupsen/logrus require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe + github.com/konsorten/go-windows-terminal-sequences v1.0.1 github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.2.2 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 diff --git a/vendor/github.com/sirupsen/logrus/go.sum b/vendor/github.com/sirupsen/logrus/go.sum index 1f0d71964c..133d34ae11 100644 --- a/vendor/github.com/sirupsen/logrus/go.sum +++ b/vendor/github.com/sirupsen/logrus/go.sum @@ -2,8 +2,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= diff --git a/vendor/github.com/sirupsen/logrus/json_formatter.go b/vendor/github.com/sirupsen/logrus/json_formatter.go index d3dadefe69..2605753599 100644 --- a/vendor/github.com/sirupsen/logrus/json_formatter.go +++ b/vendor/github.com/sirupsen/logrus/json_formatter.go @@ -11,13 +11,6 @@ type fieldKey string // FieldMap allows customization of the key names for default fields. type FieldMap map[fieldKey]string -// Default key names for the default fields -const ( - FieldKeyMsg = "msg" - FieldKeyLevel = "level" - FieldKeyTime = "time" -) - func (f FieldMap) resolve(key fieldKey) string { if k, ok := f[key]; ok { return k @@ -41,9 +34,10 @@ type JSONFormatter struct { // As an example: // formatter := &JSONFormatter{ // FieldMap: FieldMap{ - // FieldKeyTime: "@timestamp", + // FieldKeyTime: "@timestamp", // FieldKeyLevel: "@level", - // FieldKeyMsg: "@message", + // FieldKeyMsg: "@message", + // FieldKeyFunc: "@caller", // }, // } FieldMap FieldMap @@ -54,7 +48,7 @@ type JSONFormatter struct { // Format renders a single log entry func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { - data := make(Fields, len(entry.Data)+3) + data := make(Fields, len(entry.Data)+4) for k, v := range entry.Data { switch v := v.(type) { case error: @@ -72,18 +66,25 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { data = newData } - prefixFieldClashes(data, f.FieldMap) + prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) timestampFormat := f.TimestampFormat if timestampFormat == "" { timestampFormat = defaultTimestampFormat } + if entry.err != "" { + data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err + } if !f.DisableTimestamp { data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat) } data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String() + if entry.HasCaller() { + data[f.FieldMap.resolve(FieldKeyFunc)] = entry.Caller.Function + data[f.FieldMap.resolve(FieldKeyFile)] = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) + } var b *bytes.Buffer if entry.Buffer != nil { diff --git a/vendor/github.com/sirupsen/logrus/logger.go b/vendor/github.com/sirupsen/logrus/logger.go index b67bfcbd3c..5ceca0eab4 100644 --- a/vendor/github.com/sirupsen/logrus/logger.go +++ b/vendor/github.com/sirupsen/logrus/logger.go @@ -24,6 +24,10 @@ type Logger struct { // own that implements the `Formatter` interface, see the `README` or included // formatters for examples. Formatter Formatter + + // Flag for whether to log caller info (off by default) + ReportCaller bool + // The logging level the logger should log at. This is typically (and defaults // to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be // logged. @@ -32,8 +36,12 @@ type Logger struct { mu MutexWrap // Reusable empty entry entryPool sync.Pool + // Function to exit the application, defaults to `os.Exit()` + ExitFunc exitFunc } +type exitFunc func(int) + type MutexWrap struct { lock sync.Mutex disabled bool @@ -69,10 +77,12 @@ func (mw *MutexWrap) Disable() { // It's recommended to make this a global instance called `log`. func New() *Logger { return &Logger{ - Out: os.Stderr, - Formatter: new(TextFormatter), - Hooks: make(LevelHooks), - Level: InfoLevel, + Out: os.Stderr, + Formatter: new(TextFormatter), + Hooks: make(LevelHooks), + Level: InfoLevel, + ExitFunc: os.Exit, + ReportCaller: false, } } @@ -121,6 +131,14 @@ func (logger *Logger) WithTime(t time.Time) *Entry { return entry.WithTime(t) } +func (logger *Logger) Tracef(format string, args ...interface{}) { + if logger.IsLevelEnabled(TraceLevel) { + entry := logger.newEntry() + entry.Tracef(format, args...) + logger.releaseEntry(entry) + } +} + func (logger *Logger) Debugf(format string, args ...interface{}) { if logger.IsLevelEnabled(DebugLevel) { entry := logger.newEntry() @@ -173,7 +191,7 @@ func (logger *Logger) Fatalf(format string, args ...interface{}) { entry.Fatalf(format, args...) logger.releaseEntry(entry) } - Exit(1) + logger.Exit(1) } func (logger *Logger) Panicf(format string, args ...interface{}) { @@ -184,6 +202,14 @@ func (logger *Logger) Panicf(format string, args ...interface{}) { } } +func (logger *Logger) Trace(args ...interface{}) { + if logger.IsLevelEnabled(TraceLevel) { + entry := logger.newEntry() + entry.Trace(args...) + logger.releaseEntry(entry) + } +} + func (logger *Logger) Debug(args ...interface{}) { if logger.IsLevelEnabled(DebugLevel) { entry := logger.newEntry() @@ -236,7 +262,7 @@ func (logger *Logger) Fatal(args ...interface{}) { entry.Fatal(args...) logger.releaseEntry(entry) } - Exit(1) + logger.Exit(1) } func (logger *Logger) Panic(args ...interface{}) { @@ -247,6 +273,14 @@ func (logger *Logger) Panic(args ...interface{}) { } } +func (logger *Logger) Traceln(args ...interface{}) { + if logger.IsLevelEnabled(TraceLevel) { + entry := logger.newEntry() + entry.Traceln(args...) + logger.releaseEntry(entry) + } +} + func (logger *Logger) Debugln(args ...interface{}) { if logger.IsLevelEnabled(DebugLevel) { entry := logger.newEntry() @@ -299,7 +333,7 @@ func (logger *Logger) Fatalln(args ...interface{}) { entry.Fatalln(args...) logger.releaseEntry(entry) } - Exit(1) + logger.Exit(1) } func (logger *Logger) Panicln(args ...interface{}) { @@ -310,6 +344,14 @@ func (logger *Logger) Panicln(args ...interface{}) { } } +func (logger *Logger) Exit(code int) { + runHandlers() + if logger.ExitFunc == nil { + logger.ExitFunc = os.Exit + } + logger.ExitFunc(code) +} + //When file is opened with appending mode, it's safe to //write concurrently to a file (within 4k message on Linux). //In these cases user can choose to disable the lock. @@ -357,6 +399,12 @@ func (logger *Logger) SetOutput(output io.Writer) { logger.Out = output } +func (logger *Logger) SetReportCaller(reportCaller bool) { + logger.mu.Lock() + defer logger.mu.Unlock() + logger.ReportCaller = reportCaller +} + // ReplaceHooks replaces the logger hooks and returns the old ones func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks { logger.mu.Lock() diff --git a/vendor/github.com/sirupsen/logrus/logrus.go b/vendor/github.com/sirupsen/logrus/logrus.go index fa0b9dea8a..4ef4518662 100644 --- a/vendor/github.com/sirupsen/logrus/logrus.go +++ b/vendor/github.com/sirupsen/logrus/logrus.go @@ -15,6 +15,8 @@ type Level uint32 // Convert the Level to a string. E.g. PanicLevel becomes "panic". func (level Level) String() string { switch level { + case TraceLevel: + return "trace" case DebugLevel: return "debug" case InfoLevel: @@ -47,12 +49,26 @@ func ParseLevel(lvl string) (Level, error) { return InfoLevel, nil case "debug": return DebugLevel, nil + case "trace": + return TraceLevel, nil } var l Level return l, fmt.Errorf("not a valid logrus Level: %q", lvl) } +// UnmarshalText implements encoding.TextUnmarshaler. +func (level *Level) UnmarshalText(text []byte) error { + l, err := ParseLevel(string(text)) + if err != nil { + return err + } + + *level = Level(l) + + return nil +} + // A constant exposing all logging levels var AllLevels = []Level{ PanicLevel, @@ -61,6 +77,7 @@ var AllLevels = []Level{ WarnLevel, InfoLevel, DebugLevel, + TraceLevel, } // These are the different logging levels. You can set the logging level to log @@ -69,7 +86,7 @@ const ( // PanicLevel level, highest level of severity. Logs and then calls panic with the // message passed to Debug, Info, ... PanicLevel Level = iota - // FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the + // FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the // logging level is set to Panic. FatalLevel // ErrorLevel level. Logs. Used for errors that should definitely be noted. @@ -82,6 +99,8 @@ const ( InfoLevel // DebugLevel level. Usually only enabled when debugging. Very verbose logging. DebugLevel + // TraceLevel level. Designates finer-grained informational events than the Debug. + TraceLevel ) // Won't compile if StdLogger can't be realized by a log.Logger @@ -148,3 +167,12 @@ type FieldLogger interface { // IsFatalEnabled() bool // IsPanicEnabled() bool } + +// Ext1FieldLogger (the first extension to FieldLogger) is superfluous, it is +// here for consistancy. Do not use. Use Logger or Entry instead. +type Ext1FieldLogger interface { + FieldLogger + Tracef(format string, args ...interface{}) + Trace(args ...interface{}) + Traceln(args ...interface{}) +} diff --git a/vendor/github.com/sirupsen/logrus/terminal_appengine.go b/vendor/github.com/sirupsen/logrus/terminal_appengine.go deleted file mode 100644 index 72f679cdbb..0000000000 --- a/vendor/github.com/sirupsen/logrus/terminal_appengine.go +++ /dev/null @@ -1,13 +0,0 @@ -// Based on ssh/terminal: -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build appengine - -package logrus - -import "io" - -func initTerminal(w io.Writer) { -} diff --git a/vendor/github.com/sirupsen/logrus/terminal_bsd.go b/vendor/github.com/sirupsen/logrus/terminal_bsd.go deleted file mode 100644 index 62ca252d06..0000000000 --- a/vendor/github.com/sirupsen/logrus/terminal_bsd.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build darwin freebsd openbsd netbsd dragonfly -// +build !appengine,!js - -package logrus - -import ( - "io" - - "golang.org/x/sys/unix" -) - -const ioctlReadTermios = unix.TIOCGETA - -type Termios unix.Termios - -func initTerminal(w io.Writer) { -} diff --git a/vendor/github.com/sirupsen/logrus/terminal_linux.go b/vendor/github.com/sirupsen/logrus/terminal_linux.go deleted file mode 100644 index 18066f08ab..0000000000 --- a/vendor/github.com/sirupsen/logrus/terminal_linux.go +++ /dev/null @@ -1,21 +0,0 @@ -// Based on ssh/terminal: -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !appengine,!js - -package logrus - -import ( - "io" - - "golang.org/x/sys/unix" -) - -const ioctlReadTermios = unix.TCGETS - -type Termios unix.Termios - -func initTerminal(w io.Writer) { -} diff --git a/vendor/github.com/sirupsen/logrus/terminal_notwindows.go b/vendor/github.com/sirupsen/logrus/terminal_notwindows.go new file mode 100644 index 0000000000..3dbd237203 --- /dev/null +++ b/vendor/github.com/sirupsen/logrus/terminal_notwindows.go @@ -0,0 +1,8 @@ +// +build !windows + +package logrus + +import "io" + +func initTerminal(w io.Writer) { +} diff --git a/vendor/github.com/sirupsen/logrus/text_formatter.go b/vendor/github.com/sirupsen/logrus/text_formatter.go index 67fb686c6b..49ec92f172 100644 --- a/vendor/github.com/sirupsen/logrus/text_formatter.go +++ b/vendor/github.com/sirupsen/logrus/text_formatter.go @@ -107,14 +107,14 @@ func (f *TextFormatter) isColored() bool { // Format renders a single log entry func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { - prefixFieldClashes(entry.Data, f.FieldMap) + prefixFieldClashes(entry.Data, f.FieldMap, entry.HasCaller()) keys := make([]string, 0, len(entry.Data)) for k := range entry.Data { keys = append(keys, k) } - fixedKeys := make([]string, 0, 3+len(entry.Data)) + fixedKeys := make([]string, 0, 4+len(entry.Data)) if !f.DisableTimestamp { fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime)) } @@ -122,6 +122,13 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { if entry.Message != "" { fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg)) } + if entry.err != "" { + fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError)) + } + if entry.HasCaller() { + fixedKeys = append(fixedKeys, + f.FieldMap.resolve(FieldKeyFunc), f.FieldMap.resolve(FieldKeyFile)) + } if !f.DisableSorting { if f.SortingFunc == nil { @@ -157,13 +164,19 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { } else { for _, key := range fixedKeys { var value interface{} - switch key { - case f.FieldMap.resolve(FieldKeyTime): + switch { + case key == f.FieldMap.resolve(FieldKeyTime): value = entry.Time.Format(timestampFormat) - case f.FieldMap.resolve(FieldKeyLevel): + case key == f.FieldMap.resolve(FieldKeyLevel): value = entry.Level.String() - case f.FieldMap.resolve(FieldKeyMsg): + case key == f.FieldMap.resolve(FieldKeyMsg): value = entry.Message + case key == f.FieldMap.resolve(FieldKeyLogrusError): + value = entry.err + case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller(): + value = entry.Caller.Function + case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller(): + value = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) default: value = entry.Data[key] } @@ -178,7 +191,7 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) { var levelColor int switch entry.Level { - case DebugLevel: + case DebugLevel, TraceLevel: levelColor = gray case WarnLevel: levelColor = yellow @@ -197,12 +210,19 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin // the behavior of logrus text_formatter the same as the stdlib log package entry.Message = strings.TrimSuffix(entry.Message, "\n") + caller := "" + + if entry.HasCaller() { + caller = fmt.Sprintf("%s:%d %s()", + entry.Caller.File, entry.Caller.Line, entry.Caller.Function) + } + if f.DisableTimestamp { - fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m %-44s ", levelColor, levelText, entry.Message) + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message) } else if !f.FullTimestamp { - fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), entry.Message) + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message) } else { - fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message) + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message) } for _, k := range keys { v := entry.Data[k] diff --git a/vendor/github.com/sirupsen/logrus/writer.go b/vendor/github.com/sirupsen/logrus/writer.go index 7bdebedc60..9e1f751359 100644 --- a/vendor/github.com/sirupsen/logrus/writer.go +++ b/vendor/github.com/sirupsen/logrus/writer.go @@ -24,6 +24,8 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter { var printFunc func(args ...interface{}) switch level { + case TraceLevel: + printFunc = entry.Trace case DebugLevel: printFunc = entry.Debug case InfoLevel: From c52e808e34425c3c9a968df4cc38c3f310ae3801 Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Thu, 8 Nov 2018 19:25:08 +0100 Subject: [PATCH 17/23] MM-12641 not panic when dont have permission in the plugin folder (#9690) * MM-12641 not panic when dont have permission in the plugin folder * udpate return statment --- model/manifest.go | 17 +++++++---------- model/manifest_test.go | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/model/manifest.go b/model/manifest.go index 7cd14bfcbf..7a0f121d0a 100644 --- a/model/manifest.go +++ b/model/manifest.go @@ -273,25 +273,23 @@ func FindManifest(dir string) (manifest *Manifest, path string, err error) { f, ferr := os.Open(path) if ferr != nil { if !os.IsNotExist(ferr) { - err = ferr - return + return nil, "", ferr } continue } b, ioerr := ioutil.ReadAll(f) f.Close() if ioerr != nil { - err = ioerr - return + return nil, path, ioerr } var parsed Manifest err = yaml.Unmarshal(b, &parsed) if err != nil { - return + return nil, path, err } manifest = &parsed manifest.Id = strings.ToLower(manifest.Id) - return + return manifest, path, nil } path = filepath.Join(dir, "plugin.json") @@ -300,16 +298,15 @@ func FindManifest(dir string) (manifest *Manifest, path string, err error) { if os.IsNotExist(ferr) { path = "" } - err = ferr - return + return nil, path, ferr } defer f.Close() var parsed Manifest err = json.NewDecoder(f).Decode(&parsed) if err != nil { - return + return nil, path, err } manifest = &parsed manifest.Id = strings.ToLower(manifest.Id) - return + return manifest, path, nil } diff --git a/model/manifest_test.go b/model/manifest_test.go index 0f31e6272b..5978746b26 100644 --- a/model/manifest_test.go +++ b/model/manifest_test.go @@ -185,6 +185,27 @@ func TestFindManifest_FileErrors(t *testing.T) { } } +func TestFindManifest_FolderPermission(t *testing.T) { + for _, tc := range []string{"plugin.yaml", "plugin.json"} { + dir, err := ioutil.TempDir("", "mm-plugin-test") + defer os.RemoveAll(dir) + + path := filepath.Join(dir, tc) + require.NoError(t, os.Mkdir(path, 0700)) + + //User does not have permission in the plugin folder + err = os.Chmod(dir, 0066) + require.NoError(t, err) + + m, mpath, err := FindManifest(dir) + assert.Nil(t, m) + assert.Equal(t, "", mpath) + assert.Error(t, err, tc) + assert.False(t, os.IsNotExist(err), tc) + + } +} + func TestManifestJson(t *testing.T) { manifest := &Manifest{ Id: "theid", From 0c5f60f89be6eb07964f58b0d0be77d35b441a93 Mon Sep 17 00:00:00 2001 From: Harshil Sharma Date: Fri, 9 Nov 2018 02:18:14 +0530 Subject: [PATCH 18/23] #146 Terms of Service Phase 2 (#9731) * #132 added UserTermsOfService model * #132 added UserTermsOfService model * #132 added logic to save user TOS data in a new table * #132 Added logic to save and delete user TOS. Updated user TOS action logic * #132 updated store mocks * #132 added tests * #132 removed cache from UserTermsOfService SQL store * #132 fixed styling and license check * #132 added message translations in en.json * #132 fixed save user TOS logic to work second time as well * #132 removed User.AcceptedTermsOfService colum and migrated accepted TOS data into new table * #132 fixed formatting * #132 fixed formatting * #146 added field 'mandatory' to terms of service * #146 updated tests * #146 added getLatestTermsOfService API * #146 Added tests * #146 fixed styling * #146 removed code for managing mandatory/optional TOS * #146 Added TOS re-acceptance period config * #146 fixed styling * #146 removed some code left for debugging * #146 added TOS re-acceptance period in config * #146 fixed a json name from service_terms to terms_of_service * #146 Minor refactoring and added TOS re-acceptance period to diagnistics * Fixed style * Updated upgraded script to keep app backward compatible --- api4/terms_of_service.go | 4 +- api4/user.go | 17 +++- api4/user_test.go | 28 +++++- app/diagnostics.go | 15 ++-- app/user.go | 19 ---- app/user_terms_of_service.go | 33 +++++++ app/user_terms_of_service_test.go | 34 +++++++ app/user_test.go | 40 --------- i18n/en.json | 28 ++++++ model/client4.go | 19 +++- model/config.go | 20 +++-- model/terms_of_service.go | 3 +- model/user.go | 53 ++++++----- model/user_terms_of_Service_test.go | 46 ++++++++++ model/user_terms_of_service.go | 61 +++++++++++++ store/layered_store.go | 4 + store/sqlstore/store.go | 1 + store/sqlstore/supplier.go | 7 ++ store/sqlstore/terms_of_service_store.go | 4 +- store/sqlstore/upgrade.go | 8 +- store/sqlstore/user_store.go | 1 - store/sqlstore/user_terms_of_service.go | 89 +++++++++++++++++++ .../user_terms_of_service_store_test.go | 10 +++ store/store.go | 7 ++ .../mocks/LayeredStoreDatabaseLayer.go | 16 ++++ store/storetest/mocks/SqlStore.go | 16 ++++ store/storetest/mocks/Store.go | 16 ++++ .../mocks/UserTermsOfServiceStore.go | 62 +++++++++++++ store/storetest/store.go | 54 +++++------ store/storetest/user_terms_of_service.go | 81 +++++++++++++++++ utils/config.go | 1 + 31 files changed, 653 insertions(+), 144 deletions(-) create mode 100644 app/user_terms_of_service.go create mode 100644 app/user_terms_of_service_test.go create mode 100644 model/user_terms_of_Service_test.go create mode 100644 model/user_terms_of_service.go create mode 100644 store/sqlstore/user_terms_of_service.go create mode 100644 store/sqlstore/user_terms_of_service_store_test.go create mode 100644 store/storetest/mocks/UserTermsOfServiceStore.go create mode 100644 store/storetest/user_terms_of_service.go diff --git a/api4/terms_of_service.go b/api4/terms_of_service.go index 8272d3c86e..68d95f8361 100644 --- a/api4/terms_of_service.go +++ b/api4/terms_of_service.go @@ -11,11 +11,11 @@ import ( ) func (api *API) InitTermsOfService() { - api.BaseRoutes.TermsOfService.Handle("", api.ApiSessionRequired(getTermsOfService)).Methods("GET") + api.BaseRoutes.TermsOfService.Handle("", api.ApiSessionRequired(getLatestTermsOfService)).Methods("GET") api.BaseRoutes.TermsOfService.Handle("", api.ApiSessionRequired(createTermsOfService)).Methods("POST") } -func getTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) { +func getLatestTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) { termsOfService, err := c.App.GetLatestTermsOfService() if err != nil { c.Err = err diff --git a/api4/user.go b/api4/user.go index fe9d331c33..4b76c59514 100644 --- a/api4/user.go +++ b/api4/user.go @@ -40,7 +40,8 @@ func (api *API) InitUser() { api.BaseRoutes.Users.Handle("/password/reset/send", api.ApiHandler(sendPasswordReset)).Methods("POST") api.BaseRoutes.Users.Handle("/email/verify", api.ApiHandler(verifyUserEmail)).Methods("POST") api.BaseRoutes.Users.Handle("/email/verify/send", api.ApiHandler(sendVerificationEmail)).Methods("POST") - api.BaseRoutes.User.Handle("/terms_of_service", api.ApiSessionRequired(registerTermsOfServiceAction)).Methods("POST") + api.BaseRoutes.User.Handle("/terms_of_service", api.ApiSessionRequired(saveUserTermsOfService)).Methods("POST") + api.BaseRoutes.User.Handle("/terms_of_service", api.ApiSessionRequired(getUserTermsOfService)).Methods("GET") api.BaseRoutes.User.Handle("/auth", api.ApiSessionRequiredTrustRequester(updateUserAuth)).Methods("PUT") @@ -1626,7 +1627,7 @@ func enableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } -func registerTermsOfServiceAction(c *Context, w http.ResponseWriter, r *http.Request) { +func saveUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) { props := model.StringInterfaceFromJson(r.Body) userId := c.Session.UserId @@ -1638,7 +1639,7 @@ func registerTermsOfServiceAction(c *Context, w http.ResponseWriter, r *http.Req return } - if err := c.App.RecordUserTermsOfServiceAction(userId, termsOfServiceId, accepted); err != nil { + if err := c.App.SaveUserTermsOfService(userId, termsOfServiceId, accepted); err != nil { c.Err = err return } @@ -1646,3 +1647,13 @@ func registerTermsOfServiceAction(c *Context, w http.ResponseWriter, r *http.Req c.LogAudit("TermsOfServiceId=" + termsOfServiceId + ", accepted=" + strconv.FormatBool(accepted)) ReturnStatusOK(w) } + +func getUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) { + userId := c.Session.UserId + if result, err := c.App.GetUserTermsOfService(userId); err != nil { + c.Err = err + return + } else { + w.Write([]byte(result.ToJson())) + } +} diff --git a/api4/user_test.go b/api4/user_test.go index f7970128f5..bb5c782fd5 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -3086,10 +3086,34 @@ func TestRegisterTermsOfServiceAction(t *testing.T) { CheckNoError(t, resp) assert.True(t, *success) - user, err := th.App.GetUser(th.BasicUser.Id) + _, err = th.App.GetUser(th.BasicUser.Id) + if err != nil { + t.Fatal(err) + } +} + + +func TestGetUserTermsOfService(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + Client := th.Client + + _, resp := Client.GetUserTermsOfService(th.BasicUser.Id, "") + CheckErrorMessage(t, resp, "store.sql_user_terms_of_service.get_by_user.no_rows.app_error") + + termsOfService, err := th.App.CreateTermsOfService("terms of service", th.BasicUser.Id) if err != nil { t.Fatal(err) } - assert.Equal(t, user.AcceptedTermsOfServiceId, termsOfService.Id) + success, resp := Client.RegisteTermsOfServiceAction(th.BasicUser.Id, termsOfService.Id, true) + CheckNoError(t, resp) + assert.True(t, *success) + + userTermsOfService, resp := Client.GetUserTermsOfService(th.BasicUser.Id, "") + CheckNoError(t, resp) + + assert.Equal(t, th.BasicUser.Id, userTermsOfService.UserId) + assert.Equal(t, termsOfService.Id, userTermsOfService.TermsOfServiceId) + assert.NotEmpty(t, userTermsOfService.CreateAt) } diff --git a/app/diagnostics.go b/app/diagnostics.go index fdbf9cab7c..c706ce4bf2 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -408,13 +408,14 @@ func (a *App) trackConfig() { }) 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), - "isdefault_help_link": isDefault(*cfg.SupportSettings.HelpLink, model.SUPPORT_SETTINGS_DEFAULT_HELP_LINK), - "isdefault_report_a_problem_link": isDefault(*cfg.SupportSettings.ReportAProblemLink, model.SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK), - "isdefault_support_email": isDefault(*cfg.SupportSettings.SupportEmail, model.SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL), - "custom_terms_of_service_enabled": *cfg.SupportSettings.CustomTermsOfServiceEnabled, + "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), + "isdefault_help_link": isDefault(*cfg.SupportSettings.HelpLink, model.SUPPORT_SETTINGS_DEFAULT_HELP_LINK), + "isdefault_report_a_problem_link": isDefault(*cfg.SupportSettings.ReportAProblemLink, model.SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK), + "isdefault_support_email": isDefault(*cfg.SupportSettings.SupportEmail, model.SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL), + "custom_terms_of_service_enabled": *cfg.SupportSettings.CustomTermsOfServiceEnabled, + "custom_terms_of_service_re_acceptance_period": *cfg.SupportSettings.CustomTermsOfServiceReAcceptancePeriod, }) a.SendDiagnostic(TRACK_CONFIG_LDAP, map[string]interface{}{ diff --git a/app/user.go b/app/user.go index 92269e618f..05d0fe40f5 100644 --- a/app/user.go +++ b/app/user.go @@ -1622,22 +1622,3 @@ func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provide return nil } - -func (a *App) RecordUserTermsOfServiceAction(userId, termsOfServiceId string, accepted bool) *model.AppError { - user, err := a.GetUser(userId) - if err != nil { - return err - } - - if accepted { - user.AcceptedTermsOfServiceId = termsOfServiceId - } else { - user.AcceptedTermsOfServiceId = "" - } - _, err = a.UpdateUser(user, false) - if err != nil { - return err - } - - return nil -} diff --git a/app/user_terms_of_service.go b/app/user_terms_of_service.go new file mode 100644 index 0000000000..11d50c002c --- /dev/null +++ b/app/user_terms_of_service.go @@ -0,0 +1,33 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import "github.com/mattermost/mattermost-server/model" + +func (a *App) GetUserTermsOfService(userId string) (*model.UserTermsOfService, *model.AppError) { + if result := <-a.Srv.Store.UserTermsOfService().GetByUser(userId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.UserTermsOfService), nil + } +} + +func (a *App) SaveUserTermsOfService(userId, termsOfServiceId string, accepted bool) *model.AppError { + if accepted { + userTermsOfService := &model.UserTermsOfService{ + UserId: userId, + TermsOfServiceId: termsOfServiceId, + } + + if result := <-a.Srv.Store.UserTermsOfService().Save(userTermsOfService); result.Err != nil { + return result.Err + } + } else { + if result := <-a.Srv.Store.UserTermsOfService().Delete(userId, termsOfServiceId); result.Err != nil { + return result.Err + } + } + + return nil +} diff --git a/app/user_terms_of_service_test.go b/app/user_terms_of_service_test.go new file mode 100644 index 0000000000..4beb508258 --- /dev/null +++ b/app/user_terms_of_service_test.go @@ -0,0 +1,34 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUserTermsOfService(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + userTermsOfService, err := th.App.GetUserTermsOfService(th.BasicUser.Id) + checkError(t, err) + assert.Nil(t, userTermsOfService) + assert.Equal(t, "store.sql_user_terms_of_service.get_by_user.no_rows.app_error", err.Id) + + termsOfService, err := th.App.CreateTermsOfService("terms of service", th.BasicUser.Id) + checkNoError(t, err) + + err = th.App.SaveUserTermsOfService(th.BasicUser.Id, termsOfService.Id, true) + checkNoError(t, err) + + userTermsOfService, err = th.App.GetUserTermsOfService(th.BasicUser.Id) + checkNoError(t, err) + assert.NotNil(t, userTermsOfService) + assert.NotEmpty(t, userTermsOfService) + + assert.Equal(t, th.BasicUser.Id, userTermsOfService.UserId) + assert.Equal(t, termsOfService.Id, userTermsOfService.TermsOfServiceId) + assert.NotEmpty(t, userTermsOfService.CreateAt) +} diff --git a/app/user_test.go b/app/user_test.go index 7cd3697497..2aeed6fc84 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -544,43 +544,3 @@ func TestPermanentDeleteUser(t *testing.T) { t.Fatal("GetFileInfo after DeleteUser is nil") } } - -func TestRecordUserTermsOfServiceAction(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() - - user := &model.User{ - Email: strings.ToLower(model.NewId()) + "success+test@example.com", - Nickname: "Luke Skywalker", // trying to bring balance to the "Force", one test user at a time - Username: "luke" + model.NewId(), - Password: "passwd1", - AuthService: "", - } - user, err := th.App.CreateUser(user) - if err != nil { - t.Fatalf("failed to create user: %v", err) - } - - defer th.App.PermanentDeleteUser(user) - - termsOfService, err := th.App.CreateTermsOfService("text", user.Id) - if err != nil { - t.Fatalf("failed to create terms of service: %v", err) - } - - err = th.App.RecordUserTermsOfServiceAction(user.Id, termsOfService.Id, true) - if err != nil { - t.Fatalf("failed to record user action: %v", err) - } - - nuser, err := th.App.GetUser(user.Id) - assert.Equal(t, termsOfService.Id, nuser.AcceptedTermsOfServiceId) - - err = th.App.RecordUserTermsOfServiceAction(user.Id, termsOfService.Id, false) - if err != nil { - t.Fatalf("failed to record user action: %v", err) - } - - nuser, err = th.App.GetUser(user.Id) - assert.Empty(t, nuser.AcceptedTermsOfServiceId) -} diff --git a/i18n/en.json b/i18n/en.json index b98d13b471..797141af8a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4794,6 +4794,18 @@ "id": "model.terms_of_service.is_valid.text.app_error", "translation": "Custom terms of service text is too long. Maximum {{.MaxLength}} characters allowed." }, + { + "id": "model.user_terms_of_service.is_valid.user_id.app_error", + "translation": "Missing required user terms of service property: user_id." + }, + { + "id": "model.user_terms_of_service.is_valid.service_terms_id.app_error", + "translation": "Missing required user terms of service property: service_terms_id." + }, + { + "id": "model.user_terms_of_service.is_valid.create_at.app_error", + "translation": "Missing required user terms of service property: create_at." + }, { "id": "oauth.gitlab.tos.error", "translation": "GitLab's Terms of Service have updated. Please go to gitlab.com to accept them and then try logging into Mattermost again." @@ -6454,6 +6466,22 @@ "id": "store.sql_terms_of_service_store.get.no_rows.app_error", "translation": "No terms of service found." }, + { + "id": "store.sql_user_terms_of_service.get_by_user.no_rows.app_error", + "translation": "No user terms of service found." + }, + { + "id": "store.sql_user_terms_of_service.get_by_user.app_error", + "translation": "Unable to fetch user terms of service." + }, + { + "id": "store.sql_user_terms_of_service.save.app_error", + "translation": "Unable to save user terms of service." + }, + { + "id": "store.sql_user_terms_of_service.delete.app_error", + "translation": "Unable to delete user terms of service." + }, { "id": "system.message.name", "translation": "System" diff --git a/model/client4.go b/model/client4.go index 484694416b..92a0f0565d 100644 --- a/model/client4.go +++ b/model/client4.go @@ -401,7 +401,7 @@ func (c *Client4) GetRedirectLocationRoute() string { return fmt.Sprintf("/redirect_location") } -func (c *Client4) GetRegisterTermsOfServiceRoute(userId string) string { +func (c *Client4) GetUserTermsOfServiceRoute(userId string) string { return c.GetUserRoute(userId) + "/terms_of_service" } @@ -3849,7 +3849,7 @@ func (c *Client4) GetRedirectLocation(urlParam, etag string) (string, *Response) } func (c *Client4) RegisteTermsOfServiceAction(userId, termsOfServiceId string, accepted bool) (*bool, *Response) { - url := c.GetRegisterTermsOfServiceRoute(userId) + url := c.GetUserTermsOfServiceRoute(userId) data := map[string]interface{}{"termsOfServiceId": termsOfServiceId, "accepted": accepted} if r, err := c.DoApiPost(url, StringInterfaceToJson(data)); err != nil { @@ -3871,11 +3871,22 @@ func (c *Client4) GetTermsOfService(etag string) (*TermsOfService, *Response) { } } +func (c *Client4) GetUserTermsOfService(userId, etag string) (*UserTermsOfService, *Response) { + url := c.GetUserTermsOfServiceRoute(userId) + + if r, err := c.DoApiGet(url, etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return UserTermsOfServiceFromJson(r.Body), BuildResponse(r) + } +} + func (c *Client4) CreateTermsOfService(text, userId string) (*TermsOfService, *Response) { url := c.GetTermsOfServiceRoute() - data := map[string]string{"text": text} - if r, err := c.DoApiPost(url, MapToJson(data)); err != nil { + data := map[string]interface{}{"text": text} + if r, err := c.DoApiPost(url, StringInterfaceToJson(data)); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) diff --git a/model/config.go b/model/config.go index 9f25662c3a..3af08f5974 100644 --- a/model/config.go +++ b/model/config.go @@ -112,6 +112,7 @@ const ( SUPPORT_SETTINGS_DEFAULT_HELP_LINK = "https://about.mattermost.com/default-help/" SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK = "https://about.mattermost.com/default-report-a-problem/" SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL = "feedback@mattermost.com" + SUPPORT_SETTINGS_DEFAULT_RE_ACCEPTANCE_PERIOD = 365 LDAP_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE = "" LDAP_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE = "" @@ -1030,13 +1031,14 @@ type PrivacySettings struct { } type SupportSettings struct { - TermsOfServiceLink *string - PrivacyPolicyLink *string - AboutLink *string - HelpLink *string - ReportAProblemLink *string - SupportEmail *string - CustomTermsOfServiceEnabled *bool + TermsOfServiceLink *string + PrivacyPolicyLink *string + AboutLink *string + HelpLink *string + ReportAProblemLink *string + SupportEmail *string + CustomTermsOfServiceEnabled *bool + CustomTermsOfServiceReAcceptancePeriod *int } func (s *SupportSettings) SetDefaults() { @@ -1087,6 +1089,10 @@ func (s *SupportSettings) SetDefaults() { if s.CustomTermsOfServiceEnabled == nil { s.CustomTermsOfServiceEnabled = NewBool(false) } + + if s.CustomTermsOfServiceReAcceptancePeriod == nil { + s.CustomTermsOfServiceReAcceptancePeriod = NewInt(SUPPORT_SETTINGS_DEFAULT_RE_ACCEPTANCE_PERIOD) + } } type AnnouncementSettings struct { diff --git a/model/terms_of_service.go b/model/terms_of_service.go index c99a785688..e43f89095c 100644 --- a/model/terms_of_service.go +++ b/model/terms_of_service.go @@ -11,7 +11,6 @@ import ( "unicode/utf8" ) -// we only ever need the latest version of terms of service const TERMS_OF_SERVICE_CACHE_SIZE = 1 type TermsOfService struct { @@ -58,7 +57,7 @@ func InvalidTermsOfServiceError(fieldName string, termsOfServiceId string) *AppE if termsOfServiceId != "" { details = "terms_of_service_id=" + termsOfServiceId } - return NewAppError("TermsOfServiceStore.IsValid", id, map[string]interface{}{"MaxLength": POST_MESSAGE_MAX_RUNES_V2}, details, http.StatusBadRequest) + return NewAppError("TermsOfService.IsValid", id, map[string]interface{}{"MaxLength": POST_MESSAGE_MAX_RUNES_V2}, details, http.StatusBadRequest) } func (t *TermsOfService) PreSave() { diff --git a/model/user.go b/model/user.go index 40ccd16610..8fc9a771ce 100644 --- a/model/user.go +++ b/model/user.go @@ -50,33 +50,32 @@ const ( ) type User struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at,omitempty"` - UpdateAt int64 `json:"update_at,omitempty"` - DeleteAt int64 `json:"delete_at"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - AuthData *string `json:"auth_data,omitempty"` - AuthService string `json:"auth_service"` - Email string `json:"email"` - EmailVerified bool `json:"email_verified,omitempty"` - Nickname string `json:"nickname"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Position string `json:"position"` - Roles string `json:"roles"` - AllowMarketing bool `json:"allow_marketing,omitempty"` - Props StringMap `json:"props,omitempty"` - NotifyProps StringMap `json:"notify_props,omitempty"` - LastPasswordUpdate int64 `json:"last_password_update,omitempty"` - LastPictureUpdate int64 `json:"last_picture_update,omitempty"` - FailedAttempts int `json:"failed_attempts,omitempty"` - Locale string `json:"locale"` - Timezone StringMap `json:"timezone"` - MfaActive bool `json:"mfa_active,omitempty"` - MfaSecret string `json:"mfa_secret,omitempty"` - LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` - AcceptedTermsOfServiceId string `json:"accepted_terms_of_service_id,omitempty"` // TODO remove this field when new TOS user action table is created + Id string `json:"id"` + CreateAt int64 `json:"create_at,omitempty"` + UpdateAt int64 `json:"update_at,omitempty"` + DeleteAt int64 `json:"delete_at"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + AuthData *string `json:"auth_data,omitempty"` + AuthService string `json:"auth_service"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified,omitempty"` + Nickname string `json:"nickname"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Position string `json:"position"` + Roles string `json:"roles"` + AllowMarketing bool `json:"allow_marketing,omitempty"` + Props StringMap `json:"props,omitempty"` + NotifyProps StringMap `json:"notify_props,omitempty"` + LastPasswordUpdate int64 `json:"last_password_update,omitempty"` + LastPictureUpdate int64 `json:"last_picture_update,omitempty"` + FailedAttempts int `json:"failed_attempts,omitempty"` + Locale string `json:"locale"` + Timezone StringMap `json:"timezone"` + MfaActive bool `json:"mfa_active,omitempty"` + MfaSecret string `json:"mfa_secret,omitempty"` + LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` } type UserPatch struct { diff --git a/model/user_terms_of_Service_test.go b/model/user_terms_of_Service_test.go new file mode 100644 index 0000000000..f28171b41e --- /dev/null +++ b/model/user_terms_of_Service_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func TestUserTermsOfServiceIsValid(t *testing.T) { + s := UserTermsOfService{} + + if err := s.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + s.UserId = NewId() + if err := s.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + s.TermsOfServiceId = NewId() + if err := s.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + s.CreateAt = GetMillis() + if err := s.IsValid(); err != nil { + t.Fatal("should be valid") + } +} + +func TestUserTermsOfServiceJson(t *testing.T) { + o := UserTermsOfService{ + UserId: NewId(), + TermsOfServiceId: NewId(), + CreateAt: GetMillis(), + } + j := o.ToJson() + ro := UserTermsOfServiceFromJson(strings.NewReader(j)) + + assert.NotNil(t, ro) + assert.Equal(t, o, *ro) +} diff --git a/model/user_terms_of_service.go b/model/user_terms_of_service.go new file mode 100644 index 0000000000..b714f923c4 --- /dev/null +++ b/model/user_terms_of_service.go @@ -0,0 +1,61 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +type UserTermsOfService struct { + UserId string `json:"user_id"` + TermsOfServiceId string `json:"terms_of_service_id"` + CreateAt int64 `json:"create_at"` +} + +func (ut *UserTermsOfService) IsValid() *AppError { + if len(ut.UserId) != 26 { + return InvalidUserTermsOfServiceError("user_id", ut.UserId) + } + + if len(ut.TermsOfServiceId) != 26 { + return InvalidUserTermsOfServiceError("terms_of_service_id", ut.UserId) + } + + if ut.CreateAt == 0 { + return InvalidUserTermsOfServiceError("create_at", ut.UserId) + } + + return nil +} + +func (ut *UserTermsOfService) ToJson() string { + b, _ := json.Marshal(ut) + return string(b) +} + +func (ut *UserTermsOfService) PreSave() { + if ut.UserId == "" { + ut.UserId = NewId() + } + + ut.CreateAt = GetMillis() +} + +func UserTermsOfServiceFromJson(data io.Reader) *UserTermsOfService { + var userTermsOfService *UserTermsOfService + json.NewDecoder(data).Decode(&userTermsOfService) + return userTermsOfService +} + +func InvalidUserTermsOfServiceError(fieldName string, userTermsOfServiceId string) *AppError { + id := fmt.Sprintf("model.user_terms_of_service.is_valid.%s.app_error", fieldName) + details := "" + if userTermsOfServiceId != "" { + details = "user_terms_of_service_user_id=" + userTermsOfServiceId + } + return NewAppError("UserTermsOfService.IsValid", id, nil, details, http.StatusBadRequest) +} diff --git a/store/layered_store.go b/store/layered_store.go index da2880fa5f..f69f55a7eb 100644 --- a/store/layered_store.go +++ b/store/layered_store.go @@ -173,6 +173,10 @@ func (s *LayeredStore) TermsOfService() TermsOfServiceStore { return s.DatabaseLayer.TermsOfService() } +func (s *LayeredStore) UserTermsOfService() UserTermsOfServiceStore { + return s.DatabaseLayer.UserTermsOfService() +} + func (s *LayeredStore) Scheme() SchemeStore { return s.SchemeStore } diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 0408c5feb8..7f633d17de 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -94,4 +94,5 @@ type SqlStore interface { Role() store.RoleStore Scheme() store.SchemeStore TermsOfService() store.TermsOfServiceStore + UserTermsOfService() store.UserTermsOfServiceStore } diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index c0c92aaaf9..68fed70d34 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -93,6 +93,7 @@ type SqlSupplierOldStores struct { role store.RoleStore scheme store.SchemeStore TermsOfService store.TermsOfServiceStore + UserTermsOfService store.UserTermsOfServiceStore } type SqlSupplier struct { @@ -142,6 +143,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter supplier.oldStores.channelMemberHistory = NewSqlChannelMemberHistoryStore(supplier) supplier.oldStores.plugin = NewSqlPluginStore(supplier) supplier.oldStores.TermsOfService = NewSqlTermsOfServiceStore(supplier, metrics) + supplier.oldStores.UserTermsOfService = NewSqlUserTermsOfServiceStore(supplier) initSqlSupplierReactions(supplier) initSqlSupplierRoles(supplier) @@ -178,6 +180,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter supplier.oldStores.userAccessToken.(*SqlUserAccessTokenStore).CreateIndexesIfNotExists() supplier.oldStores.plugin.(*SqlPluginStore).CreateIndexesIfNotExists() supplier.oldStores.TermsOfService.(SqlTermsOfServiceStore).CreateIndexesIfNotExists() + supplier.oldStores.UserTermsOfService.(SqlUserTermsOfServiceStore).CreateIndexesIfNotExists() supplier.oldStores.preference.(*SqlPreferenceStore).DeleteUnusedFeatures() @@ -963,6 +966,10 @@ func (ss *SqlSupplier) TermsOfService() store.TermsOfServiceStore { return ss.oldStores.TermsOfService } +func (ss *SqlSupplier) UserTermsOfService() store.UserTermsOfServiceStore { + return ss.oldStores.UserTermsOfService +} + func (ss *SqlSupplier) Scheme() store.SchemeStore { return ss.oldStores.scheme } diff --git a/store/sqlstore/terms_of_service_store.go b/store/sqlstore/terms_of_service_store.go index 47557ee8ce..33907b297e 100644 --- a/store/sqlstore/terms_of_service_store.go +++ b/store/sqlstore/terms_of_service_store.go @@ -20,7 +20,9 @@ type SqlTermsOfServiceStore struct { var termsOfServiceCache = utils.NewLru(model.TERMS_OF_SERVICE_CACHE_SIZE) -const termsOfServiceCacheName = "TermsOfServiceStore" +const ( + termsOfServiceCacheName = "TermsOfServiceStore" +) func NewSqlTermsOfServiceStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.TermsOfServiceStore { s := SqlTermsOfServiceStore{sqlStore, metrics} diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 26f0ed88db..73830032c6 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -504,7 +504,6 @@ func UpgradeDatabaseToVersion54(sqlStore SqlStore) { time.Sleep(time.Second) os.Exit(EXIT_GENERIC_FAILURE) } - sqlStore.CreateColumnIfNotExists("Users", "AcceptedTermsOfServiceId", "varchar(64)", "varchar(64)", "") saveSchemaVersion(sqlStore, VERSION_5_4_0) } } @@ -516,9 +515,12 @@ func UpgradeDatabaseToVersion55(sqlStore SqlStore) { } func UpgradeDatabaseToVersion56(sqlStore SqlStore) { - // TODO: Uncomment following condition when version 5.5.0 is released + // TODO: Uncomment following condition when version 5.6.0 is released //if shouldPerformUpgrade(sqlStore, VERSION_5_5_0, VERSION_5_6_0) { sqlStore.CreateColumnIfNotExists("PluginKeyValueStore", "ExpireAt", "bigint(20)", "bigint", "0") - // saveSchemaVersion(sqlStore, VERSION_5_5_0) + + // migrating user's accepted terms of service data into the new table + sqlStore.GetMaster().Exec("INSERT INTO UserTermsOfService SELECT Id, AcceptedTermsOfServiceId as TermsOfServiceId, :CreateAt FROM Users WHERE AcceptedTermsOfServiceId != \"\" AND AcceptedTermsOfServiceId IS NOT NULL", map[string]interface{}{"CreateAt": model.GetMillis()}) + //saveSchemaVersion(sqlStore, VERSION_5_6_0) //} } diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 0e70a87d9c..8136d4ac4f 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -82,7 +82,6 @@ func NewSqlUserStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) st table.ColMap("MfaSecret").SetMaxSize(128) table.ColMap("Position").SetMaxSize(128) table.ColMap("Timezone").SetMaxSize(256) - table.ColMap("AcceptedTermsOfServiceId").SetMaxSize(64) } return us diff --git a/store/sqlstore/user_terms_of_service.go b/store/sqlstore/user_terms_of_service.go new file mode 100644 index 0000000000..1385029819 --- /dev/null +++ b/store/sqlstore/user_terms_of_service.go @@ -0,0 +1,89 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sqlstore + +import ( + "database/sql" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" + "net/http" +) + +type SqlUserTermsOfServiceStore struct { + SqlStore +} + +func NewSqlUserTermsOfServiceStore(sqlStore SqlStore) store.UserTermsOfServiceStore { + s := SqlUserTermsOfServiceStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.UserTermsOfService{}, "UserTermsOfService").SetKeys(false, "UserId") + table.ColMap("UserId").SetMaxSize(26) + table.ColMap("TermsOfServiceId").SetMaxSize(26) + } + + return s +} + +func (s SqlUserTermsOfServiceStore) CreateIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_user_terms_of_service_user_id", "UserTermsOfService", "UserId") +} + +func (s SqlUserTermsOfServiceStore) GetByUser(userId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + var userTermsOfService *model.UserTermsOfService + + err := s.GetReplica().SelectOne(&userTermsOfService, "SELECT * FROM UserTermsOfService WHERE UserId = :userId", map[string]interface{}{"userId": userId}) + if err != nil { + if err == sql.ErrNoRows { + result.Err = model.NewAppError("NewSqlUserTermsOfServiceStore.GetByUser", "store.sql_user_terms_of_service.get_by_user.no_rows.app_error", nil, "", http.StatusNotFound) + } else { + result.Err = model.NewAppError("NewSqlUserTermsOfServiceStore.GetByUser", "store.sql_user_terms_of_service.get_by_user.app_error", nil, "", http.StatusInternalServerError) + } + } else { + result.Data = userTermsOfService + } + }) +} + +func (s SqlUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + userTermsOfService.PreSave() + + if result.Err = userTermsOfService.IsValid(); result.Err != nil { + return + } + + if c, err := s.GetMaster().Update(userTermsOfService); err != nil { + result.Err = model.NewAppError( + "SqlUserTermsOfServiceStore.Save", + "store.sql_user_terms_of_service.save.app_error", + nil, + "user_terms_of_service_user_id="+userTermsOfService.UserId+",user_terms_of_service_terms_of_service_id="+userTermsOfService.TermsOfServiceId+",err="+err.Error(), + http.StatusInternalServerError, + ) + } else if c == 0 { + if err := s.GetMaster().Insert(userTermsOfService); err != nil { + result.Err = model.NewAppError( + "SqlUserTermsOfServiceStore.Save", + "store.sql_user_terms_of_service.save.app_error", + nil, + "user_terms_of_service_user_id="+userTermsOfService.UserId+",user_terms_of_service_terms_of_service_id="+userTermsOfService.TermsOfServiceId+",err="+err.Error(), + http.StatusInternalServerError, + ) + } + } + + result.Data = userTermsOfService + }) +} + +func (s SqlUserTermsOfServiceStore) Delete(userId, termsOfServiceId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + if _, err := s.GetMaster().Exec("DELETE FROM UserTermsOfService WHERE UserId = :UserId AND TermsOfServiceId = :TermsOfServiceId", map[string]interface{}{"UserId": userId, "TermsOfServiceId": termsOfServiceId}); err != nil { + result.Err = model.NewAppError("SqlUserTermsOfServiceStore.Delete", "store.sql_user_terms_of_service.delete.app_error", nil, "userId="+userId+", termsOfServiceId="+termsOfServiceId, http.StatusInternalServerError) + return + } + }) +} diff --git a/store/sqlstore/user_terms_of_service_store_test.go b/store/sqlstore/user_terms_of_service_store_test.go new file mode 100644 index 0000000000..ed1dd8f2ac --- /dev/null +++ b/store/sqlstore/user_terms_of_service_store_test.go @@ -0,0 +1,10 @@ +package sqlstore + +import ( + "github.com/mattermost/mattermost-server/store/storetest" + "testing" +) + +func TestUserTermsOfServiceStore(t *testing.T) { + StoreTest(t, storetest.TestUserTermsOfServiceStore) +} diff --git a/store/store.go b/store/store.go index eefaa4649b..6af7de6bad 100644 --- a/store/store.go +++ b/store/store.go @@ -66,6 +66,7 @@ type Store interface { ChannelMemberHistory() ChannelMemberHistoryStore Plugin() PluginStore TermsOfService() TermsOfServiceStore + UserTermsOfService() UserTermsOfServiceStore MarkSystemRanUnitTests() Close() LockToMaster() @@ -527,3 +528,9 @@ type TermsOfServiceStore interface { GetLatest(allowFromCache bool) StoreChannel Get(id string, allowFromCache bool) StoreChannel } + +type UserTermsOfServiceStore interface { + GetByUser(userId string) StoreChannel + Save(userTermsOfService *model.UserTermsOfService) StoreChannel + Delete(userId, termsOfServiceId string) StoreChannel +} diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go index 3b06bbdf5d..0531d7c377 100644 --- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go +++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go @@ -909,6 +909,22 @@ func (_m *LayeredStoreDatabaseLayer) UserAccessToken() store.UserAccessTokenStor return r0 } +// UserTermsOfService provides a mock function with given fields: +func (_m *LayeredStoreDatabaseLayer) UserTermsOfService() store.UserTermsOfServiceStore { + ret := _m.Called() + + var r0 store.UserTermsOfServiceStore + if rf, ok := ret.Get(0).(func() store.UserTermsOfServiceStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.UserTermsOfServiceStore) + } + } + + return r0 +} + // Webhook provides a mock function with given fields: func (_m *LayeredStoreDatabaseLayer) Webhook() store.WebhookStore { ret := _m.Called() diff --git a/store/storetest/mocks/SqlStore.go b/store/storetest/mocks/SqlStore.go index 278ca1a619..6f76c8a030 100644 --- a/store/storetest/mocks/SqlStore.go +++ b/store/storetest/mocks/SqlStore.go @@ -778,6 +778,22 @@ func (_m *SqlStore) UserAccessToken() store.UserAccessTokenStore { return r0 } +// UserTermsOfService provides a mock function with given fields: +func (_m *SqlStore) UserTermsOfService() store.UserTermsOfServiceStore { + ret := _m.Called() + + var r0 store.UserTermsOfServiceStore + if rf, ok := ret.Get(0).(func() store.UserTermsOfServiceStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.UserTermsOfServiceStore) + } + } + + return r0 +} + // Webhook provides a mock function with given fields: func (_m *SqlStore) Webhook() store.WebhookStore { ret := _m.Called() diff --git a/store/storetest/mocks/Store.go b/store/storetest/mocks/Store.go index b55df20971..1f52d98ecc 100644 --- a/store/storetest/mocks/Store.go +++ b/store/storetest/mocks/Store.go @@ -495,6 +495,22 @@ func (_m *Store) UserAccessToken() store.UserAccessTokenStore { return r0 } +// UserTermsOfService provides a mock function with given fields: +func (_m *Store) UserTermsOfService() store.UserTermsOfServiceStore { + ret := _m.Called() + + var r0 store.UserTermsOfServiceStore + if rf, ok := ret.Get(0).(func() store.UserTermsOfServiceStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.UserTermsOfServiceStore) + } + } + + return r0 +} + // Webhook provides a mock function with given fields: func (_m *Store) Webhook() store.WebhookStore { ret := _m.Called() diff --git a/store/storetest/mocks/UserTermsOfServiceStore.go b/store/storetest/mocks/UserTermsOfServiceStore.go new file mode 100644 index 0000000000..f13faecc9f --- /dev/null +++ b/store/storetest/mocks/UserTermsOfServiceStore.go @@ -0,0 +1,62 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +// Regenerate this file using `make store-mocks`. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import model "github.com/mattermost/mattermost-server/model" +import store "github.com/mattermost/mattermost-server/store" + +// UserTermsOfServiceStore is an autogenerated mock type for the UserTermsOfServiceStore type +type UserTermsOfServiceStore struct { + mock.Mock +} + +// Delete provides a mock function with given fields: userId, termsOfServiceId +func (_m *UserTermsOfServiceStore) Delete(userId string, termsOfServiceId string) store.StoreChannel { + ret := _m.Called(userId, termsOfServiceId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok { + r0 = rf(userId, termsOfServiceId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// GetByUser provides a mock function with given fields: userId +func (_m *UserTermsOfServiceStore) GetByUser(userId string) store.StoreChannel { + ret := _m.Called(userId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// Save provides a mock function with given fields: userTermsOfService +func (_m *UserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) store.StoreChannel { + ret := _m.Called(userTermsOfService) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(*model.UserTermsOfService) store.StoreChannel); ok { + r0 = rf(userTermsOfService) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} diff --git a/store/storetest/store.go b/store/storetest/store.go index d6ef4fcd00..15971a53fa 100644 --- a/store/storetest/store.go +++ b/store/storetest/store.go @@ -46,34 +46,36 @@ type Store struct { RoleStore mocks.RoleStore SchemeStore mocks.SchemeStore TermsOfServiceStore mocks.TermsOfServiceStore + UserTermsOfServiceStore mocks.UserTermsOfServiceStore } -func (s *Store) Team() store.TeamStore { return &s.TeamStore } -func (s *Store) Channel() store.ChannelStore { return &s.ChannelStore } -func (s *Store) Post() store.PostStore { return &s.PostStore } -func (s *Store) User() store.UserStore { return &s.UserStore } -func (s *Store) Audit() store.AuditStore { return &s.AuditStore } -func (s *Store) ClusterDiscovery() store.ClusterDiscoveryStore { return &s.ClusterDiscoveryStore } -func (s *Store) Compliance() store.ComplianceStore { return &s.ComplianceStore } -func (s *Store) Session() store.SessionStore { return &s.SessionStore } -func (s *Store) OAuth() store.OAuthStore { return &s.OAuthStore } -func (s *Store) System() store.SystemStore { return &s.SystemStore } -func (s *Store) Webhook() store.WebhookStore { return &s.WebhookStore } -func (s *Store) Command() store.CommandStore { return &s.CommandStore } -func (s *Store) CommandWebhook() store.CommandWebhookStore { return &s.CommandWebhookStore } -func (s *Store) Preference() store.PreferenceStore { return &s.PreferenceStore } -func (s *Store) License() store.LicenseStore { return &s.LicenseStore } -func (s *Store) Token() store.TokenStore { return &s.TokenStore } -func (s *Store) Emoji() store.EmojiStore { return &s.EmojiStore } -func (s *Store) Status() store.StatusStore { return &s.StatusStore } -func (s *Store) FileInfo() store.FileInfoStore { return &s.FileInfoStore } -func (s *Store) Reaction() store.ReactionStore { return &s.ReactionStore } -func (s *Store) Job() store.JobStore { return &s.JobStore } -func (s *Store) UserAccessToken() store.UserAccessTokenStore { return &s.UserAccessTokenStore } -func (s *Store) Plugin() store.PluginStore { return &s.PluginStore } -func (s *Store) Role() store.RoleStore { return &s.RoleStore } -func (s *Store) Scheme() store.SchemeStore { return &s.SchemeStore } -func (s *Store) TermsOfService() store.TermsOfServiceStore { return &s.TermsOfServiceStore } +func (s *Store) Team() store.TeamStore { return &s.TeamStore } +func (s *Store) Channel() store.ChannelStore { return &s.ChannelStore } +func (s *Store) Post() store.PostStore { return &s.PostStore } +func (s *Store) User() store.UserStore { return &s.UserStore } +func (s *Store) Audit() store.AuditStore { return &s.AuditStore } +func (s *Store) ClusterDiscovery() store.ClusterDiscoveryStore { return &s.ClusterDiscoveryStore } +func (s *Store) Compliance() store.ComplianceStore { return &s.ComplianceStore } +func (s *Store) Session() store.SessionStore { return &s.SessionStore } +func (s *Store) OAuth() store.OAuthStore { return &s.OAuthStore } +func (s *Store) System() store.SystemStore { return &s.SystemStore } +func (s *Store) Webhook() store.WebhookStore { return &s.WebhookStore } +func (s *Store) Command() store.CommandStore { return &s.CommandStore } +func (s *Store) CommandWebhook() store.CommandWebhookStore { return &s.CommandWebhookStore } +func (s *Store) Preference() store.PreferenceStore { return &s.PreferenceStore } +func (s *Store) License() store.LicenseStore { return &s.LicenseStore } +func (s *Store) Token() store.TokenStore { return &s.TokenStore } +func (s *Store) Emoji() store.EmojiStore { return &s.EmojiStore } +func (s *Store) Status() store.StatusStore { return &s.StatusStore } +func (s *Store) FileInfo() store.FileInfoStore { return &s.FileInfoStore } +func (s *Store) Reaction() store.ReactionStore { return &s.ReactionStore } +func (s *Store) Job() store.JobStore { return &s.JobStore } +func (s *Store) UserAccessToken() store.UserAccessTokenStore { return &s.UserAccessTokenStore } +func (s *Store) Plugin() store.PluginStore { return &s.PluginStore } +func (s *Store) Role() store.RoleStore { return &s.RoleStore } +func (s *Store) Scheme() store.SchemeStore { return &s.SchemeStore } +func (s *Store) TermsOfService() store.TermsOfServiceStore { return &s.TermsOfServiceStore } +func (s *Store) UserTermsOfService() store.UserTermsOfServiceStore { return &s.UserTermsOfServiceStore } func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore { return &s.ChannelMemberHistoryStore } diff --git a/store/storetest/user_terms_of_service.go b/store/storetest/user_terms_of_service.go new file mode 100644 index 0000000000..0b2d132e78 --- /dev/null +++ b/store/storetest/user_terms_of_service.go @@ -0,0 +1,81 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package storetest + +import ( + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUserTermsOfServiceStore(t *testing.T, ss store.Store) { + t.Run("TestSaveUserTermsOfService", func(t *testing.T) { testSaveUserTermsOfService(t, ss) }) + t.Run("TestGetByUserTermsOfService", func(t *testing.T) { testGetByUserTermsOfService(t, ss) }) + t.Run("TestDeleteUserTermsOfService", func(t *testing.T) { testDeleteUserTermsOfService(t, ss) }) +} + +func testSaveUserTermsOfService(t *testing.T, ss store.Store) { + userTermsOfService := &model.UserTermsOfService{ + UserId: model.NewId(), + TermsOfServiceId: model.NewId(), + } + + r1 := <-ss.UserTermsOfService().Save(userTermsOfService) + if r1.Err != nil { + t.Fatal(r1.Err) + } + + savedUserTermsOfService := r1.Data.(*model.UserTermsOfService) + assert.Equal(t, userTermsOfService.UserId, savedUserTermsOfService.UserId) + assert.Equal(t, userTermsOfService.TermsOfServiceId, savedUserTermsOfService.TermsOfServiceId) + assert.NotEmpty(t, savedUserTermsOfService.CreateAt) +} + +func testGetByUserTermsOfService(t *testing.T, ss store.Store) { + userTermsOfService := &model.UserTermsOfService{ + UserId: model.NewId(), + TermsOfServiceId: model.NewId(), + } + + r1 := <-ss.UserTermsOfService().Save(userTermsOfService) + if r1.Err != nil { + t.Fatal(r1.Err) + } + + r1 = <-ss.UserTermsOfService().GetByUser(userTermsOfService.UserId) + if r1.Err != nil { + t.Fatal(r1.Err) + } + + fetchedUserTermsOfService := r1.Data.(*model.UserTermsOfService) + assert.Equal(t, userTermsOfService.UserId, fetchedUserTermsOfService.UserId) + assert.Equal(t, userTermsOfService.TermsOfServiceId, fetchedUserTermsOfService.TermsOfServiceId) + assert.NotEmpty(t, fetchedUserTermsOfService.CreateAt) +} + +func testDeleteUserTermsOfService(t *testing.T, ss store.Store) { + userTermsOfService := &model.UserTermsOfService{ + UserId: model.NewId(), + TermsOfServiceId: model.NewId(), + } + + r1 := <-ss.UserTermsOfService().Save(userTermsOfService) + if r1.Err != nil { + t.Fatal(r1.Err) + } + + r1 = <-ss.UserTermsOfService().GetByUser(userTermsOfService.UserId) + if r1.Err != nil { + t.Fatal(r1.Err) + } + + r1 = <-ss.UserTermsOfService().Delete(userTermsOfService.UserId, userTermsOfService.TermsOfServiceId) + if r1.Err != nil { + t.Fatal(r1.Err) + } + + r1 = <-ss.UserTermsOfService().GetByUser(userTermsOfService.UserId) + assert.Equal(t, "store.sql_user_terms_of_service.get_by_user.no_rows.app_error", r1.Err.Id) +} diff --git a/utils/config.go b/utils/config.go index 4e14c3bf4e..4becfb3843 100644 --- a/utils/config.go +++ b/utils/config.go @@ -690,6 +690,7 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L if *license.Features.CustomTermsOfService { props["EnableCustomTermsOfService"] = strconv.FormatBool(*c.SupportSettings.CustomTermsOfServiceEnabled) + props["CustomTermsOfServiceReAcceptancePeriod"] = strconv.FormatInt(int64(*c.SupportSettings.CustomTermsOfServiceReAcceptancePeriod), 10) } } From 7a758eae72b3938950111ed8faecbb03ee856cf8 Mon Sep 17 00:00:00 2001 From: Daniel Fiori Date: Fri, 9 Nov 2018 15:34:31 -0500 Subject: [PATCH 19/23] Optimize searching for users on Postgres (#9782) The searches on Postgres using LIKE with prefix matching will only use an index if it has the text_pattern_ops flag (forcing C collation). This drops the old indexes and adds properly optimized ones. --- store/sqlstore/upgrade.go | 8 ++++++++ store/sqlstore/user_store.go | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 73830032c6..88011feb4b 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -521,6 +521,14 @@ func UpgradeDatabaseToVersion56(sqlStore SqlStore) { // migrating user's accepted terms of service data into the new table sqlStore.GetMaster().Exec("INSERT INTO UserTermsOfService SELECT Id, AcceptedTermsOfServiceId as TermsOfServiceId, :CreateAt FROM Users WHERE AcceptedTermsOfServiceId != \"\" AND AcceptedTermsOfServiceId IS NOT NULL", map[string]interface{}{"CreateAt": model.GetMillis()}) + + if sqlStore.DriverName() == model.DATABASE_DRIVER_POSTGRES { + sqlStore.RemoveIndexIfExists("idx_users_email_lower", "lower(Email)") + sqlStore.RemoveIndexIfExists("idx_users_username_lower", "lower(Username)") + sqlStore.RemoveIndexIfExists("idx_users_nickname_lower", "lower(Nickname)") + sqlStore.RemoveIndexIfExists("idx_users_firstname_lower", "lower(FirstName)") + sqlStore.RemoveIndexIfExists("idx_users_lastname_lower", "lower(LastName)") + } //saveSchemaVersion(sqlStore, VERSION_5_6_0) //} } diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 8136d4ac4f..02092d2240 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -94,11 +94,11 @@ func (us SqlUserStore) CreateIndexesIfNotExists() { us.CreateIndexIfNotExists("idx_users_delete_at", "Users", "DeleteAt") if us.DriverName() == model.DATABASE_DRIVER_POSTGRES { - us.CreateIndexIfNotExists("idx_users_email_lower", "Users", "lower(Email)") - us.CreateIndexIfNotExists("idx_users_username_lower", "Users", "lower(Username)") - us.CreateIndexIfNotExists("idx_users_nickname_lower", "Users", "lower(Nickname)") - us.CreateIndexIfNotExists("idx_users_firstname_lower", "Users", "lower(FirstName)") - us.CreateIndexIfNotExists("idx_users_lastname_lower", "Users", "lower(LastName)") + us.CreateIndexIfNotExists("idx_users_email_lower_textpattern", "Users", "lower(Email) text_pattern_ops") + us.CreateIndexIfNotExists("idx_users_username_lower_textpattern", "Users", "lower(Username) text_pattern_ops") + us.CreateIndexIfNotExists("idx_users_nickname_lower_textpattern", "Users", "lower(Nickname) text_pattern_ops") + us.CreateIndexIfNotExists("idx_users_firstname_lower_textpattern", "Users", "lower(FirstName) text_pattern_ops") + us.CreateIndexIfNotExists("idx_users_lastname_lower_textpattern", "Users", "lower(LastName) text_pattern_ops") } us.CreateFullTextIndexIfNotExists("idx_users_all_txt", "Users", strings.Join(USER_SEARCH_TYPE_ALL, ", ")) From fa0aecce1eb845cb3c7f31797ad5554ddda62e8e Mon Sep 17 00:00:00 2001 From: Daniel Fiori Date: Mon, 12 Nov 2018 15:54:12 -0500 Subject: [PATCH 20/23] Ignore "@" sign at the beginning of user searches (#9780) This trims the "@" symbol from the begging of the search term before executing the query. --- store/sqlstore/user_store.go | 2 +- store/storetest/user_store.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 02092d2240..1b46ae8077 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1166,7 +1166,7 @@ func generateSearchQuery(searchQuery string, terms []string, fields []string, pa } } searchTerms = append(searchTerms, fmt.Sprintf("(%s)", strings.Join(searchFields, " OR "))) - parameters[fmt.Sprintf("Term%d", i)] = fmt.Sprintf("%s%%", term) + parameters[fmt.Sprintf("Term%d", i)] = fmt.Sprintf("%s%%", strings.TrimLeft(term, "@")) } searchClause := strings.Join(searchTerms, " AND ") diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index a3c1af5a35..390adf3bca 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -1658,6 +1658,16 @@ func testUserStoreSearch(t *testing.T, ss store.Store) { }, []*model.User{u1}, }, + { + "leading @ should be ignored", + tid, + "@jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, } for _, testCase := range testCases { From 4aca95fff98aa62b14732356a77ebf3b383ea35f Mon Sep 17 00:00:00 2001 From: Tsilavina Razafinirina Date: Wed, 14 Nov 2018 03:25:57 +0300 Subject: [PATCH 21/23] [MM-8404] Channel notification setting for disabling channel mentions (#9777) * Channel notification setting for disabling channel mentions * Updates unit tests (#MM-8404) * Adds constants (#MM-8404) * Refactors if statement and adds unit test (#MM-8404) * Moves ignore_channel_mentions_notify_prop constant to channel model (#MM8484) --- app/channel.go | 4 ++ app/notification.go | 13 +++-- app/notification_test.go | 112 ++++++++++++++++++++++++++++++++++++--- i18n/en.json | 4 ++ model/channel_member.go | 35 ++++++++---- 5 files changed, 148 insertions(+), 20 deletions(-) diff --git a/app/channel.go b/app/channel.go index 14cd74d87b..99265f3408 100644 --- a/app/channel.go +++ b/app/channel.go @@ -663,6 +663,10 @@ func (a *App) UpdateChannelMemberNotifyProps(data map[string]string, channelId s member.NotifyProps[model.PUSH_NOTIFY_PROP] = push } + if ignoreChannelMentions, exists := data[model.IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP]; exists { + member.NotifyProps[model.IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP] = ignoreChannelMentions + } + result := <-a.Srv.Store.Channel().UpdateMember(member) if result.Err != nil { return nil, result.Err diff --git a/app/notification.go b/app/notification.go index 4501b311d1..6a26ec0030 100644 --- a/app/notification.go +++ b/app/notification.go @@ -84,7 +84,7 @@ 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) + keywords := a.GetMentionKeywordsInChannel(profileMap, post.Type != model.POST_HEADER_CHANGE && post.Type != model.POST_PURPOSE_CHANGE, channelMemberNotifyPropsMap) m := GetExplicitMentions(post, keywords) @@ -555,7 +555,7 @@ func GetMentionsEnabledFields(post *model.Post) model.StringArray { // Given a map of user IDs to profiles, returns a list of mention // keywords for all users in the channel. -func (a *App) GetMentionKeywordsInChannel(profiles map[string]*model.User, lookForSpecialMentions bool) map[string][]string { +func (a *App) GetMentionKeywordsInChannel(profiles map[string]*model.User, lookForSpecialMentions bool, channelMemberNotifyPropsMap map[string]model.StringMap) map[string][]string { keywords := make(map[string][]string) for id, profile := range profiles { @@ -577,9 +577,16 @@ func (a *App) GetMentionKeywordsInChannel(profiles map[string]*model.User, lookF keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id) } + ignoreChannelMentions := false + if ignoreChannelMentionsNotifyProp, ok := channelMemberNotifyPropsMap[profile.Id][model.IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP]; ok { + if ignoreChannelMentionsNotifyProp == model.IGNORE_CHANNEL_MENTIONS_ON { + ignoreChannelMentions = true + } + } + // Add @channel and @all to keywords if user has them turned on if lookForSpecialMentions { - if int64(len(profiles)) <= *a.Config().TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" { + if int64(len(profiles)) <= *a.Config().TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" && !ignoreChannelMentions { keywords["@channel"] = append(keywords["@channel"], profile.Id) keywords["@all"] = append(keywords["@all"], profile.Id) diff --git a/app/notification_test.go b/app/notification_test.go index fa7bf7209a..d2247ac685 100644 --- a/app/notification_test.go +++ b/app/notification_test.go @@ -625,8 +625,14 @@ func TestGetMentionKeywords(t *testing.T) { }, } + channelMemberNotifyPropsMap1Off := map[string]model.StringMap{ + user1.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + } + profiles := map[string]*model.User{user1.Id: user1} - mentions := th.App.GetMentionKeywordsInChannel(profiles, true) + mentions := th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMap1Off) if len(mentions) != 3 { t.Fatal("should've returned three mention keywords") } else if ids, ok := mentions["user"]; !ok || ids[0] != user1.Id { @@ -647,8 +653,14 @@ func TestGetMentionKeywords(t *testing.T) { }, } + channelMemberNotifyPropsMap2Off := map[string]model.StringMap{ + user2.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + } + profiles = map[string]*model.User{user2.Id: user2} - mentions = th.App.GetMentionKeywordsInChannel(profiles, true) + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMap2Off) if len(mentions) != 2 { t.Fatal("should've returned two mention keyword") } else if ids, ok := mentions["First"]; !ok || ids[0] != user2.Id { @@ -665,8 +677,14 @@ func TestGetMentionKeywords(t *testing.T) { }, } + // Channel-wide mentions are not ignored on channel level + channelMemberNotifyPropsMap3Off := map[string]model.StringMap{ + user3.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + } profiles = map[string]*model.User{user3.Id: user3} - mentions = th.App.GetMentionKeywordsInChannel(profiles, true) + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMap3Off) if len(mentions) != 3 { t.Fatal("should've returned three mention keywords") } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id { @@ -675,6 +693,45 @@ func TestGetMentionKeywords(t *testing.T) { t.Fatal("should've returned mention key of @all") } + // Channel member notify props is set to default + channelMemberNotifyPropsMapDefault := map[string]model.StringMap{ + user3.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_DEFAULT, + }, + } + profiles = map[string]*model.User{user3.Id: user3} + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMapDefault) + if len(mentions) != 3 { + t.Fatal("should've returned three mention keywords") + } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id { + t.Fatal("should've returned mention key of @channel") + } else if ids, ok := mentions["@all"]; !ok || ids[0] != user3.Id { + t.Fatal("should've returned mention key of @all") + } + + // Channel member notify props is empty + channelMemberNotifyPropsMapEmpty := map[string]model.StringMap{} + profiles = map[string]*model.User{user3.Id: user3} + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMapEmpty) + if len(mentions) != 3 { + t.Fatal("should've returned three mention keywords") + } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id { + t.Fatal("should've returned mention key of @channel") + } else if ids, ok := mentions["@all"]; !ok || ids[0] != user3.Id { + t.Fatal("should've returned mention key of @all") + } + + // Channel-wide mentions are ignored channel level + channelMemberNotifyPropsMap3On := map[string]model.StringMap{ + user3.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_ON, + }, + } + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMap3On) + if len(mentions) == 0 { + t.Fatal("should've not returned any keywords") + } + // user with all types of mentions enabled user4 := &model.User{ Id: model.NewId(), @@ -687,8 +744,15 @@ func TestGetMentionKeywords(t *testing.T) { }, } + // Channel-wide mentions are not ignored on channel level + channelMemberNotifyPropsMap4Off := map[string]model.StringMap{ + user4.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + } + profiles = map[string]*model.User{user4.Id: user4} - mentions = th.App.GetMentionKeywordsInChannel(profiles, true) + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMap4Off) if len(mentions) != 6 { t.Fatal("should've returned six mention keywords") } else if ids, ok := mentions["user"]; !ok || ids[0] != user4.Id { @@ -705,6 +769,25 @@ func TestGetMentionKeywords(t *testing.T) { t.Fatal("should've returned mention key of @all") } + // Channel-wide mentions are ignored on channel level + channelMemberNotifyPropsMap4On := map[string]model.StringMap{ + user4.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_ON, + }, + } + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMap4On) + if len(mentions) != 4 { + t.Fatal("should've returned four mention keywords") + } else if ids, ok := mentions["user"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of user") + } else if ids, ok := mentions["@user"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of @user") + } else if ids, ok := mentions["mention"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of mention") + } else if ids, ok := mentions["First"]; !ok || ids[0] != user4.Id { + t.Fatal("should've returned mention key of First") + } + dup_count := func(list []string) map[string]int { duplicate_frequency := make(map[string]int) @@ -731,7 +814,22 @@ func TestGetMentionKeywords(t *testing.T) { user3.Id: user3, user4.Id: user4, } - mentions = th.App.GetMentionKeywordsInChannel(profiles, true) + // Channel-wide mentions are not ignored on channel level for all users + channelMemberNotifyPropsMap5Off := map[string]model.StringMap{ + user1.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + user2.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + user3.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + user4.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + } + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMap5Off) if len(mentions) != 6 { t.Fatal("should've returned six mention keywords") } else if ids, ok := mentions["user"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { @@ -750,7 +848,7 @@ func TestGetMentionKeywords(t *testing.T) { // multiple users and more than MaxNotificationsPerChannel th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxNotificationsPerChannel = 3 }) - mentions = th.App.GetMentionKeywordsInChannel(profiles, true) + mentions = th.App.GetMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMap4Off) if len(mentions) != 4 { t.Fatal("should've returned four mention keywords") } else if _, ok := mentions["@channel"]; ok { @@ -765,7 +863,7 @@ func TestGetMentionKeywords(t *testing.T) { profiles = map[string]*model.User{ user1.Id: user1, } - mentions = th.App.GetMentionKeywordsInChannel(profiles, false) + mentions = th.App.GetMentionKeywordsInChannel(profiles, false, channelMemberNotifyPropsMap4Off) if len(mentions) != 3 { t.Fatal("should've returned three mention keywords") } else if ids, ok := mentions["user"]; !ok || len(ids) != 1 || ids[0] != user1.Id { diff --git a/i18n/en.json b/i18n/en.json index 797141af8a..3f8dca0f01 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3750,6 +3750,10 @@ "id": "model.channel_member.is_valid.email_value.app_error", "translation": "Invalid email notification value" }, + { + "id": "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", + "translation": "Invalid ignore channel mentions status" + }, { "id": "model.channel_member.is_valid.notify_level.app_error", "translation": "Invalid notify level" diff --git a/model/channel_member.go b/model/channel_member.go index 941db62f79..753e0eb552 100644 --- a/model/channel_member.go +++ b/model/channel_member.go @@ -11,12 +11,16 @@ import ( ) const ( - CHANNEL_NOTIFY_DEFAULT = "default" - CHANNEL_NOTIFY_ALL = "all" - CHANNEL_NOTIFY_MENTION = "mention" - CHANNEL_NOTIFY_NONE = "none" - CHANNEL_MARK_UNREAD_ALL = "all" - CHANNEL_MARK_UNREAD_MENTION = "mention" + CHANNEL_NOTIFY_DEFAULT = "default" + CHANNEL_NOTIFY_ALL = "all" + CHANNEL_NOTIFY_MENTION = "mention" + CHANNEL_NOTIFY_NONE = "none" + CHANNEL_MARK_UNREAD_ALL = "all" + CHANNEL_MARK_UNREAD_MENTION = "mention" + IGNORE_CHANNEL_MENTIONS_DEFAULT = "default" + IGNORE_CHANNEL_MENTIONS_OFF = "off" + IGNORE_CHANNEL_MENTIONS_ON = "on" + IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP = "ignore_channel_mentions" ) type ChannelUnread struct { @@ -116,6 +120,12 @@ func (o *ChannelMember) IsValid() *AppError { } } + if ignoreChannelMentions, ok := o.NotifyProps[IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP]; ok { + if len(ignoreChannelMentions) > 40 || !IsIgnoreChannelMentionsValid(ignoreChannelMentions) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", nil, "ignore_channel_mentions="+ignoreChannelMentions, http.StatusBadRequest) + } + } + return nil } @@ -146,11 +156,16 @@ func IsSendEmailValid(sendEmail string) bool { return sendEmail == CHANNEL_NOTIFY_DEFAULT || sendEmail == "true" || sendEmail == "false" } +func IsIgnoreChannelMentionsValid(ignoreChannelMentions string) bool { + return ignoreChannelMentions == IGNORE_CHANNEL_MENTIONS_ON || ignoreChannelMentions == IGNORE_CHANNEL_MENTIONS_OFF || ignoreChannelMentions == IGNORE_CHANNEL_MENTIONS_DEFAULT +} + func GetDefaultChannelNotifyProps() StringMap { return StringMap{ - DESKTOP_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, - MARK_UNREAD_NOTIFY_PROP: CHANNEL_MARK_UNREAD_ALL, - PUSH_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, - EMAIL_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, + DESKTOP_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, + MARK_UNREAD_NOTIFY_PROP: CHANNEL_MARK_UNREAD_ALL, + PUSH_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, + EMAIL_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, + IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP: IGNORE_CHANNEL_MENTIONS_DEFAULT, } } From 0e4094a2ade83a2aa2556f9177b7f3dafe0ba7c3 Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Wed, 14 Nov 2018 11:41:20 +0100 Subject: [PATCH 22/23] default text to SAML (#9827) --- config/default.json | 2 +- model/saml.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default.json b/config/default.json index a5f89afefe..6cbca6a0a6 100644 --- a/config/default.json +++ b/config/default.json @@ -330,7 +330,7 @@ "NicknameAttribute": "", "LocaleAttribute": "", "PositionAttribute": "", - "LoginButtonText": "With SAML", + "LoginButtonText": "SAML", "LoginButtonColor": "", "LoginButtonBorderColor": "", "LoginButtonTextColor": "" diff --git a/model/saml.go b/model/saml.go index 528ac45cc7..c184b0350b 100644 --- a/model/saml.go +++ b/model/saml.go @@ -10,7 +10,7 @@ import ( const ( USER_AUTH_SERVICE_SAML = "saml" - USER_AUTH_SERVICE_SAML_TEXT = "With SAML" + USER_AUTH_SERVICE_SAML_TEXT = "SAML" ) type SamlAuthRequest struct { From e0569e766a6a319a6513352cbf452d3a9997b879 Mon Sep 17 00:00:00 2001 From: Chetanya Kandhari Date: Wed, 14 Nov 2018 20:14:40 +0530 Subject: [PATCH 23/23] Update handlers_test.go (#9775) --- web/handlers_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/handlers_test.go b/web/handlers_test.go index 2756e143ea..976ee73bc1 100644 --- a/web/handlers_test.go +++ b/web/handlers_test.go @@ -33,8 +33,8 @@ func TestHandlerServeHTTPErrors(t *testing.T) { mobile bool redirect bool }{ - {"redirect on destkop non-api endpoint", "/login/sso/saml", false, true}, - {"not redirect on destkop api endpoint", "/api/v4/test", false, false}, + {"redirect on desktop non-api endpoint", "/login/sso/saml", false, true}, + {"not redirect on desktop api endpoint", "/api/v4/test", false, false}, {"not redirect on mobile non-api endpoint", "/login/sso/saml", true, false}, {"not redirect on mobile api endpoint", "/api/v4/test", true, false}, }