From 7c7acb476e15ea233a00502beea3e21cd8f4b3fa Mon Sep 17 00:00:00 2001 From: cyrilzhang-mm <112951043+cyrilzhang-mm@users.noreply.github.com> Date: Thu, 8 Dec 2022 15:38:29 -0500 Subject: [PATCH 01/43] [MM-45052] Mark method as deprecated (#21703) --- model/insights.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/model/insights.go b/model/insights.go index 437cdeec37..22d105f867 100644 --- a/model/insights.go +++ b/model/insights.go @@ -248,6 +248,9 @@ func ToDailyPostCountViewModel(dpc []*DurationPostCount, startTime *time.Time, n return viewModel } +// Deprecated: This method doesn't perform error checking. +// Use GetStartOfDayForTimeRange instead. +// // StartOfDayForTimeRange gets the unix start time in milliseconds from the given time range. // Time range can be one of: "today", "7_day", or "28_day". func StartOfDayForTimeRange(timeRange string, location *time.Location) *time.Time { From 671959333eada2a26fd915b123c92eda389b603a Mon Sep 17 00:00:00 2001 From: Christopher Poile Date: Thu, 8 Dec 2022 16:59:09 -0500 Subject: [PATCH 02/43] MM-48476 - Upgrade calls to v0.11.0 (#21834) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f97b3a173a..ae391ac8e8 100644 --- a/Makefile +++ b/Makefile @@ -149,7 +149,7 @@ TEMPLATES_DIR=templates PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2 PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.2.2 PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0 -PLUGIN_PACKAGES += mattermost-plugin-calls-v0.10.0 +PLUGIN_PACKAGES += mattermost-plugin-calls-v0.11.0 PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0 PLUGIN_PACKAGES += mattermost-plugin-confluence-v1.3.0 PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.1 From a0fe5014a9adcf27de2dc90bc11bdd96098403ea Mon Sep 17 00:00:00 2001 From: emmyni <44761757+emmyni@users.noreply.github.com> Date: Thu, 8 Dec 2022 17:07:34 -0500 Subject: [PATCH 03/43] [MM-48649]: Call the SendSubscriptionHistoryEvent function in a GO routine (#21836) --- app/user.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/user.go b/app/user.go index a2f72eddb5..6e12bda34b 100644 --- a/app/user.go +++ b/app/user.go @@ -316,10 +316,13 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m }, plugin.UserHasBeenCreatedID) }) - _, cwsErr := a.SendSubscriptionHistoryEvent(ruser.Id) - if cwsErr != nil { - c.Logger().Error("Failed to create/update the SubscriptionHistoryEvent", mlog.Err(cwsErr)) - } + // Create/Update the subscriptionHistoryEvent + go func() { + _, err := a.SendSubscriptionHistoryEvent(ruser.Id) + if err != nil { + c.Logger().Error("Failed to create/update the SubscriptionHistoryEvent", mlog.Err(err)) + } + }() return ruser, nil } From fc31b1713e510eda2ac881a4f0c39b4c92165575 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Fri, 9 Dec 2022 08:28:22 +0300 Subject: [PATCH 04/43] [MM-48947] app/platform: do not restart metrics on every config save (#21831) --- app/platform/config.go | 6 ------ app/platform/config_test.go | 27 +++++++++++++++++++++++++++ app/platform/service.go | 8 ++++++++ app/platform/service_test.go | 14 +++++--------- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/app/platform/config.go b/app/platform/config.go index fd5bb05f76..e8eb425b96 100644 --- a/app/platform/config.go +++ b/app/platform/config.go @@ -75,12 +75,6 @@ func (ps *PlatformService) SaveConfig(newCfg *model.Config, sendConfigChangeClus return nil, nil, model.NewAppError("saveConfig", "app.save_config.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - if ps.startMetrics && *ps.Config().MetricsSettings.Enable { - ps.RestartMetrics() - } else { - ps.ShutdownMetrics() - } - if ps.clusterIFace != nil { err := ps.clusterIFace.ConfigChanged(ps.configStore.RemoveEnvironmentOverrides(oldCfg), ps.configStore.RemoveEnvironmentOverrides(newCfg), sendConfigChangeClusterMessage) diff --git a/app/platform/config_test.go b/app/platform/config_test.go index af99b47d89..8268b32929 100644 --- a/app/platform/config_test.go +++ b/app/platform/config_test.go @@ -70,4 +70,31 @@ func TestConfigSave(t *testing.T) { updatedCfg := th.Service.Config() assert.Equal(t, "http://newhost.me", *updatedCfg.ServiceSettings.SiteURL) }) + + t.Run("do not restart the metrics server on a different type of config change", func(t *testing.T) { + th := Setup(t, StartMetrics()) + defer th.TearDown() + + metricsMock := &mocks.MetricsInterface{} + metricsMock.On("IncrementWebsocketEvent", mock.AnythingOfType("string")).Return() + metricsMock.On("IncrementWebSocketBroadcastBufferSize", mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return() + metricsMock.On("DecrementWebSocketBroadcastBufferSize", mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return() + metricsMock.On("Register").Return() + th.Service.metricsIFace = metricsMock + + // Change a random config setting + cfg := th.Service.Config().Clone() + cfg.ThemeSettings.EnableThemeSelection = model.NewBool(!*cfg.ThemeSettings.EnableThemeSelection) + th.Service.SaveConfig(cfg, false) + metricsMock.AssertNumberOfCalls(t, "Register", 0) + + // Disable metrics + cfg.MetricsSettings.Enable = model.NewBool(false) + th.Service.SaveConfig(cfg, false) + + // Change the metrics setting + cfg.MetricsSettings.Enable = model.NewBool(true) + th.Service.SaveConfig(cfg, false) + metricsMock.AssertNumberOfCalls(t, "Register", 1) + }) } diff --git a/app/platform/service.go b/app/platform/service.go index b2f02c2c77..857e14e3c8 100644 --- a/app/platform/service.go +++ b/app/platform/service.go @@ -268,6 +268,14 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) { if mErr := ps.resetMetrics(); mErr != nil { return nil, mErr } + + ps.configStore.AddListener(func(oldCfg, newCfg *model.Config) { + if *oldCfg.MetricsSettings.Enable != *newCfg.MetricsSettings.Enable || *oldCfg.MetricsSettings.ListenAddress != *newCfg.MetricsSettings.ListenAddress { + if mErr := ps.resetMetrics(); mErr != nil { + mlog.Warn("Failed to reset metrics", mlog.Err(mErr)) + } + } + }) } // Step 9: Init AsymmetricSigningKey depends on step 6 (store) diff --git a/app/platform/service_test.go b/app/platform/service_test.go index 11633b2d5b..8654b18685 100644 --- a/app/platform/service_test.go +++ b/app/platform/service_test.go @@ -105,10 +105,9 @@ func TestMetrics(t *testing.T) { // there is no config listener for the metrics // we handle it on config save step - th.Service.UpdateConfig(func(c *model.Config) { - c.MetricsSettings.Enable = model.NewBool(true) - }) - th.Service.SaveConfig(th.Service.Config(), false) + cfg := th.Service.Config().Clone() + cfg.MetricsSettings.Enable = model.NewBool(true) + th.Service.SaveConfig(cfg, false) require.NotNil(t, th.Service.metrics) metricsAddr := strings.Replace(th.Service.metrics.listenAddr, "[::]", "http://localhost", 1) @@ -117,17 +116,14 @@ func TestMetrics(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) - th.Service.UpdateConfig(func(c *model.Config) { - c.MetricsSettings.Enable = model.NewBool(false) - }) - th.Service.SaveConfig(th.Service.Config(), false) + cfg.MetricsSettings.Enable = model.NewBool(false) + th.Service.SaveConfig(cfg, false) _, err = http.Get(metricsAddr) require.Error(t, err) }) t.Run("ensure the metrics server is started with advanced metrics", func(t *testing.T) { - t.Skip("MM-47635") th := Setup(t, StartMetrics()) defer th.TearDown() From 0193bfd7de03a3e50118bf8ffdda419d9d64c53a Mon Sep 17 00:00:00 2001 From: Shivashis Padhi Date: Fri, 9 Dec 2022 11:43:13 +0530 Subject: [PATCH 05/43] [MM-47378] Respond with bad requests for wrong query parameters 'roles' in getUsers (#21569) * Respond with bad requests for wrong query parameters in roles * Revert "Respond with bad requests for wrong query parameters in roles" This reverts commit d8374d94e0b1f61ad445127010f9780475c48d1a. * Add GetUser client function to query with channel_id and roles * Return bad parameters error on invalid roles * Make client function generic, lint fixes * i18n strings addition * Validate 'role', add stricter check for comma separated roles --- api4/user.go | 40 +++++++++++++++++++++++++ api4/user_local.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++ api4/user_test.go | 14 +++++++++ i18n/en.json | 4 +++ model/client4.go | 18 ++++++++++++ 5 files changed, 149 insertions(+) diff --git a/api4/user.go b/api4/user.go index 15af912ab7..e0e32708b3 100644 --- a/api4/user.go +++ b/api4/user.go @@ -693,14 +693,44 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.SetInvalidURLParam("inactive") } + roleNamesAll := []string{} + // MM-47378: validate 'role' related parameters + if role != "" || rolesString != "" || channelRolesString != "" || teamRolesString != "" { + // fetch all role names + rolesAll, err := c.App.GetAllRoles() + if err != nil { + c.Err = model.NewAppError("Api4.getUsers", "api.user.get_users.validation.app_error", nil, "Error fetching roles during validation.", http.StatusBadRequest) + return + } + for _, role := range rolesAll { + roleNamesAll = append(roleNamesAll, role.Name) + } + } roles := []string{} var rolesValid bool + if role != "" { + roles, rolesValid = model.CleanRoleNames([]string{role}) + if !rolesValid { + c.SetInvalidParam("role") + return + } + roleValid := utils.StringInSlice(role, roleNamesAll) + if !roleValid { + c.SetInvalidParam("role") + return + } + } if rolesString != "" { roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ",")) if !rolesValid { c.SetInvalidParam("roles") return } + validRoleNames := utils.StringArrayIntersection(roleNamesAll, roles) + if len(validRoleNames) != len(roles) { + c.SetInvalidParam("roles") + return + } } channelRoles := []string{} if channelRolesString != "" && inChannelId != "" { @@ -709,6 +739,11 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.SetInvalidParam("channelRoles") return } + validRoleNames := utils.StringArrayIntersection(roleNamesAll, channelRoles) + if len(validRoleNames) != len(channelRoles) { + c.SetInvalidParam("channelRoles") + return + } } teamRoles := []string{} if teamRolesString != "" && inTeamId != "" { @@ -717,6 +752,11 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.SetInvalidParam("teamRoles") return } + validRoleNames := utils.StringArrayIntersection(roleNamesAll, teamRoles) + if len(validRoleNames) != len(teamRoles) { + c.SetInvalidParam("teamRoles") + return + } } restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId) diff --git a/api4/user_local.go b/api4/user_local.go index 50a451ea96..79578f2484 100644 --- a/api4/user_local.go +++ b/api4/user_local.go @@ -7,11 +7,13 @@ import ( "encoding/json" "net/http" "strconv" + "strings" "github.com/mattermost/mattermost-server/v6/audit" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/store" + "github.com/mattermost/mattermost-server/v6/utils" ) func (api *API) InitUserLocal() { @@ -56,7 +58,78 @@ func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) { active := r.URL.Query().Get("active") inactive := r.URL.Query().Get("inactive") role := r.URL.Query().Get("role") + rolesString := r.URL.Query().Get("roles") + channelRolesString := r.URL.Query().Get("channel_roles") + teamRolesString := r.URL.Query().Get("team_roles") sort := r.URL.Query().Get("sort") + roleNamesAll := []string{} + // MM-47378: validate 'role' related parameters + if role != "" || rolesString != "" || channelRolesString != "" || teamRolesString != "" { + // fetch all role names + rolesAll, err := c.App.GetAllRoles() + if err != nil { + c.Err = model.NewAppError("Api4.getUsers", "api.user.get_users.validation.app_error", nil, "Error fetching roles during validation.", http.StatusBadRequest) + return + } + for _, role := range rolesAll { + roleNamesAll = append(roleNamesAll, role.Name) + } + } + + var roles []string + var rolesValid bool + + if role != "" { + _, rolesValid = model.CleanRoleNames([]string{role}) + if !rolesValid { + c.SetInvalidParam("role") + return + } + roleValid := utils.StringInSlice(role, roleNamesAll) + if !roleValid { + c.SetInvalidParam("role") + return + } + } + + if rolesString != "" { + roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ",")) + if !rolesValid { + c.SetInvalidParam("roles") + return + } + validRoleNames := utils.StringArrayIntersection(roleNamesAll, roles) + if len(validRoleNames) != len(roles) { + c.SetInvalidParam("roles") + return + } + } + var channelRoles []string + if channelRolesString != "" && inChannelId != "" { + channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ",")) + if !rolesValid { + c.SetInvalidParam("channelRoles") + return + } + validRoleNames := utils.StringArrayIntersection(roleNamesAll, channelRoles) + if len(validRoleNames) != len(channelRoles) { + c.SetInvalidParam("channelRoles") + return + } + } + var teamRoles []string + if teamRolesString != "" && inTeamId != "" { + teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ",")) + if !rolesValid { + c.SetInvalidParam("teamRoles") + return + } + validRoleNames := utils.StringArrayIntersection(roleNamesAll, teamRoles) + if len(validRoleNames) != len(teamRoles) { + c.SetInvalidParam("teamRoles") + return + } + } if notInChannelId != "" && inTeamId == "" { c.SetInvalidURLParam("team_id") diff --git a/api4/user_test.go b/api4/user_test.go index 17195f7d6d..871f2c56e8 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -2410,6 +2410,20 @@ func TestGetUsers(t *testing.T) { // Check default params for page and per_page _, err = client.DoAPIGet("/users", "") require.NoError(t, err) + + // Check role params validity + _, _, err = client.GetUsersWithCustomQueryParameters(0, 5, "in_channel=random_channel_id&channel_roles=random_role_doesnt_exist", "") + require.Error(t, err) + require.Equal(t, err.Error(), ": Invalid or missing channelRoles in request body.") + _, _, err = client.GetUsersWithCustomQueryParameters(0, 5, "in_team=random_channel_id&team_roles=random_role_doesnt_exist", "") + require.Error(t, err) + require.Equal(t, err.Error(), ": Invalid or missing teamRoles in request body.") + _, _, err = client.GetUsersWithCustomQueryParameters(0, 5, "roles=random_role_doesnt_exist%2Csystem_user", "") + require.Error(t, err) + require.Equal(t, err.Error(), ": Invalid or missing roles in request body.") + _, _, err = client.GetUsersWithCustomQueryParameters(0, 5, "role=random_role_doesnt_exist", "") + require.Error(t, err) + require.Equal(t, err.Error(), ": Invalid or missing role in request body.") }) th.Client.Logout() diff --git a/i18n/en.json b/i18n/en.json index 43b65baa82..d1399190f6 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4131,6 +4131,10 @@ "id": "api.user.get_user_by_email.permissions.app_error", "translation": "Unable to get user by email." }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Error fetching roles during validation." + }, { "id": "api.user.invalidate_verify_email_tokens.error", "translation": "Unable to get tokens by type when invalidating email verification tokens" diff --git a/model/client4.go b/model/client4.go index 5347048f17..511bc44a3f 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1060,6 +1060,24 @@ func (c *Client4) GetUsers(page int, perPage int, etag string) ([]*User, *Respon return list, BuildResponse(r), nil } +// GetUsersWithChannelRoles returns a page of users on the system. Page counting starts at 0. +func (c *Client4) GetUsersWithCustomQueryParameters(page int, perPage int, queryParameters, etag string) ([]*User, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&%v", page, perPage, queryParameters) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if err := json.NewDecoder(r.Body).Decode(&list); err != nil { + return nil, nil, NewAppError("GetUsers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + return list, BuildResponse(r), nil +} + // GetUsersInTeam returns a page of users on a team. Page counting starts at 0. func (c *Client4) GetUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response, error) { query := fmt.Sprintf("?in_team=%v&page=%v&per_page=%v", teamId, page, perPage) From 3b043c1f126634d87327f9bc794fb171492bb5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Fri, 9 Dec 2022 11:03:03 +0100 Subject: [PATCH 06/43] Skip "block user by domain but allow bot" test (#21841) See https://mattermost.atlassian.net/browse/MM-48973 for more details. --- app/team_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/team_test.go b/app/team_test.go index 23fd15117b..e281a1feaa 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -102,6 +102,7 @@ func TestAddUserToTeam(t *testing.T) { }) t.Run("block user by domain but allow bot", func(t *testing.T) { + t.Skip("MM-48973") th.BasicTeam.AllowedDomains = "example.com" _, err := th.App.UpdateTeam(th.BasicTeam) require.Nil(t, err, "Should update the team") From 9ab1d8f805c6a7e9b560f3f71d051dc0fb51137e Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Mon, 12 Dec 2022 14:59:47 +0530 Subject: [PATCH 07/43] MM-48984: Add missing timeout while creating a connection (#21847) If a timeout is missing, this goroutine waits indefinitely trying to get a connection. Leading to a goroutine accumulation in a scenario where the DB is somehow not release connections. https://mattermost.atlassian.net/browse/MM-48984 ```release-note NONE ``` Co-authored-by: Mattermod --- app/plugin_db_driver.go | 6 +++++- app/plugin_db_driver_test.go | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 app/plugin_db_driver_test.go diff --git a/app/plugin_db_driver.go b/app/plugin_db_driver.go index 753bd0671c..a29fa7467f 100644 --- a/app/plugin_db_driver.go +++ b/app/plugin_db_driver.go @@ -8,6 +8,7 @@ import ( "database/sql" "database/sql/driver" "sync" + "time" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" @@ -44,7 +45,10 @@ func (d *DriverImpl) Conn(isMaster bool) (string, error) { if !isMaster { dbFunc = d.s.Platform().Store.GetInternalReplicaDB } - conn, err := dbFunc().Conn(context.Background()) + timeout := time.Duration(*d.s.Config().SqlSettings.QueryTimeout) * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + conn, err := dbFunc().Conn(ctx) if err != nil { return "", err } diff --git a/app/plugin_db_driver_test.go b/app/plugin_db_driver_test.go new file mode 100644 index 0000000000..a2c428fb6f --- /dev/null +++ b/app/plugin_db_driver_test.go @@ -0,0 +1,21 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConnCreateTimeout(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + *th.App.Config().SqlSettings.QueryTimeout = 0 + + d := NewDriverImpl(th.Server) + _, err := d.Conn(true) + require.Error(t, err) +} From 242d7a4466f4ed1fd07d87b4f654b338e931b09f Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Mon, 12 Dec 2022 20:35:09 +0530 Subject: [PATCH 08/43] MM-48553: Fix panic in json.MarshalerError (#21846) Although this isn't the root cause for the panic in the sentry crash, this is indeed a bug and will cause a crash in the exact same way. I have looked at other possibilities and I don't see any other way for model.ChannelMembers to panic during json marshaling. Other sentry crashes are there for ths customer and they point to data corruption which indicates there is something funky going on. Nevertheless, this is a valid bug and should be fixed. https://mattermost.atlassian.net/browse/MM-48553 ```release-note NONE ``` --- model/group_syncable.go | 4 +--- model/group_syncable_test.go | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 model/group_syncable_test.go diff --git a/model/group_syncable.go b/model/group_syncable.go index afad357a10..e876a4c55f 100644 --- a/model/group_syncable.go +++ b/model/group_syncable.go @@ -144,9 +144,7 @@ func (syncable *GroupSyncable) MarshalJSON() ([]byte, error) { Alias: (*Alias)(syncable), }) default: - return nil, &json.MarshalerError{ - Err: fmt.Errorf("unknown syncable type: %s", syncable.Type), - } + return nil, fmt.Errorf("unknown syncable type: %s", syncable.Type) } } diff --git a/model/group_syncable_test.go b/model/group_syncable_test.go new file mode 100644 index 0000000000..525ddf02a4 --- /dev/null +++ b/model/group_syncable_test.go @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGroupSyncableMarshal(t *testing.T) { + require.NotPanics(t, func() { + var syncable GroupSyncable + _, err := json.Marshal(&syncable) + require.Error(t, err) + t.Log(err.Error()) + }, "marshaling groupsyncable should not panic") +} From 29f29b1e5e9aa9af1cb5081d6e26c8570f59d0a8 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Mon, 12 Dec 2022 10:22:35 -0500 Subject: [PATCH 09/43] MM-48924 Don't cache remote_entry.js files (#21817) * MM-48924 Don't cache remote_entry.js files * Change caching of remote_entry.js to match root.html --- web/static.go | 6 ++++- web/web_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/web/static.go b/web/static.go index d354962215..a694f747db 100644 --- a/web/static.go +++ b/web/static.go @@ -81,7 +81,11 @@ func staticFilesHandler(handler http.Handler) http.Handler { //wrap our ResponseWriter with our no-cache 404-handler w = ¬FoundNoCacheResponseWriter{ResponseWriter: w} - w.Header().Set("Cache-Control", "max-age=31556926, public") + if path.Base(r.URL.Path) == "remote_entry.js" { + w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public") + } else { + w.Header().Set("Cache-Control", "max-age=31556926, public") + } if strings.HasSuffix(r.URL.Path, "/") { http.NotFound(w, r) diff --git a/web/web_test.go b/web/web_test.go index ae8e322f91..4214649031 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -8,6 +8,8 @@ import ( "net/http" "net/http/httptest" "os" + "os/exec" + "path" "path/filepath" "testing" "time" @@ -359,6 +361,65 @@ func TestStatic(t *testing.T) { } */ +func TestStaticFilesCaching(t *testing.T) { + th := Setup(t).InitPlugins() + defer th.TearDown() + + wd, _ := os.Getwd() + cmd := exec.Command("ls", path.Join(wd, "client", "plugins")) + cmd.Stdout = os.Stdout + cmd.Run() + + fakeMainBundleName := "main.1234ab.js" + fakeRootHTML := ` + + Mattermost + +` + fakeMainBundle := `module.exports = 'main';` + fakeRemoteEntry := `module.exports = 'remote';` + + err := os.WriteFile("./client/root.html", []byte(fakeRootHTML), 0600) + require.NoError(t, err) + err = os.WriteFile("./client/"+fakeMainBundleName, []byte(fakeMainBundle), 0600) + require.NoError(t, err) + err = os.WriteFile("./client/remote_entry.js", []byte(fakeRemoteEntry), 0600) + require.NoError(t, err) + + err = os.MkdirAll("./client/products/boards", 0777) + require.NoError(t, err) + err = os.WriteFile("./client/products/boards/remote_entry.js", []byte(fakeRemoteEntry), 0600) + require.NoError(t, err) + + req, _ := http.NewRequest("GET", "/", nil) + res := httptest.NewRecorder() + th.Web.MainRouter.ServeHTTP(res, req) + require.Equal(t, http.StatusOK, res.Code) + require.Equal(t, fakeRootHTML, res.Body.String()) + require.Equal(t, []string{"no-cache, max-age=31556926, public"}, res.Result().Header[http.CanonicalHeaderKey("Cache-Control")]) + + req, _ = http.NewRequest("GET", "/static/"+fakeMainBundleName, nil) + res = httptest.NewRecorder() + th.Web.MainRouter.ServeHTTP(res, req) + require.Equal(t, http.StatusOK, res.Code) + require.Equal(t, fakeMainBundle, res.Body.String()) + require.Equal(t, []string{"max-age=31556926, public"}, res.Result().Header[http.CanonicalHeaderKey("Cache-Control")]) + + req, _ = http.NewRequest("GET", "/static/remote_entry.js", nil) + res = httptest.NewRecorder() + th.Web.MainRouter.ServeHTTP(res, req) + require.Equal(t, http.StatusOK, res.Code) + require.Equal(t, fakeRemoteEntry, res.Body.String()) + require.Equal(t, []string{"no-cache, max-age=31556926, public"}, res.Result().Header[http.CanonicalHeaderKey("Cache-Control")]) + + req, _ = http.NewRequest("GET", "/static/products/boards/remote_entry.js", nil) + res = httptest.NewRecorder() + th.Web.MainRouter.ServeHTTP(res, req) + require.Equal(t, http.StatusOK, res.Code) + require.Equal(t, fakeRemoteEntry, res.Body.String()) + require.Equal(t, []string{"no-cache, max-age=31556926, public"}, res.Result().Header[http.CanonicalHeaderKey("Cache-Control")]) +} + func TestCheckClientCompatability(t *testing.T) { //Browser Name, UA String, expected result (if the browser should fail the test false and if it should pass the true) type uaTest struct { From 731c81cd108973d5515a61e299b60956c646065f Mon Sep 17 00:00:00 2001 From: Tim Scheuermann Date: Mon, 12 Dec 2022 21:56:45 +0100 Subject: [PATCH 10/43] [MM-46417] Added more logging to the import (#21764) --- app/export_test.go | 4 +- app/import.go | 52 +++++++++++++++++----- app/import_functions.go | 86 ++++++++++++++++++++++++++++++------ app/import_functions_test.go | 54 +++++++++++----------- app/import_test.go | 36 ++++++++------- 5 files changed, 163 insertions(+), 69 deletions(-) diff --git a/app/export_test.go b/app/export_test.go index 066aec3771..7dbaa984b7 100644 --- a/app/export_test.go +++ b/app/export_test.go @@ -185,7 +185,7 @@ func TestExportAllUsers(t *testing.T) { defer th2.TearDown() err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, err) - assert.Equal(t, 0, i) + assert.EqualValues(t, 0, i) users1, err := th1.App.GetUsersFromProfiles(&model.UserGetOptions{ Page: 0, @@ -323,7 +323,7 @@ func TestExportDMChannelToSelf(t *testing.T) { // import the exported channel err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, err) - assert.Equal(t, 0, i) + assert.EqualValues(t, 0, i) channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") require.NoError(t, nErr) diff --git a/app/import.go b/app/import.go index 832524aa99..9510a91493 100644 --- a/app/import.go +++ b/app/import.go @@ -6,7 +6,6 @@ package app import ( "archive/zip" "bufio" - "bytes" "encoding/json" "fmt" "io" @@ -26,6 +25,7 @@ type ReactionImportData = imports.ReactionImportData // part of the app interfac const ( importMultiplePostsThreshold = 1000 maxScanTokenSize = 16 * 1024 * 1024 // Need to set a higher limit than default because some customers cross the limit. See MM-22314 + statusUpdateAfterLines = 8192 ) func stopOnError(c request.CTX, err imports.LineImportWorkerError) bool { @@ -41,7 +41,7 @@ func stopOnError(c request.CTX, err imports.LineImportWorkerError) bool { } } -func processAttachmentPaths(files *[]imports.AttachmentImportData, basePath string, filesMap map[string]*zip.File) error { +func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData, basePath string, filesMap map[string]*zip.File) error { if files == nil { return nil } @@ -61,20 +61,20 @@ func processAttachmentPaths(files *[]imports.AttachmentImportData, basePath stri return nil } -func processAttachments(line *imports.LineImportData, basePath string, filesMap map[string]*zip.File) error { +func processAttachments(c request.CTX, line *imports.LineImportData, basePath string, filesMap map[string]*zip.File) error { var ok bool switch line.Type { case "post", "direct_post": var replies []imports.ReplyImportData if line.Type == "direct_post" { - if err := processAttachmentPaths(line.DirectPost.Attachments, basePath, filesMap); err != nil { + if err := processAttachmentPaths(c, line.DirectPost.Attachments, basePath, filesMap); err != nil { return err } if line.DirectPost.Replies != nil { replies = *line.DirectPost.Replies } } else { - if err := processAttachmentPaths(line.Post.Attachments, basePath, filesMap); err != nil { + if err := processAttachmentPaths(c, line.Post.Attachments, basePath, filesMap); err != nil { return err } if line.Post.Replies != nil { @@ -82,7 +82,7 @@ func processAttachments(line *imports.LineImportData, basePath string, filesMap } } for _, reply := range replies { - if err := processAttachmentPaths(reply.Attachments, basePath, filesMap); err != nil { + if err := processAttachmentPaths(c, reply.Attachments, basePath, filesMap); err != nil { return err } } @@ -112,6 +112,15 @@ func processAttachments(line *imports.LineImportData, basePath string, filesMap } func (a *App) bulkImportWorker(c request.CTX, dryRun bool, wg *sync.WaitGroup, lines <-chan imports.LineImportWorkerData, errors chan<- imports.LineImportWorkerError) { + workerID := model.NewId() + processedLines := uint64(0) + + c.Logger().Info("Started new bulk import worker", mlog.String("bulk_import_worker_id", workerID)) + defer func() { + wg.Done() + c.Logger().Info("Bulk import worker finished", mlog.String("bulk_import_worker_id", workerID), mlog.Uint64("processed_lines", processedLines)) + }() + postLines := []imports.LineImportWorkerData{} directPostLines := []imports.LineImportWorkerData{} for line := range lines { @@ -143,6 +152,11 @@ func (a *App) bulkImportWorker(c request.CTX, dryRun bool, wg *sync.WaitGroup, l errors <- imports.LineImportWorkerError{Error: err, LineNumber: line.LineNumber} } } + + processedLines++ + if processedLines%statusUpdateAfterLines == 0 { + c.Logger().Info("Worker progress", mlog.String("bulk_import_worker_id", workerID), mlog.Uint64("processed_lines", processedLines)) + } } if len(postLines) > 0 { @@ -155,7 +169,6 @@ func (a *App) bulkImportWorker(c request.CTX, dryRun bool, wg *sync.WaitGroup, l errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine} } } - wg.Done() } func (a *App) BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) { @@ -194,15 +207,17 @@ func (a *App) bulkImport(c request.CTX, jsonlReader io.Reader, attachmentsReader } for scanner.Scan() { - decoder := json.NewDecoder(bytes.NewReader(scanner.Bytes())) lineNumber++ + if lineNumber%statusUpdateAfterLines == 0 { + c.Logger().Info("Reader progress", mlog.Int("processed_lines", lineNumber)) + } var line imports.LineImportData - if err := decoder.Decode(&line); err != nil { + if err := json.Unmarshal(scanner.Bytes(), &line); err != nil { return model.NewAppError("BulkImport", "app.import.bulk_import.json_decode.error", nil, "", http.StatusBadRequest).Wrap(err), lineNumber } - if err := processAttachments(&line, importPath, attachedFiles); err != nil { + if err := processAttachments(c, &line, importPath, attachedFiles); err != nil { c.Logger().Warn("Error while processing import attachments. Objects might be broken.", mlog.Err(err)) } @@ -222,6 +237,12 @@ func (a *App) bulkImport(c request.CTX, jsonlReader io.Reader, attachmentsReader if line.Type != lastLineType { // Only clear the worker queue if is not the first data entry if lineNumber != 2 { + c.Logger().Info( + "Finished parsing segment, waiting for workers to finish", + mlog.String("old_segment", lastLineType), + mlog.String("new_segment", line.Type), + ) + // Changing type. Clear out the worker queue before continuing. close(linesChan) wg.Wait() @@ -235,6 +256,13 @@ func (a *App) bulkImport(c request.CTX, jsonlReader io.Reader, attachmentsReader } } + c.Logger().Info( + "Starting workers for new segment", + mlog.String("old_segment", lastLineType), + mlog.String("new_segment", line.Type), + mlog.Int("workers", workers), + ) + // Set up the workers and channel for this type. lastLineType = line.Type linesChan = make(chan imports.LineImportWorkerData, workers) @@ -290,7 +318,7 @@ func (a *App) importLine(c request.CTX, line imports.LineImportData, dryRun bool if line.Scheme == nil { return model.NewAppError("BulkImport", "app.import.import_line.null_scheme.error", nil, "", http.StatusBadRequest) } - return a.importScheme(line.Scheme, dryRun) + return a.importScheme(c, line.Scheme, dryRun) case line.Type == "team": if line.Team == nil { return model.NewAppError("BulkImport", "app.import.import_line.null_team.error", nil, "", http.StatusBadRequest) @@ -315,7 +343,7 @@ func (a *App) importLine(c request.CTX, line imports.LineImportData, dryRun bool if line.Emoji == nil { return model.NewAppError("BulkImport", "app.import.import_line.null_emoji.error", nil, "", http.StatusBadRequest) } - return a.importEmoji(line.Emoji, dryRun) + return a.importEmoji(c, line.Emoji, dryRun) default: return model.NewAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]any{"Type": line.Type}, "", http.StatusBadRequest) } diff --git a/app/import_functions.go b/app/import_functions.go index bf6d073f72..1f28b046d3 100644 --- a/app/import_functions.go +++ b/app/import_functions.go @@ -15,6 +15,7 @@ import ( "path" "strings" + "github.com/mattermost/logr/v2" "github.com/mattermost/mattermost-server/v6/app/imports" "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/app/teams" @@ -25,13 +26,16 @@ import ( "github.com/mattermost/mattermost-server/v6/utils" ) -// // -- Bulk Import Functions -- // These functions import data directly into the database. Security and permission checks are bypassed but validity is // still enforced. -// +func (a *App) importScheme(c request.CTX, data *imports.SchemeImportData, dryRun bool) *model.AppError { + var fields []logr.Field + if data != nil && data.Name != nil { + fields = append(fields, mlog.String("schema_name", *data.Name)) + } + c.Logger().Info("Validating schema", fields...) -func (a *App) importScheme(data *imports.SchemeImportData, dryRun bool) *model.AppError { if err := imports.ValidateSchemeImportData(data); err != nil { return err } @@ -41,6 +45,8 @@ func (a *App) importScheme(data *imports.SchemeImportData, dryRun bool) *model.A return nil } + c.Logger().Info("Importing schema", fields...) + scheme, err := a.GetSchemeByName(*data.Name) if err != nil { scheme = new(model.Scheme) @@ -68,12 +74,12 @@ func (a *App) importScheme(data *imports.SchemeImportData, dryRun bool) *model.A if scheme.Scope == model.SchemeScopeTeam { data.DefaultTeamAdminRole.Name = &scheme.DefaultTeamAdminRole - if err := a.importRole(data.DefaultTeamAdminRole, dryRun, true); err != nil { + if err := a.importRole(c, data.DefaultTeamAdminRole, dryRun, true); err != nil { return err } data.DefaultTeamUserRole.Name = &scheme.DefaultTeamUserRole - if err := a.importRole(data.DefaultTeamUserRole, dryRun, true); err != nil { + if err := a.importRole(c, data.DefaultTeamUserRole, dryRun, true); err != nil { return err } @@ -83,19 +89,19 @@ func (a *App) importScheme(data *imports.SchemeImportData, dryRun bool) *model.A } } data.DefaultTeamGuestRole.Name = &scheme.DefaultTeamGuestRole - if err := a.importRole(data.DefaultTeamGuestRole, dryRun, true); err != nil { + if err := a.importRole(c, data.DefaultTeamGuestRole, dryRun, true); err != nil { return err } } if scheme.Scope == model.SchemeScopeTeam || scheme.Scope == model.SchemeScopeChannel { data.DefaultChannelAdminRole.Name = &scheme.DefaultChannelAdminRole - if err := a.importRole(data.DefaultChannelAdminRole, dryRun, true); err != nil { + if err := a.importRole(c, data.DefaultChannelAdminRole, dryRun, true); err != nil { return err } data.DefaultChannelUserRole.Name = &scheme.DefaultChannelUserRole - if err := a.importRole(data.DefaultChannelUserRole, dryRun, true); err != nil { + if err := a.importRole(c, data.DefaultChannelUserRole, dryRun, true); err != nil { return err } @@ -105,7 +111,7 @@ func (a *App) importScheme(data *imports.SchemeImportData, dryRun bool) *model.A } } data.DefaultChannelGuestRole.Name = &scheme.DefaultChannelGuestRole - if err := a.importRole(data.DefaultChannelGuestRole, dryRun, true); err != nil { + if err := a.importRole(c, data.DefaultChannelGuestRole, dryRun, true); err != nil { return err } } @@ -113,8 +119,15 @@ func (a *App) importScheme(data *imports.SchemeImportData, dryRun bool) *model.A return nil } -func (a *App) importRole(data *imports.RoleImportData, dryRun bool, isSchemeRole bool) *model.AppError { +func (a *App) importRole(c request.CTX, data *imports.RoleImportData, dryRun bool, isSchemeRole bool) *model.AppError { + var fields []logr.Field + if data != nil && data.Name != nil { + fields = append(fields, mlog.String("role_name", *data.Name)) + } + if !isSchemeRole { + c.Logger().Info("Validating role", fields...) + if err := imports.ValidateRoleImportData(data); err != nil { return err } @@ -125,6 +138,8 @@ func (a *App) importRole(data *imports.RoleImportData, dryRun bool, isSchemeRole return nil } + c.Logger().Info("Importing role", fields...) + role, err := a.GetRoleByName(context.Background(), *data.Name) if err != nil { role = new(model.Role) @@ -160,6 +175,12 @@ func (a *App) importRole(data *imports.RoleImportData, dryRun bool, isSchemeRole } func (a *App) importTeam(c request.CTX, data *imports.TeamImportData, dryRun bool) *model.AppError { + var fields []logr.Field + if data != nil && data.Name != nil { + fields = append(fields, mlog.String("team_name", *data.Name)) + } + c.Logger().Info("Validating team", fields...) + if err := imports.ValidateTeamImportData(data); err != nil { return err } @@ -169,6 +190,8 @@ func (a *App) importTeam(c request.CTX, data *imports.TeamImportData, dryRun boo return nil } + c.Logger().Info("Importing team", fields...) + var team *model.Team team, err := a.Srv().Store().Team().GetByName(*data.Name) @@ -228,6 +251,12 @@ func (a *App) importTeam(c request.CTX, data *imports.TeamImportData, dryRun boo } func (a *App) importChannel(c request.CTX, data *imports.ChannelImportData, dryRun bool) *model.AppError { + var fields []logr.Field + if data != nil && data.Name != nil { + fields = append(fields, mlog.String("channel_name", *data.Name)) + } + c.Logger().Info("Validating channel", fields...) + if err := imports.ValidateChannelImportData(data); err != nil { return err } @@ -237,6 +266,8 @@ func (a *App) importChannel(c request.CTX, data *imports.ChannelImportData, dryR return nil } + c.Logger().Info("Importing channel", fields...) + team, err := a.Srv().Store().Team().GetByName(*data.Team) if err != nil { return model.NewAppError("BulkImport", "app.import.import_channel.team_not_found.error", map[string]any{"TeamName": *data.Team}, "", http.StatusBadRequest).Wrap(err) @@ -293,6 +324,12 @@ func (a *App) importChannel(c request.CTX, data *imports.ChannelImportData, dryR } func (a *App) importUser(c request.CTX, data *imports.UserImportData, dryRun bool) *model.AppError { + var fields []logr.Field + if data != nil && data.Username != nil { + fields = append(fields, mlog.String("user_name", *data.Username)) + } + c.Logger().Info("Validating user", fields...) + if err := imports.ValidateUserImportData(data); err != nil { return err } @@ -302,6 +339,8 @@ func (a *App) importUser(c request.CTX, data *imports.UserImportData, dryRun boo return nil } + c.Logger().Info("Importing user", fields...) + // We want to avoid database writes if nothing has changed. hasUserChanged := false hasNotifyPropsChanged := false @@ -1214,6 +1253,8 @@ func (a *App) importAttachment(c request.CTX, data *imports.AttachmentImportData defer zipFile.Close() name = data.Data.Name file = zipFile.(io.Reader) + + c.Logger().Info("Preparing file upload from ZIP", mlog.String("file_name", name), mlog.Uint64("file_size", data.Data.UncompressedSize64)) } else { realFile, err := os.Open(*data.Path) if err != nil { @@ -1222,6 +1263,12 @@ func (a *App) importAttachment(c request.CTX, data *imports.AttachmentImportData defer realFile.Close() name = realFile.Name() file = realFile + + fields := []logr.Field{mlog.String("file_name", name)} + if info, err := realFile.Stat(); err != nil { + fields = append(fields, mlog.Int64("file_size", info.Size())) + } + c.Logger().Info("Preparing file upload from file system", fields...) } timestamp := utils.TimeFromMillis(post.CreateAt) @@ -1241,7 +1288,8 @@ func (a *App) importAttachment(c request.CTX, data *imports.AttachmentImportData if oldFile.Name != path.Base(name) || oldFile.Size != int64(len(fileData)) { continue } - // check md5 + + // check sha1 newHash := sha1.Sum(fileData) oldFileData, err := a.getFileIgnoreCloudLimit(oldFile.Id) if err != nil { @@ -1260,7 +1308,7 @@ func (a *App) importAttachment(c request.CTX, data *imports.AttachmentImportData fileInfo, appErr := a.DoUploadFile(c, timestamp, teamID, post.ChannelId, post.UserId, name, fileData) if appErr != nil { - mlog.Error("Failed to upload file:", mlog.Err(appErr)) + mlog.Error("Failed to upload file", mlog.Err(appErr), mlog.String("file_name", name)) return nil, appErr } @@ -1358,6 +1406,8 @@ func (a *App) importMultiplePostLines(c request.CTX, lines []imports.LineImportW return 0, nil } + c.Logger().Info("Validating post lines", mlog.Int("count", len(lines)), mlog.Int("first_line", lines[0].LineNumber)) + for _, line := range lines { if err := imports.ValidatePostImportData(line.Post, a.MaxPostSize()); err != nil { return line.LineNumber, err @@ -1369,6 +1419,8 @@ func (a *App) importMultiplePostLines(c request.CTX, lines []imports.LineImportW return 0, nil } + c.Logger().Info("Importing post lines", mlog.Int("count", len(lines)), mlog.Int("first_line", lines[0].LineNumber)) + usernames := []string{} teamNames := make([]string, len(lines)) postsData := make([]*imports.PostImportData, len(lines)) @@ -1855,7 +1907,13 @@ func (a *App) importMultipleDirectPostLines(c request.CTX, lines []imports.LineI return 0, nil } -func (a *App) importEmoji(data *imports.EmojiImportData, dryRun bool) *model.AppError { +func (a *App) importEmoji(c request.CTX, data *imports.EmojiImportData, dryRun bool) *model.AppError { + var fields []logr.Field + if data != nil && data.Name != nil { + fields = append(fields, mlog.String("emoji_name", *data.Name)) + } + c.Logger().Info("Validating emoji", fields...) + aerr := imports.ValidateEmojiImportData(data) if aerr != nil { if aerr.Id == "model.emoji.system_emoji_name.app_error" { @@ -1870,6 +1928,8 @@ func (a *App) importEmoji(data *imports.EmojiImportData, dryRun bool) *model.App return nil } + c.Logger().Info("Importing emoji", fields...) + var emoji *model.Emoji emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), *data.Name, true) diff --git a/app/import_functions_test.go b/app/import_functions_test.go index bb2271eeb6..864eef4a7f 100644 --- a/app/import_functions_test.go +++ b/app/import_functions_test.go @@ -65,7 +65,7 @@ func TestImportImportScheme(t *testing.T) { Description: ptrStr("description"), } - err := th.App.importScheme(&data, true) + err := th.App.importScheme(th.Context, &data, true) require.NotNil(t, err, "Should have failed to import.") _, nErr := th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -74,7 +74,7 @@ func TestImportImportScheme(t *testing.T) { // Try importing a valid scheme in dryRun mode. data.DisplayName = ptrStr("display name") - err = th.App.importScheme(&data, true) + err = th.App.importScheme(th.Context, &data, true) require.Nil(t, err, "Should have succeeded.") _, nErr = th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -83,7 +83,7 @@ func TestImportImportScheme(t *testing.T) { // Try importing an invalid scheme. data.DisplayName = nil - err = th.App.importScheme(&data, false) + err = th.App.importScheme(th.Context, &data, false) require.NotNil(t, err, "Should have failed to import.") _, nErr = th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -92,7 +92,7 @@ func TestImportImportScheme(t *testing.T) { // Try importing a valid scheme with all params set. data.DisplayName = ptrStr("display name") - err = th.App.importScheme(&data, false) + err = th.App.importScheme(th.Context, &data, false) require.Nil(t, err, "Should have succeeded.") scheme, nErr := th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -149,7 +149,7 @@ func TestImportImportScheme(t *testing.T) { data.DisplayName = ptrStr("new display name") data.Description = ptrStr("new description") - err = th.App.importScheme(&data, false) + err = th.App.importScheme(th.Context, &data, false) require.Nil(t, err, "Should have succeeded: %v", err) scheme, nErr = th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -205,7 +205,7 @@ func TestImportImportScheme(t *testing.T) { // Try changing the scope of the scheme and reimporting. data.Scope = ptrStr("channel") - err = th.App.importScheme(&data, false) + err = th.App.importScheme(th.Context, &data, false) require.NotNil(t, err, "Should have failed to import.") scheme, nErr = th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -252,7 +252,7 @@ func TestImportImportSchemeWithoutGuestRoles(t *testing.T) { Description: ptrStr("description"), } - err := th.App.importScheme(&data, true) + err := th.App.importScheme(th.Context, &data, true) require.NotNil(t, err, "Should have failed to import.") _, nErr := th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -261,7 +261,7 @@ func TestImportImportSchemeWithoutGuestRoles(t *testing.T) { // Try importing a valid scheme in dryRun mode. data.DisplayName = ptrStr("display name") - err = th.App.importScheme(&data, true) + err = th.App.importScheme(th.Context, &data, true) require.Nil(t, err, "Should have succeeded.") _, nErr = th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -270,7 +270,7 @@ func TestImportImportSchemeWithoutGuestRoles(t *testing.T) { // Try importing an invalid scheme. data.DisplayName = nil - err = th.App.importScheme(&data, false) + err = th.App.importScheme(th.Context, &data, false) require.NotNil(t, err, "Should have failed to import.") _, nErr = th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -279,7 +279,7 @@ func TestImportImportSchemeWithoutGuestRoles(t *testing.T) { // Try importing a valid scheme with all params set. data.DisplayName = ptrStr("display name") - err = th.App.importScheme(&data, false) + err = th.App.importScheme(th.Context, &data, false) require.Nil(t, err, "Should have succeeded.") scheme, nErr := th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -336,7 +336,7 @@ func TestImportImportSchemeWithoutGuestRoles(t *testing.T) { data.DisplayName = ptrStr("new display name") data.Description = ptrStr("new description") - err = th.App.importScheme(&data, false) + err = th.App.importScheme(th.Context, &data, false) require.Nil(t, err, "Should have succeeded: %v", err) scheme, nErr = th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -392,7 +392,7 @@ func TestImportImportSchemeWithoutGuestRoles(t *testing.T) { // Try changing the scope of the scheme and reimporting. data.Scope = ptrStr("channel") - err = th.App.importScheme(&data, false) + err = th.App.importScheme(th.Context, &data, false) require.NotNil(t, err, "Should have failed to import.") scheme, nErr = th.App.Srv().Store().Scheme().GetByName(*data.Name) @@ -414,7 +414,7 @@ func TestImportImportRole(t *testing.T) { Name: &rid1, } - err := th.App.importRole(&data, true, false) + err := th.App.importRole(th.Context, &data, true, false) require.NotNil(t, err, "Should have failed to import.") _, nErr := th.App.Srv().Store().Role().GetByName(context.Background(), rid1) @@ -423,7 +423,7 @@ func TestImportImportRole(t *testing.T) { // Try importing the valid role in dryRun mode. data.DisplayName = ptrStr("display name") - err = th.App.importRole(&data, true, false) + err = th.App.importRole(th.Context, &data, true, false) require.Nil(t, err, "Should have succeeded.") _, nErr = th.App.Srv().Store().Role().GetByName(context.Background(), rid1) @@ -432,7 +432,7 @@ func TestImportImportRole(t *testing.T) { // Try importing an invalid role. data.DisplayName = nil - err = th.App.importRole(&data, false, false) + err = th.App.importRole(th.Context, &data, false, false) require.NotNil(t, err, "Should have failed to import.") _, nErr = th.App.Srv().Store().Role().GetByName(context.Background(), rid1) @@ -443,7 +443,7 @@ func TestImportImportRole(t *testing.T) { data.Description = ptrStr("description") data.Permissions = &[]string{"invite_user", "add_user_to_team"} - err = th.App.importRole(&data, false, false) + err = th.App.importRole(th.Context, &data, false, false) require.Nil(t, err, "Should have succeeded.") role, nErr := th.App.Srv().Store().Role().GetByName(context.Background(), rid1) @@ -461,7 +461,7 @@ func TestImportImportRole(t *testing.T) { data.Description = ptrStr("description") data.Permissions = &[]string{"use_slash_commands"} - err = th.App.importRole(&data, false, true) + err = th.App.importRole(th.Context, &data, false, true) require.Nil(t, err, "Should have succeeded. %v", err) role, nErr = th.App.Srv().Store().Role().GetByName(context.Background(), rid1) @@ -480,7 +480,7 @@ func TestImportImportRole(t *testing.T) { DisplayName: ptrStr("new display name again"), } - err = th.App.importRole(&data2, false, false) + err = th.App.importRole(th.Context, &data2, false, false) require.Nil(t, err, "Should have succeeded.") role, nErr = th.App.Srv().Store().Role().GetByName(context.Background(), rid1) @@ -1384,7 +1384,7 @@ func TestImportImportUser(t *testing.T) { Description: ptrStr("description"), } - appErr = th.App.importScheme(teamSchemeData, false) + appErr = th.App.importScheme(th.Context, teamSchemeData, false) assert.Nil(t, appErr) teamScheme, nErr := th.App.Srv().Store().Scheme().GetByName(*teamSchemeData.Name) @@ -4151,7 +4151,7 @@ func TestImportImportEmoji(t *testing.T) { testImage := filepath.Join(testsDir, "test.png") data := imports.EmojiImportData{Name: ptrStr(model.NewId())} - appErr := th.App.importEmoji(&data, true) + appErr := th.App.importEmoji(th.Context, &data, true) assert.NotNil(t, appErr, "Invalid emoji should have failed dry run") emoji, nErr := th.App.Srv().Store().Emoji().GetByName(context.Background(), *data.Name, true) @@ -4159,35 +4159,35 @@ func TestImportImportEmoji(t *testing.T) { assert.Error(t, nErr) data.Image = ptrStr(testImage) - appErr = th.App.importEmoji(&data, true) + appErr = th.App.importEmoji(th.Context, &data, true) assert.Nil(t, appErr, "Valid emoji should have passed dry run") data = imports.EmojiImportData{Name: ptrStr(model.NewId())} - appErr = th.App.importEmoji(&data, false) + appErr = th.App.importEmoji(th.Context, &data, false) assert.NotNil(t, appErr, "Invalid emoji should have failed apply mode") data.Image = ptrStr("non-existent-file") - appErr = th.App.importEmoji(&data, false) + appErr = th.App.importEmoji(th.Context, &data, false) assert.NotNil(t, appErr, "Emoji with bad image file should have failed apply mode") data.Image = ptrStr(testImage) - appErr = th.App.importEmoji(&data, false) + appErr = th.App.importEmoji(th.Context, &data, false) assert.Nil(t, appErr, "Valid emoji should have succeeded apply mode") emoji, nErr = th.App.Srv().Store().Emoji().GetByName(context.Background(), *data.Name, true) assert.NotNil(t, emoji, "Emoji should have been imported") assert.NoError(t, nErr, "Emoji should have been imported without any error") - appErr = th.App.importEmoji(&data, false) + appErr = th.App.importEmoji(th.Context, &data, false) assert.Nil(t, appErr, "Second run should have succeeded apply mode") data = imports.EmojiImportData{Name: ptrStr("smiley"), Image: ptrStr(testImage)} - appErr = th.App.importEmoji(&data, false) + appErr = th.App.importEmoji(th.Context, &data, false) assert.Nil(t, appErr, "System emoji should not fail") largeImage := filepath.Join(testsDir, "large_image_file.jpg") data = imports.EmojiImportData{Name: ptrStr(model.NewId()), Image: ptrStr(largeImage)} - appErr = th.App.importEmoji(&data, false) + appErr = th.App.importEmoji(th.Context, &data, false) require.NotNil(t, appErr) require.ErrorIs(t, appErr.Unwrap(), utils.SizeLimitExceeded) } diff --git a/app/import_test.go b/app/import_test.go index 0ddb6834ec..60f8778165 100644 --- a/app/import_test.go +++ b/app/import_test.go @@ -17,7 +17,9 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/v6/app/imports" + "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/utils" "github.com/mattermost/mattermost-server/v6/utils/fileutils" ) @@ -238,7 +240,7 @@ func TestImportBulkImport(t *testing.T) { {"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme1 + `", "channels": [{"name": "` + channelName + `"}]}]}} {"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012, "attachments":[{"path": "` + testImage + `"}], "props":{"attachments":[{"id":0,"fallback":"[February 4th, 2020 2:46 PM] author: fallback","color":"D0D0D0","pretext":"","author_name":"author","author_link":"","title":"","title_link":"","text":"this post has props","fields":null,"image_url":"","thumb_url":"","footer":"Posted in #general","footer_icon":"","ts":"1580823992.000100"}]}}} {"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username + `"]}} -{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username + `"], "user": "` + username + `", "message": "Hello Direct Channel to myself", "create_at": 123456789014, "props":{"attachments":[{"id":0,"fallback":"[February 4th, 2020 2:46 PM] author: fallback","color":"D0D0D0","pretext":"","author_name":"author","author_link":"","title":"","title_link":"","text":"this post has props","fields":null,"image_url":"","thumb_url":"","footer":"Posted in #general","footer_icon":"","ts":"1580823992.000100"}]}}}}` +{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username + `"], "user": "` + username + `", "message": "Hello Direct Channel to myself", "create_at": 123456789014, "props":{"attachments":[{"id":0,"fallback":"[February 4th, 2020 2:46 PM] author: fallback","color":"D0D0D0","pretext":"","author_name":"author","author_link":"","title":"","title_link":"","text":"this post has props","fields":null,"image_url":"","thumb_url":"","footer":"Posted in #general","footer_icon":"","ts":"1580823992.000100"}]}}}` err, line := th.App.BulkImport(th.Context, strings.NewReader(data6), nil, false, 2) require.Nil(t, err, "BulkImport should have succeeded") @@ -285,6 +287,9 @@ func AssertFileIdsInPost(files []*model.FileInfo, th *TestHelper, t *testing.T) } func TestProcessAttachments(t *testing.T) { + logger, _ := mlog.NewLogger() + c := request.EmptyContext(logger) + genAttachments := func() *[]imports.AttachmentImportData { return &[]imports.AttachmentImportData{ { @@ -333,10 +338,11 @@ func TestProcessAttachments(t *testing.T) { Path: model.NewString("somedir/file.jpg"), }, } - err := processAttachments(&line, "", nil) + + err := processAttachments(c, &line, "", nil) require.NoError(t, err) require.Equal(t, expected, line.Post.Attachments) - err = processAttachments(&line2, "", nil) + err = processAttachments(c, &line2, "", nil) require.NoError(t, err) require.Equal(t, expected, line2.DirectPost.Attachments) }) @@ -352,27 +358,27 @@ func TestProcessAttachments(t *testing.T) { } t.Run("post attachments", func(t *testing.T) { - err := processAttachments(&line, "/tmp", nil) + err := processAttachments(c, &line, "/tmp", nil) require.NoError(t, err) require.Equal(t, expected, line.Post.Attachments) }) t.Run("direct post attachments", func(t *testing.T) { - err := processAttachments(&line2, "/tmp", nil) + err := processAttachments(c, &line2, "/tmp", nil) require.NoError(t, err) require.Equal(t, expected, line2.DirectPost.Attachments) }) t.Run("profile image", func(t *testing.T) { expected := "/tmp/profile.jpg" - err := processAttachments(&userLine, "/tmp", nil) + err := processAttachments(c, &userLine, "/tmp", nil) require.NoError(t, err) require.Equal(t, expected, *userLine.User.ProfileImage) }) t.Run("emoji", func(t *testing.T) { expected := "/tmp/emoji.png" - err := processAttachments(&emojiLine, "/tmp", nil) + err := processAttachments(c, &emojiLine, "/tmp", nil) require.NoError(t, err) require.Equal(t, expected, *emojiLine.Emoji.Image) }) @@ -383,11 +389,11 @@ func TestProcessAttachments(t *testing.T) { filesMap := map[string]*zip.File{ "/tmp/file.jpg": nil, } - err := processAttachments(&line, "", filesMap) + err := processAttachments(c, &line, "", filesMap) require.Error(t, err) filesMap["/tmp/somedir/file.jpg"] = nil - err = processAttachments(&line, "", filesMap) + err = processAttachments(c, &line, "", filesMap) require.NoError(t, err) }) @@ -395,11 +401,11 @@ func TestProcessAttachments(t *testing.T) { filesMap := map[string]*zip.File{ "/tmp/file.jpg": nil, } - err := processAttachments(&line2, "", filesMap) + err := processAttachments(c, &line2, "", filesMap) require.Error(t, err) filesMap["/tmp/somedir/file.jpg"] = nil - err = processAttachments(&line2, "", filesMap) + err = processAttachments(c, &line2, "", filesMap) require.NoError(t, err) }) @@ -407,11 +413,11 @@ func TestProcessAttachments(t *testing.T) { filesMap := map[string]*zip.File{ "/tmp/file.jpg": nil, } - err := processAttachments(&userLine, "", filesMap) + err := processAttachments(c, &userLine, "", filesMap) require.Error(t, err) filesMap["/tmp/profile.jpg"] = nil - err = processAttachments(&userLine, "", filesMap) + err = processAttachments(c, &userLine, "", filesMap) require.NoError(t, err) }) @@ -419,11 +425,11 @@ func TestProcessAttachments(t *testing.T) { filesMap := map[string]*zip.File{ "/tmp/file.jpg": nil, } - err := processAttachments(&emojiLine, "", filesMap) + err := processAttachments(c, &emojiLine, "", filesMap) require.Error(t, err) filesMap["/tmp/emoji.png"] = nil - err = processAttachments(&emojiLine, "", filesMap) + err = processAttachments(c, &emojiLine, "", filesMap) require.NoError(t, err) }) }) From 27143b3cbf1ff23cc9304a01b164a5ad9999bd58 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 12 Dec 2022 09:06:11 +0100 Subject: [PATCH 11/43] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ --- i18n/de.json | 4 ---- i18n/en_AU.json | 4 ---- i18n/es.json | 4 ---- i18n/fr.json | 4 ---- i18n/hu.json | 4 ---- i18n/it.json | 4 ---- i18n/ja.json | 8 -------- i18n/nl.json | 4 ---- i18n/pl.json | 4 ---- i18n/ru.json | 8 -------- i18n/sv.json | 4 ---- i18n/tr.json | 4 ---- i18n/zh-CN.json | 4 ---- 13 files changed, 60 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index 6b8f7e7db0..d643daca0d 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -9170,10 +9170,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Du bist jetzt upgegraded worden!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "Dein {{.WorkspaceName}} Arbeitsbereich wurde jetzt aktualisiert. Dein {{.WorkspaceName}} wird ab {{.Date}} abgerechnet" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Mattermost Upgrade Bestätigung" diff --git a/i18n/en_AU.json b/i18n/en_AU.json index 169abdd703..6754060585 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -9166,10 +9166,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "You are now upgraded!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "Your {{.WorkspaceName}} workspace has now been upgraded. You'll be billed from {{.Date}}" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Mattermost Upgrade Confirmation" diff --git a/i18n/es.json b/i18n/es.json index b3682a2730..68dd0230be 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -9175,10 +9175,6 @@ "id": "app.insights.feature_disabled", "translation": "La característica Perspectivas está deshabilitada." }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": " " - }, { "id": "api.insights.feature_disabled", "translation": "Las Perspectivas están detrás de una bandera de ajuste que no está habilitada." diff --git a/i18n/fr.json b/i18n/fr.json index 2600e826fa..074f142e3c 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -8803,10 +8803,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Vous avez été mis à niveau !" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "Votre espace de travail {{.WorkspaceName}} a été mis à niveau. Vous serez facturé le {{.Date}}" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Confirmation de mise à niveau de Mattermost" diff --git a/i18n/hu.json b/i18n/hu.json index 1ccbb817e8..3ddcd9f7b7 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -9159,10 +9159,6 @@ "id": "app.channel.get_file_count.app_error", "translation": "A csatorna fájljainak számát nem lehet lekérdezni" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "Az Ön {{.WorkspaceName}} munkaterülete mostantól a megemelt verziót használja. A számlázás {{.Date}} napon kezdődik" - }, { "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Ön mostantól a megemelt verziót használja!" diff --git a/i18n/it.json b/i18n/it.json index 1773114ab7..762c7d9bcc 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -8175,10 +8175,6 @@ "id": "app.insights.feature_disabled", "translation": " " }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": " " - }, { "id": "api.templates.questions_footer.title", "translation": " " diff --git a/i18n/ja.json b/i18n/ja.json index cc82a4b220..cb8acd6c8e 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -9163,10 +9163,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "アップグレードが完了しました!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "あなたの {{.WorkspaceName}} ワークスペースがアップグレードされました。{{.Date}}から課金されます" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Mattermostアップグレードの確認" @@ -9187,10 +9183,6 @@ "id": "api.file.cloud_upload.app_error", "translation": "クラウドインスタンスへのmmctlによるアップロードはサポートされていません。こちらのドキュメントを確認してください:https://docs.mattermost.com/manage/cloud-data-export.html。" }, - { - "id": "app.install_integration.reached_max_limit.error", - "translation": "有効な統合機能数の上限 {{.NumIntegrations}} に達しました。無制限に統合機能をインストールするには、いずれかの有料プランにアップグレードしてください。" - }, { "id": "model.config.is_valid.image_decoder_concurrency.app_error", "translation": "デコーダーの並列数 {{.Value}} は不正です。正の数または-1であるべきです。" diff --git a/i18n/nl.json b/i18n/nl.json index 2191b1f2bc..683aa0f875 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -9178,10 +9178,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Je bent nu geüpgraded!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "Jouw {{.WorkspaceName}} werkruimte is nu geüpgraded. Dit zal gefactureerd worden vanaf {{.Date}}" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Bevestiging Mattermost upgrade" diff --git a/i18n/pl.json b/i18n/pl.json index 1a7d81b48a..baa9790c06 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -9171,10 +9171,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Zostałeś uaktualniony!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "Twoja {{.WorkspaceName}} została zaktualizowana. Opłaty będą naliczane od {{.Date}}" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Potwierdzenie Aktualizacji Mattermost" diff --git a/i18n/ru.json b/i18n/ru.json index ce6fdce143..6ee29f16a8 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -9171,10 +9171,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Обновление прошло успешно!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "Ваше рабочее пространство {{.WorkspaceName}} обновлено. Вам будет выставлен счет с {{.Date}}" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Подтверждение обновления Mattermost" @@ -9343,10 +9339,6 @@ "id": "app.job.error", "translation": "Ошибка во время выполнения задания." }, - { - "id": "app.install_integration.reached_max_limit.error", - "translation": "Вы достигли лимита включенных интеграций - {{.NumIntegrations}} . Чтобы установить неограниченное количество интеграций, перейдите на один из наших платных тарифных планов." - }, { "id": "app.insights.feature_disabled", "translation": "Функция Insights отключена." diff --git a/i18n/sv.json b/i18n/sv.json index 1a23d896c6..affdc3cbba 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -9134,10 +9134,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Du är nu uppgraderad!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "Din {{.WorkspaceName}}-arbetsyta har nu uppgraderats. Du kommer att faktureras från och med {{.Date}}" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Bekräftelse av uppgradering av Mattermost" diff --git a/i18n/tr.json b/i18n/tr.json index 82331240fc..4da98f74f3 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -9170,10 +9170,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Üst tarifeye geçtiniz!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "{{.WorkspaceName}} çalışma alanınız üst tarifeye geçirildi. Faturanız {{.Date}} tarihinden başlayarak hesaplanacak" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Mattermost üst tarifeye geçme onayı" diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index a04ead8fd9..2d91449bce 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -9059,10 +9059,6 @@ "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "您已经完成升级更新!" }, - { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", - "translation": "您的“ {{.WorkspaceName}}”工作空间现在已经升级。您将从{{.TrialEnd}}开始收到账单通知" - }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "确认升级Mattermost" From 760dd1d5b4e6a9d2db36c9d29b7cd2402ac876e2 Mon Sep 17 00:00:00 2001 From: MArtin Johnson Date: Mon, 12 Dec 2022 09:06:12 +0100 Subject: [PATCH 12/43] Translated using Weblate (Swedish) Currently translated at 99.9% (2427 of 2429 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/sv/ Translated using Weblate (Swedish) Currently translated at 99.1% (2398 of 2418 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/sv/ Translated using Weblate (Swedish) Currently translated at 98.6% (2386 of 2418 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/sv/ --- i18n/sv.json | 184 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 178 insertions(+), 6 deletions(-) diff --git a/i18n/sv.json b/i18n/sv.json index affdc3cbba..ec2aa98509 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -65,7 +65,7 @@ }, { "id": "web.error.unsupported_browser.min_os_version.mac", - "translation": "macOS 10.14+" + "translation": "macOS 11+" }, { "id": "web.error.unsupported_browser.min_browser_version.safari", @@ -9300,7 +9300,7 @@ }, { "id": "api.templates.delinquency_45.subtitle1", - "translation": "Vi har inte kunnat få betalt för utestående fakturor med datum {{.DelinquencyDate}}. Din arbetsyta riskerar att nedgraderas." + "translation": "Vi har inte kunnat få betalt för utestående fakturor sedan {{.DelinquencyDate}}. Din arbetsyta riskerar att nedgraderas." }, { "id": "api.templates.delinquency_45.subject", @@ -9316,7 +9316,7 @@ }, { "id": "api.templates.delinquency_30.subtitle2", - "translation": "om ingen åtgärd vidtas kommer din arbetsyta att nedgraderas och följande uppgifter kan komma att arkiveras:" + "translation": "Om ingen åtgärd vidtas kommer din arbetsyta att nedgraderas och följande uppgifter kan komma att arkiveras:" }, { "id": "api.templates.delinquency_30.subtitle1", @@ -9352,7 +9352,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Betalningen för din Mattermost {{.Plan}} är försenad." + "translation": "Betalningen för din Mattermost {{.Plan}} är försenad" }, { "id": "ent.saml.configure.certificate_parse_error.app_error", @@ -9444,7 +9444,7 @@ }, { "id": "api.templates.delinquency_75.subtitle1", - "translation": "Detta är en sista påminnelse. Vi har inte mottagit betalning för din Mattermost Cloud-arbetsyta sedan {{.DelinquencyDate}}" + "translation": "Detta är en sista påminnelse. Vi har inte mottagit betalning för din Mattermost Cloud-arbetsyta sedan {{.DelinquencyDate}}." }, { "id": "api.templates.delinquency_75.subject", @@ -9468,7 +9468,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "Vi kunde inte behandla din senaste betalning" + "translation": "Vi kunde inte behandla din senaste betalning." }, { "id": "api.templates.delinquency_7.button", @@ -9553,5 +9553,177 @@ { "id": "app.collection.add_topic.exists.app_error", "translation": "Ämnestypen finns redan." + }, + { + "id": "app.draft.delete.app_error", + "translation": "Kunde inte att radera utkastet." + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "Det går inte att få en bekräftelse för publiceringen." + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "Det går inte att få en bekräftelse." + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Det går inte att radera bekräftelsen." + }, + { + "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", + "translation": "inte en ldap-användare" + }, + { + "id": "api.upload.create.upload_too_large.app_error", + "translation": "Kunde inte ladda upp fil. Filen är för stor." + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "Funktionen Utkast är inaktiverad." + }, + { + "id": "api.draft.create_draft.can_not_draft_to_deleted.error", + "translation": "Kan inte spara utkast i en borttagen kanal" + }, + { + "id": "api.admin.syncables_error", + "translation": "misslyckades med att lägga till användaren i gruppens team och gruppens kanaler" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "Du kan inte bekräfta i en arkiverad kanal." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Du kan inte radera en bekräftelse efter att 5 minuter har gått." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Du kan inte ta bort en bekräftelse i en arkiverad kanal." + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Skapa transparenta arbetsflöden mellan utvecklingsteamen för att säkerställa att utvecklingsprocessen för funktioner är smidig." + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Öka produktiviteten i kanalen genom att integrera en Jira-bot och en Github-bot. Dessa kommer att laddas ner åt dig." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Chatta med ditt team i kanalen för Kommande leveranser som enkelt kan anslutas till dina boards, playbooks och app-bottar." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Använd vår mall för mötesagenda för återkommande möten exempelvis standup och vår Projekt-board för projektuppgifter och att hantera uppgifternas framskridande." + }, + { + "id": "worktemplate.category.product_teams", + "translation": "Produkt-team" + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "Ogiltigt användarid." + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "\"Update at\" måste vara en giltig tid." + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "Ogiltigt root-id." + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "Ogiltig prioritet" + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "Ogiltigt meddelande." + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "Ogiltiga fil-ID:n." + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "Skapad vid måste vara en giltig tid." + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "Ogiltigt kanal-id." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Ogiltigt användarid." + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "Ogiltigt meddelande-id." + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "Det går inte att få fram arbetsmallar" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "Det går inte att få fram kategorier för arbetsmallar" + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "Det går inte att få fram inläggets prioritet" + }, + { + "id": "app.draft.update.app_error", + "translation": "Kunde inte uppdatera utkastet." + }, + { + "id": "app.draft.save.app_error", + "translation": "Kunde inte spara utkastet." + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "Det går inte att hämta utkastets filer." + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "Kunde inte hämta användarens utkast." + }, + { + "id": "app.draft.get.app_error", + "translation": "Kunde inte hämta utkastet." + }, + { + "id": "app.draft.feature_disabled", + "translation": "Funktionen utkast är inaktiverad." + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "Det går inte att få fram inläggets prioritet" + }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "Kunde inte räkna brådskande meddelanden från angivet datum." + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "Det går inte att spara bekräftelsen för inlägget." + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Fel vid hämtning av roller vid valideringen." + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "Visa fakturan" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Din arbetsyta {{.WorkspaceName}} har nu uppgraderats." + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "Din arbetsyta {{.WorkspaceName}} har nu uppgraderats. Du kommer att faktureras från och med {{.Date}}" } ] From 0d47ab8bead35f087b76a32064253a91d0783f20 Mon Sep 17 00:00:00 2001 From: jprusch Date: Mon, 12 Dec 2022 09:06:12 +0100 Subject: [PATCH 13/43] Translated using Weblate (German) Currently translated at 100.0% (2429 of 2429 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2428 of 2428 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2426 of 2426 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2418 of 2418 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ --- i18n/de.json | 176 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 172 insertions(+), 4 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index d643daca0d..d2ebc4bbd5 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -9296,7 +9296,7 @@ }, { "id": "api.templates.delinquency_75.subtitle1", - "translation": "Dies ist eine letzte Erinnerung, dass wir seit {{.DelinquencyDate}} keine Zahlung für deinen Mattermost Cloud-Arbeitsbereich erhalten haben" + "translation": "Dies ist eine letzte Erinnerung, dass wir seit {{.DelinquencyDate}} keine Zahlung für deinen Mattermost Cloud-Arbeitsbereich erhalten haben." }, { "id": "api.templates.delinquency_75.subject", @@ -9320,7 +9320,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "Wir konnten deine letzte Zahlung nicht bearbeiten" + "translation": "Wir konnten deine letzte Zahlung nicht bearbeiten." }, { "id": "api.templates.delinquency_7.button", @@ -9368,7 +9368,7 @@ }, { "id": "api.templates.delinquency_45.subtitle1", - "translation": "Wir waren nicht in der Lage, ausstehende Rechnungen mit Datum {{.DelinquencyDate}} zu begleichen. Dein Arbeitsbereich ist in Gefahr heruntergestuft zu werden." + "translation": "Wir waren nicht in der Lage, ausstehende Rechnungen seit {{.DelinquencyDate}} zu begleichen. Dein Arbeitsbereich ist in Gefahr heruntergestuft zu werden." }, { "id": "api.templates.delinquency_45.subject", @@ -9432,7 +9432,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Bezahlung ist überfällig für deinen Mattermost {{.Plan}}." + "translation": "Bezahlung ist überfällig für deinen Mattermost {{.Plan}}" }, { "id": "api.templates.delinquency_14.button", @@ -9561,5 +9561,173 @@ { "id": "api.admin.syncables_error", "translation": "Fehlschlag beim Hinzufügen des Benutzers zu Gruppen-Teams und -Kanälen" + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "Ungültige Benutzer-ID." + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "Aktualisiert am muss eine gültige Zeit sein." + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "Ungültige Root-ID." + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Ungültige Eigenschaften." + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "Ungültige Nachricht." + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "Ungültige Datei-IDs." + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "Erstellt am muss eine gültige Zeit sein." + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "Ungültige Kanal-ID." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Ungültige Benutzer-ID." + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "Ungültige Nachrichten-ID." + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "Die Priorität der Nachricht kann nicht ermittelt werden" + }, + { + "id": "app.draft.update.app_error", + "translation": "Die Aktualisierung des Entwurfs ist nicht möglich." + }, + { + "id": "app.draft.save.app_error", + "translation": "Der Entwurf kann nicht gespeichert werden." + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "Es können keine Dateien für den Entwurf abgerufen werden." + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "Die Entwürfe des Benutzers können nicht abgerufen werden." + }, + { + "id": "app.draft.get.app_error", + "translation": "Der Entwurf kann nicht abgerufen werden." + }, + { + "id": "app.draft.feature_disabled", + "translation": "Die Funktion Entwürfe ist deaktiviert." + }, + { + "id": "app.draft.delete.app_error", + "translation": "Der Entwurf kann nicht gelöscht werden." + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "Die Priorität der Nachrichten kann nicht ermittelt werden" + }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "Es ist nicht möglich, dringende Nachrichten seit dem angegebenen Datum zu zählen." + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "Die Bestätigung für die Nachricht kann nicht gespeichert werden." + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "Keine Bestätigung für den Beitrag erhalten." + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "Bestätigung nicht möglich." + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Bestätigung kann nicht gelöscht werden." + }, + { + "id": "api.upload.create.upload_too_large.app_error", + "translation": "Konnte Datei nicht hochladen. Datei ist zu groß." + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "Die Funktion Entwürfe ist deaktiviert." + }, + { + "id": "api.draft.create_draft.can_not_draft_to_deleted.error", + "translation": "Entwurf kann nicht in einem gelöschten Kanal gespeichert werden" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "Du kannst in einem archivierten Kanal nicht bestätigen." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Du kannst eine Bestätigung nach Ablauf von 5 Minuten nicht mehr löschen." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Du kannst eine Bestätigung in einem archivierten Kanal nicht entfernen." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Chatten mit deinem Team in einem Feature-Release-Kanal, der sich problemlos mit deinen Boards, Playbooks und App-Bots verbinden lässt." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Verwende unsere Vorlage für die Besprechungsagenda für wiederkehrende Besprechungen wie z. B. Standup-Meetings und unsere Projektaufgabentafel, um den Fortschritt der Aufgaben zu verwalten." + }, + { + "id": "worktemplate.category.product_teams", + "translation": "Produkt-Teams" + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "Ungültige Priorität" + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "Arbeitsvorlagen können nicht abgerufen werden" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "Arbeitsvorlagenkategorien können nicht abgerufen werden" + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Erstelle transparente Arbeitsabläufe zwischen den Entwicklungsteams, um einen nahtlosen Entwicklungsprozess zu gewährleisten." + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Steigere die Produktivität in deinem Kanal durch die Integration eines Jira-Bots und eines Github-Bots. Diese werden für dich heruntergeladen." + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "Deine Rechnung einsehen" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Dein {{.WorkspaceName}} Arbeitsbereich wurde jetzt hochgestuft." + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "Dein {{.WorkspaceName}} Arbeitsbereich wurde jetzt hochgestuft. Du wirst ab dem {{.Date}} abgerechnet" + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Fehler beim Abrufen von Rollen während der Validierung." } ] From b1a626457a74b2ee7817b275b296356a101e6445 Mon Sep 17 00:00:00 2001 From: kaakaa Date: Mon, 12 Dec 2022 09:06:12 +0100 Subject: [PATCH 14/43] Translated using Weblate (Japanese) Currently translated at 100.0% (2418 of 2418 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ja/ --- i18n/ja.json | 138 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 5 deletions(-) diff --git a/i18n/ja.json b/i18n/ja.json index cb8acd6c8e..4df39843dd 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -4741,7 +4741,7 @@ }, { "id": "web.error.unsupported_browser.min_os_version.mac", - "translation": "macOS 10.14+" + "translation": "macOS 11+" }, { "id": "web.error.unsupported_browser.min_browser_version.safari", @@ -9305,7 +9305,7 @@ }, { "id": "api.templates.delinquency_75.subtitle1", - "translation": "これは、{{.DelinquencyDate}}以降、Mattermost Cloudワークスペースの支払いを受け取っていないことを知らせる最後のリマンダーです" + "translation": "これは、{{.DelinquencyDate}}以降、Mattermost Cloudワークスペースの支払いを受け取っていないことを知らせる最後のリマンダーです。" }, { "id": "api.templates.delinquency_75.subject", @@ -9325,7 +9325,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "直近のお支払いを処理できませんでした" + "translation": "直近のお支払いを処理できませんでした。" }, { "id": "api.templates.delinquency_60.title", @@ -9365,7 +9365,7 @@ }, { "id": "api.templates.delinquency_45.subtitle1", - "translation": "{{.DelinquencyDate}} 付の未払い請求書への支払いを行うことができませんでした。お客様のワークスペースはダウングレードされる可能性があります。" + "translation": "{{.DelinquencyDate}} 以降の未払い請求書への支払いを行うことができませんでした。お客様のワークスペースはダウングレードされる可能性があります。" }, { "id": "api.templates.delinquency_45.subject", @@ -9417,7 +9417,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Mattermost {{.Plan}} の支払いが遅れています。" + "translation": "Mattermost {{.Plan}} の支払いが遅れています" }, { "id": "api.command_marketplace.unsupported.app_error", @@ -9546,5 +9546,133 @@ { "id": "app.collection.add_collection.exists.app_error", "translation": "Collection typeはすでに存在しています。" + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "不正なuser idです。" + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "更新日時は有効な時刻でなくてはなりません。" + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "不正なroot idです。" + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "不正なpropsです。" + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "不正なmessageです。" + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "不正なfile idsです。" + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "Create atは有効な時刻でなくてはなりません。" + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "不正なchannel idです。" + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "不正なuser idです。" + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "不正なpost idです。" + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "投稿に対する優先度を取得できませんでした" + }, + { + "id": "app.draft.update.app_error", + "translation": "下書きを更新できませんでした。" + }, + { + "id": "app.draft.save.app_error", + "translation": "下書きを保存できませんでした。" + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "下書きに添付されたファイルを取得できませんでした。" + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "ユーザーの下書きを取得できませんでした。" + }, + { + "id": "app.draft.get.app_error", + "translation": "下書きを取得できませんでした。" + }, + { + "id": "app.draft.feature_disabled", + "translation": "下書き機能は無効化されています。" + }, + { + "id": "app.draft.delete.app_error", + "translation": "下書きを削除できませんでした。" + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "投稿の優先度を取得できませんでした" + }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "指定された日付以降の緊急の投稿をカウントできませんでした。" + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "投稿への確認応答を保存できませんでした。" + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "投稿への確認応答を取得できませんでした。" + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "確認応答を取得できませんでした。" + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "確認応答を削除できませんでした。" + }, + { + "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", + "translation": "LDAPユーザーではありません" + }, + { + "id": "api.upload.create.upload_too_large.app_error", + "translation": "ファイルをアップロードできませんでした。ファイルが大きすぎます。" + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "下書き機能は無効化されています。" + }, + { + "id": "api.draft.create_draft.can_not_draft_to_deleted.error", + "translation": "削除されたチャンネルの下書きは保存できません" + }, + { + "id": "api.admin.syncables_error", + "translation": "group-teams と group-channels にユーザーを追加できませんでした" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "アーカイブされたチャンネルで確認応答をすることはできません。" + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "5分以上経過した確認応答は削除できません。" + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "アーカイブされたチャンネルでは、確認応答を削除することはできません。" } ] From cbc87dead60588855272d95fbfd13219db9f8a0d Mon Sep 17 00:00:00 2001 From: master7 Date: Mon, 12 Dec 2022 09:06:13 +0100 Subject: [PATCH 15/43] Translated using Weblate (Polish) Currently translated at 100.0% (2429 of 2429 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (2426 of 2426 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (2418 of 2418 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ --- i18n/pl.json | 174 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 169 insertions(+), 5 deletions(-) diff --git a/i18n/pl.json b/i18n/pl.json index baa9790c06..2489a694c5 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -9309,7 +9309,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Zaległa płatność za Twój Mattermost {{.Plan}}." + "translation": "Zaległa płatność za Twój Mattermost {{.Plan}}" }, { "id": "api.templates.delinquency_14.button", @@ -9325,7 +9325,7 @@ }, { "id": "api.templates.delinquency_75.subtitle1", - "translation": "Jest to ostateczne przypomnienie, że nie otrzymaliśmy płatności za obszar roboczy Mattermost Cloud od {{.DelinquencyDate}}" + "translation": "Jest to ostateczne przypomnienie, że nie otrzymaliśmy płatności za obszar roboczy Mattermost Cloud od {{.DelinquencyDate}}." }, { "id": "api.templates.delinquency_75.subject", @@ -9349,7 +9349,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "Nie mogliśmy przetworzyć Twojej ostatniej płatności" + "translation": "Nie mogliśmy przetworzyć Twojej ostatniej płatności." }, { "id": "api.templates.delinquency_7.button", @@ -9397,7 +9397,7 @@ }, { "id": "api.templates.delinquency_45.subtitle1", - "translation": "Nie udało nam się zebrać płatności za zaległe faktury z datą {{.DelinquencyDate}}. Twój obszar roboczy jest zagrożony zdegradowaniem." + "translation": "Od {{.DelinquencyDate}} nie udało nam się zebrać płatności za zaległe faktury. Twój obszar roboczy jest zagrożony obniżeniem poziomu." }, { "id": "api.templates.delinquency_45.subject", @@ -9413,7 +9413,7 @@ }, { "id": "api.templates.delinquency_30.subtitle2", - "translation": "jeśli nie zostaną podjęte żadne działania, Twój obszar roboczy zostanie zdegradowany, a następujące dane mogą zostać zarchiwizowane:" + "translation": "Jeśli nie zostaną podjęte żadne działania, Twój obszar roboczy zostanie zdegradowany, a następujące dane mogą zostać zarchiwizowane:" }, { "id": "api.templates.delinquency_30.subtitle1", @@ -9566,5 +9566,169 @@ { "id": "api.upload.create.upload_too_large.app_error", "translation": "Nie można przesłać pliku. Plik jest zbyt duży." + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "Nieprawidłowe id użytkownika." + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "Aktualizacja w musi zawierać prawidłowy czas." + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "Nieprawidłowy root id." + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Nieprawidłowa wartość." + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "Nieprawidłowa wiadomość." + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "Niepoprawne identyfikatory plików." + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "Data utworzenia musi zawierać prawidłowy czas." + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "Nieprawidłowy identyfikator kanału." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Nieprawidłowe id użytkownika." + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "Nieprawidłowy identyfikator posta." + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "Nie można uzyskać priorytetu dla posta" + }, + { + "id": "app.draft.update.app_error", + "translation": "Nie można zaktualizować szkicu." + }, + { + "id": "app.draft.save.app_error", + "translation": "Nie można zapisać Szkicu." + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "Nie można uzyskać plików dla Szkiców." + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "Nie można uzyskać szkiców użytkownika." + }, + { + "id": "app.draft.get.app_error", + "translation": "Nie mogę pobrać szablonu." + }, + { + "id": "app.draft.feature_disabled", + "translation": "Funkcja szkiców jest wyłączona." + }, + { + "id": "app.draft.delete.app_error", + "translation": "Nie można usunąć projektu." + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "Nie można uzyskać priorytetu dla postów" + }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "Nie można zliczyć pilnych postów od podanej daty." + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "Nie można zapisać potwierdzenia dla posta." + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "Nie można uzyskać potwierdzenia dla posta." + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "Nie można uzyskać potwierdzenia." + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Nie można usunąć potwierdzenia." + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "Funkcja szkiców jest wyłączona." + }, + { + "id": "api.draft.create_draft.can_not_draft_to_deleted.error", + "translation": "Nie można zapisać wersji roboczej do usuniętego kanału" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "Nie można potwierdzić w zarchiwizowanym kanale." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Nie można usunąć potwierdzenia po upływie 5 minut." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Nie można usunąć potwierdzenia w zarchiwizowanym kanale." + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Twórz przejrzyste przepływy pracy pomiędzy zespołami programistów, aby zapewnić płynny proces rozwoju funkcji." + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Zwiększ wydajność na swoim kanale, integrując bota Jira i bota Github. Zostaną one pobrane za Ciebie." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Czatuj ze swoim zespołem na kanale Feature Release, który łatwo łączy się z tablicami, playbookami i botami aplikacji." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Użyj naszego szablonu tablicy Meeting Agenda do powtarzających się spotkań, takich jak standup, oraz naszej tablicy Project Tasks do zarządzania postępem zadań w trakcie." + }, + { + "id": "worktemplate.category.product_teams", + "translation": "Zespoły Produkcyjne" + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "Nieprawidłowy priorytet" + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "Nie można uzyskać szablonów roboczych" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "Nie można uzyskać kategorii szablonów roboczych" + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Błąd pobierania ról podczas sprawdzania poprawności." + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "Zobacz swoją fakturę" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Twoja przestrzeń robocza {{.WorkspaceName}} została zaktualizowana." + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "Twoja {{.WorkspaceName}} została zaktualizowana. Opłaty będą naliczane od {{.Date}}" } ] From ec4a26fcf7790eb2003b75cc6ef6f8612a14169c Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 12 Dec 2022 09:06:13 +0100 Subject: [PATCH 16/43] Translated using Weblate (Russian) Currently translated at 100.0% (2429 of 2429 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ru/ Translated using Weblate (Russian) Currently translated at 100.0% (2428 of 2428 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ru/ Translated using Weblate (Russian) Currently translated at 100.0% (2426 of 2426 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ru/ Translated using Weblate (Russian) Currently translated at 100.0% (2418 of 2418 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ru/ --- i18n/ru.json | 172 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 168 insertions(+), 4 deletions(-) diff --git a/i18n/ru.json b/i18n/ru.json index 6ee29f16a8..406b4220b0 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -9189,7 +9189,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Просрочена оплата вашего Mattermost {{.Plan}}." + "translation": "Просрочена оплата вашего Mattermost {{.Plan}}" }, { "id": "api.templates.delinquency_14.button", @@ -9425,7 +9425,7 @@ }, { "id": "api.templates.delinquency_75.subtitle1", - "translation": "Это последнее напоминание о том, что мы не получили оплату за ваше рабочее пространство Mattermost Cloud с {{.DelinquencyDate}}" + "translation": "Это последнее напоминание о том, что мы не получили оплату за ваше рабочее пространство Mattermost Cloud с {{.DelinquencyDate}}." }, { "id": "api.templates.delinquency_75.subject", @@ -9445,7 +9445,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "Мы не смогли обработать ваш последний платёж" + "translation": "Мы не смогли обработать ваш последний платёж." }, { "id": "api.templates.delinquency_7.button", @@ -9505,7 +9505,7 @@ }, { "id": "api.templates.delinquency_30.subtitle2", - "translation": "если не предпринять никаких действий, ваше рабочее пространство будет понижено по тарифной сетке, а следующие данные могут быть заархивированы:" + "translation": "Если не предпринять никаких действий, ваше рабочее пространство будет понижено по тарифной сетке, а следующие данные могут быть заархивированы:" }, { "id": "api.templates.delinquency_30.subtitle1", @@ -9566,5 +9566,169 @@ { "id": "api.admin.syncables_error", "translation": "не удалось добавить пользователя в group-teams и group-channels" + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "Некорректный user id." + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "\"Обновить в\" должно быть корректным временем." + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "Некорректный идентификатор root." + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Некорректные свойства." + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "Неверное сообщение." + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "Недопустимые идентификаторы файлов." + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "\"Создать\" должно быть корректным временем." + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "Некорректный идентификатор канала." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Некорректный user id." + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "Неверный идентификатор сообщения." + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "Невозможно получить приоритет для сообщения" + }, + { + "id": "app.draft.update.app_error", + "translation": "Невозможно обновить черновик." + }, + { + "id": "app.draft.save.app_error", + "translation": "Невозможно сохранить черновик." + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "Невозможно получить файлы для черновика." + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "Невозможно получить черновики пользователя." + }, + { + "id": "app.draft.get.app_error", + "translation": "Невозможно получить черновик." + }, + { + "id": "app.draft.feature_disabled", + "translation": "Функция \"Черновики\" отключена." + }, + { + "id": "app.draft.delete.app_error", + "translation": "Невозможно удалить черновик." + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "Невозможно получить приоритет для сообщений" + }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "Невозможно подсчитать срочные сообщения с указанной даты." + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "Невозможно сохранить подтверждение для сообщения." + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "Невозможно получить подтверждение о получении сообщения." + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "Невозможно получить подтверждение." + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Невозможно удалить подтверждение." + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "Функция \"Черновики\" отключена." + }, + { + "id": "api.draft.create_draft.can_not_draft_to_deleted.error", + "translation": "Невозможно сохранить черновик в удаленном канале" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "Вы не можете подтвердить в архивированном канале." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Вы не можете удалить подтверждение после того, как прошло 5 минут." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Вы не можете удалить подтверждение в архивированном канале." + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Создайте прозрачные рабочие процессы между командами разработчиков, чтобы обеспечить бесперебойный процесс разработки функций." + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Повысьте производительность вашего канала, интегрировав бота для Jira и бота для Github. Они будут загружены для вас." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Общайтесь со своей командой в канале Feature Release, который легко соединяется с вашими досками, сценариями и ботами приложений." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Используйте наш шаблон доски \"Повестка дня совещания\" для повторяющихся совещаний, например, совещаний по подготовке к работе, и доску \"Задачи проекта\" для управления ходом выполнения задач." + }, + { + "id": "worktemplate.category.product_teams", + "translation": "Продуктовые команды" + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "Неверный приоритет" + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "Невозможно получить рабочие шаблоны" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "Невозможно получить категории рабочих шаблонов" + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "Просмотр счета-фактуры" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Ваше рабочее пространство {{.WorkspaceName}} теперь обновлено." + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "Ваше рабочее пространство {{.WorkspaceName}} обновлено. Вам будет выставлен счет с {{.Date}}" + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Ошибка при получении ролей во время проверки." } ] From b60e7acd49176c80f9acdbfb684d3fb2ac79da61 Mon Sep 17 00:00:00 2001 From: Matthew Williams Date: Mon, 12 Dec 2022 09:06:14 +0100 Subject: [PATCH 17/43] Translated using Weblate (English (Australia)) Currently translated at 99.6% (2420 of 2429 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/en_AU/ Translated using Weblate (English (Australia)) Currently translated at 99.6% (2419 of 2428 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/en_AU/ Translated using Weblate (English (Australia)) Currently translated at 99.6% (2417 of 2426 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/en_AU/ Translated using Weblate (English (Australia)) Currently translated at 99.7% (2411 of 2418 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/en_AU/ --- i18n/en_AU.json | 136 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 4 deletions(-) diff --git a/i18n/en_AU.json b/i18n/en_AU.json index 6754060585..3283b75397 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -9324,7 +9324,7 @@ }, { "id": "api.templates.delinquency_75.subtitle1", - "translation": "This is a final reminder that payment for your Mattermost Cloud workspace hasn't been received since {{.DelinquencyDate}}" + "translation": "This is a final reminder that payment for your Mattermost Cloud workspace hasn't been received since {{.DelinquencyDate}}." }, { "id": "api.templates.delinquency_75.subject", @@ -9344,7 +9344,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "Your most recent payment couldn't be processed" + "translation": "Your most recent payment couldn't be processed." }, { "id": "api.templates.delinquency_7.button", @@ -9392,7 +9392,7 @@ }, { "id": "api.templates.delinquency_45.subtitle1", - "translation": "Payment for outstanding invoices dated {{.DelinquencyDate}} have not been able to be collected. Your workspace is at risk of being downgraded." + "translation": "Payment for outstanding invoices since {{.DelinquencyDate}} have not been able to be collected. Your workspace is at risk of being downgraded." }, { "id": "api.templates.delinquency_45.subject", @@ -9452,7 +9452,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Payment is overdue for your Mattermost {{.Plan}}." + "translation": "Payment is overdue for your Mattermost {{.Plan}}" }, { "id": "api.templates.delinquency_14.button", @@ -9565,5 +9565,133 @@ { "id": "api.admin.syncables_error", "translation": "Failed to add user to group-teams and group-channels" + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "Invalid user ID." + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "Update at must be a valid time." + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "Invalid root ID." + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Invalid props." + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "Invalid message." + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "Invalid file IDs." + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "Create at must be a valid time." + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "Invalid channel ID." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Invalid user ID." + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "Invalid post ID." + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "Unable to get post priority for post" + }, + { + "id": "app.draft.update.app_error", + "translation": "Unable to update the draft." + }, + { + "id": "app.draft.save.app_error", + "translation": "Unable to save the draft." + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "Unable to get files for draft." + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "Unable to get user's drafts." + }, + { + "id": "app.draft.get.app_error", + "translation": "Unable to get the draft." + }, + { + "id": "app.draft.feature_disabled", + "translation": "Drafts feature is disabled." + }, + { + "id": "app.draft.delete.app_error", + "translation": "Unable to delete the draft." + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "Unable to get the priority for posts" + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Unable to delete acknowledgement." + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "Drafts feature is disabled." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "You cannot remove an acknowledgment in an archived channel." + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Create transparent workflows across development teams to ensure your feature development process is seamless." + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Increase productivity in your channel by integrating a Jira bot and GitHub bot. These will be downloaded for you." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Use the Meeting Agenda board template for recurring meetings like standup and the Project Tasks board to manage the progress of tasks along the way." + }, + { + "id": "worktemplate.category.product_teams", + "translation": "Product Teams" + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "Invalid priority" + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "View your invoice" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Your {{.WorkspaceName}} workspace has now been upgraded." + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "Your {{.WorkspaceName}} workspace has now been upgraded. You'll be charged from {{.Date}}." + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Error fetching roles during validation." } ] From aa4e6105f2813f35c0e97fa7ac5e68ca7ba3bf51 Mon Sep 17 00:00:00 2001 From: Kaya Zeren Date: Mon, 12 Dec 2022 09:06:14 +0100 Subject: [PATCH 18/43] Translated using Weblate (Turkish) Currently translated at 100.0% (2429 of 2429 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (2426 of 2426 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ Translated using Weblate (Turkish) Currently translated at 98.4% (2388 of 2426 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ --- i18n/tr.json | 188 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 182 insertions(+), 6 deletions(-) diff --git a/i18n/tr.json b/i18n/tr.json index 4da98f74f3..39a1074bc0 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -5857,7 +5857,7 @@ }, { "id": "web.error.unsupported_browser.min_os_version.mac", - "translation": "macOS 10.14+" + "translation": "macOS 11+" }, { "id": "web.error.unsupported_browser.min_os_version.windows", @@ -6733,7 +6733,7 @@ }, { "id": "app.channel.count_posts_since.app_error", - "translation": "Belirtilen tarihten sonraki ileti sayıları hesaplanamadı." + "translation": "Belirtilen tarihten sonraki ileti sayıları belirlenemedi." }, { "id": "app.channel.analytics_type_count.app_error", @@ -9332,7 +9332,7 @@ }, { "id": "api.templates.delinquency_75.subtitle1", - "translation": "Bu son uyarıdır. Mattermost Cloud çalışma alanınızın {{.DelinquencyDate}} tarihindeki ödemesini alamadık" + "translation": "Bu son uyarıdır. Mattermost Cloud çalışma alanınızın ödemesini {{.DelinquencyDate}} tarihinden beri alamadık." }, { "id": "api.templates.delinquency_7.subtitle2", @@ -9356,7 +9356,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "Son ödemenizi alamadık" + "translation": "Son ödemenizi alamadık." }, { "id": "api.templates.delinquency_7.button", @@ -9384,7 +9384,7 @@ }, { "id": "api.templates.delinquency_45.subtitle1", - "translation": "{{.DelinquencyDate}} tarihli ödenmemiş faturaların ödemesini alamadık. Çalışma alanınızın alt tarifeye geçirilme riski var." + "translation": "{{.DelinquencyDate}} tarihinden beri ödenmemiş faturaların ödemesini alamadık. Çalışma alanınızın alt tarifeye geçirilme riski var." }, { "id": "api.templates.delinquency_45.subject", @@ -9472,7 +9472,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Mattermost {{.Plan}} tarifenizin ödeme süresi geçmiş." + "translation": "Mattermost {{.Plan}} tarifenizin ödeme süresi geçmiş" }, { "id": "api.templates.delinquency_14.button", @@ -9553,5 +9553,181 @@ { "id": "app.collection.add_collection.exists.app_error", "translation": "Derleme türü zaten var." + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "Taslaklar özelliği devre dışı." + }, + { + "id": "api.draft.create_draft.can_not_draft_to_deleted.error", + "translation": "Taslak silinmiş kanala kaydedilemedi" + }, + { + "id": "api.admin.syncables_error", + "translation": "kullanıcı group-teams ve group-channels üzerine eklenemedi" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "Arşivlenmiş bir kanalda onay veremezsiniz." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Bir onayı, verilmesinden 5 dakika geçtikten sonra kaldıramazsınız." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Arşivlenmiş bir kanaldaki bir onayı kaldıramazsınız." + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Özellik geliştirme sürecinizin sorunsuz olmasını sağlamak için geliştirme ekipleri arasında şeffaf iş akışları oluşturun." + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Bir Jira botu ve Github botu ile bütünleştirerek kanalınızdaki üretkenliği artırın. Bu botlar sizin için indirilir." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Panolarınıza, senaryolarınıza ve uygulama botlarınıza kolayca bağlanan bir özellik yayını kanalında ekibinizle sohbet edin." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Ayaküstü gibi yinelenen toplantılar için toplantı gündemi panosu kalıbımızı ve yol boyunca görevlerin ilerleyişini yönetmek için proje görevleri panomuzu kullanın." + }, + { + "id": "worktemplate.category.product_teams", + "translation": "Ürün takımları" + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "Kullanıcı kodu geçersiz." + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "Güncelleme zamanı geçerli bir zaman olmalıdır." + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "Kök kodu geçersiz." + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Özellikler geçersiz." + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "Öncelik geçersiz" + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "İleti geçersiz." + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "Dosya kodları geçersiz." + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "Oluşturulma zamanı geçerli bir zaman olmalıdır." + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "Kanal kodu geçersiz." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Kullanıcı kodu geçersiz." + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "İleti kodu geçersiz." + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "Çalışma kalıpları alınamadı" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "Çalışma kalıbı kategorileri alınamadı" + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "İletinin önceliği alınamadı" + }, + { + "id": "app.draft.update.app_error", + "translation": "Taslak güncellenemedi." + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "Kullanıcının taslakları alınamadı." + }, + { + "id": "app.draft.save.app_error", + "translation": "Taslak kaydedilemedi." + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "Taslağın dosyaları alınamadı." + }, + { + "id": "app.draft.get.app_error", + "translation": "Taslak alınamadı." + }, + { + "id": "app.draft.feature_disabled", + "translation": "Taslaklar özelliği devre dışı." + }, + { + "id": "app.draft.delete.app_error", + "translation": "Taslak silinemedi." + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "İletilerin önceliği alınamadı" + }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "Belirtilen tarihten sonraki acil iletilerin sayısı belirlenemedi." + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "İletinin onayı kaydedilemedi." + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "İletinin onayı alınamadı." + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "Onay alınamadı." + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Onay silinemedi." + }, + { + "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", + "translation": "bir LDAP kullanıcısı değil" + }, + { + "id": "api.upload.create.upload_too_large.app_error", + "translation": "Dosya yüklenemedi. Dosya çok büyük." + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Doğrulama sırasında roller alınırken sorun çıktı." + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "Faturanızı görüntüleyin" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "{{.WorkspaceName}} çalışma alanınız yükseltildi." + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "{{.WorkspaceName}} çalışma alanınız güncellendi. Faturanız {{.Date}} tarihinden başlayarak hesaplanacak" } ] From 28486e86905d0457174b5fc20b8c10b3e7b1b96a Mon Sep 17 00:00:00 2001 From: Remy J Date: Mon, 12 Dec 2022 09:06:14 +0100 Subject: [PATCH 19/43] Translated using Weblate (French) Currently translated at 94.8% (2300 of 2426 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/fr/ --- i18n/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/fr.json b/i18n/fr.json index 074f142e3c..107121d484 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -4573,7 +4573,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "Les conditions d'utilisation de GitLab ont été mises à jour. Veuillez vous rendre sur gitlab.com pour les accepter et réessayez de vous connecter à Mattermost." + "translation": "Les conditions d'utilisation de GitLab ont été mises à jour. Veuillez vous rendre sur {{.URL}} pour les accepter et réessayez de vous connecter à Mattermost." }, { "id": "plugin.api.update_user_status.bad_status", From 701d0ecaa2f0840a8d170f09aec0567ac3c322b5 Mon Sep 17 00:00:00 2001 From: emmyni <44761757+emmyni@users.noreply.github.com> Date: Tue, 13 Dec 2022 08:55:52 -0500 Subject: [PATCH 20/43] [MM-48098]: "Downgrade to Starter" CTA's go to purchase modal instead of pricing modal (#21849) --- templates/cloud_45_day_arrears.html | 2 +- templates/cloud_90_day_arrears.html | 2 +- templates/partials/cloud_title_3subtitles_button.mjml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/cloud_45_day_arrears.html b/templates/cloud_45_day_arrears.html index f868f37ab4..58d7859d95 100644 --- a/templates/cloud_45_day_arrears.html +++ b/templates/cloud_45_day_arrears.html @@ -437,7 +437,7 @@ diff --git a/templates/cloud_90_day_arrears.html b/templates/cloud_90_day_arrears.html index 21769839ca..5bdeacbab7 100644 --- a/templates/cloud_90_day_arrears.html +++ b/templates/cloud_90_day_arrears.html @@ -437,7 +437,7 @@
- + {{.Props.SecondaryActionButtonText}}
diff --git a/templates/partials/cloud_title_3subtitles_button.mjml b/templates/partials/cloud_title_3subtitles_button.mjml index 38c39071af..5ac4edf7e9 100644 --- a/templates/partials/cloud_title_3subtitles_button.mjml +++ b/templates/partials/cloud_title_3subtitles_button.mjml @@ -16,7 +16,7 @@ {{.Props.Button}} {{if .IncludeSecondaryActionButton}} - + {{.Props.SecondaryActionButtonText}} {{end}} From 4cd205027cc7c7fab644193ff6af8ca434b94196 Mon Sep 17 00:00:00 2001 From: emmyni <44761757+emmyni@users.noreply.github.com> Date: Tue, 13 Dec 2022 08:57:46 -0500 Subject: [PATCH 21/43] [MM-48091]: Remove is_paid_tier (#21850) --- api4/cloud.go | 1 - api4/cloud_test.go | 3 --- app/user_test.go | 1 - model/cloud.go | 1 - 4 files changed, 6 deletions(-) diff --git a/api4/cloud.go b/api4/cloud.go index 02fab0752a..2499164fac 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -80,7 +80,6 @@ func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) { Seats: 0, Status: "", DNS: "", - IsPaidTier: "", LastInvoice: &model.Invoice{}, DelinquentSince: subscription.DelinquentSince, } diff --git a/api4/cloud_test.go b/api4/cloud_test.go index f44176656c..75bf89f631 100644 --- a/api4/cloud_test.go +++ b/api4/cloud_test.go @@ -124,7 +124,6 @@ func Test_GetSubscription(t *testing.T) { Seats: 10, IsFreeTrial: "true", DNS: "some.dns.server", - IsPaidTier: "false", TrialEndAt: 2000000000, LastInvoice: &model.Invoice{}, DelinquentSince: &deliquencySince, @@ -141,7 +140,6 @@ func Test_GetSubscription(t *testing.T) { Seats: 0, IsFreeTrial: "true", DNS: "", - IsPaidTier: "", TrialEndAt: 2000000000, LastInvoice: &model.Invoice{}, DelinquentSince: &deliquencySince, @@ -209,7 +207,6 @@ func Test_requestTrial(t *testing.T) { CreateAt: 1000000000, Seats: 10, DNS: "some.dns.server", - IsPaidTier: "false", } newValidBusinessEmail := model.StartCloudTrialRequest{Email: ""} diff --git a/app/user_test.go b/app/user_test.go index 80a9ffd77d..788ae11275 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -1877,7 +1877,6 @@ func TestSendSubscriptionHistoryEvent(t *testing.T) { CreateAt: 1000000000, Seats: 10, DNS: "some.dns.server", - IsPaidTier: "false", } subscriptionHistory := &model.SubscriptionHistory{ diff --git a/model/cloud.go b/model/cloud.go index 6e66015a87..bdbc39bfe7 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -166,7 +166,6 @@ type Subscription struct { Seats int `json:"seats"` Status string `json:"status"` DNS string `json:"dns"` - IsPaidTier string `json:"is_paid_tier"` LastInvoice *Invoice `json:"last_invoice"` UpcomingInvoice *Invoice `json:"upcoming_invoice"` IsFreeTrial string `json:"is_free_trial"` From 68373e992bb8d33eaac3b1a9d72d93cfaf08ce63 Mon Sep 17 00:00:00 2001 From: Mylon Suren <23694620+mylonsuren@users.noreply.github.com> Date: Tue, 13 Dec 2022 09:44:09 -0500 Subject: [PATCH 22/43] set global drafts feature flag to true (#21811) Automatic Merge --- model/feature_flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/feature_flags.go b/model/feature_flags.go index 9356df0b89..27bab4830e 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -104,7 +104,7 @@ func (f *FeatureFlags) SetDefaults() { f.AnnualSubscription = false f.ReduceOnBoardingTaskList = false f.ThreadsEverywhere = false - f.GlobalDrafts = false + f.GlobalDrafts = true } func (f *FeatureFlags) Plugins() map[string]string { From 6fd174a95fa68fc183b112096d0dec70a04288cb Mon Sep 17 00:00:00 2001 From: Claudio Costa Date: Tue, 13 Dec 2022 11:47:05 -0600 Subject: [PATCH 23/43] [MM-48946] Fix read after write issue when uploading data (#21868) * Fix read after write issue when uploading data * Prefer request.CTX interface --- api4/remote_cluster.go | 4 +++- api4/upload.go | 6 ++++-- app/app_iface.go | 4 ++-- app/opentracing/opentracing_layer.go | 6 +++--- app/plugin_api.go | 4 +++- app/upload.go | 11 ++++++----- store/opentracinglayer/opentracinglayer.go | 4 ++-- store/retrylayer/retrylayer.go | 4 ++-- store/sqlstore/upload_session_store.go | 5 +++-- store/store.go | 2 +- store/storetest/mocks/UploadSessionStore.go | 16 +++++++++------- store/storetest/upload_session_store.go | 9 +++++---- store/timerlayer/timerlayer.go | 4 ++-- 13 files changed, 45 insertions(+), 34 deletions(-) diff --git a/api4/remote_cluster.go b/api4/remote_cluster.go index 0fe0be57be..c70d767cd3 100644 --- a/api4/remote_cluster.go +++ b/api4/remote_cluster.go @@ -9,6 +9,7 @@ import ( "net/http" "time" + "github.com/mattermost/mattermost-server/v6/app" "github.com/mattermost/mattermost-server/v6/audit" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/services/remotecluster" @@ -194,7 +195,8 @@ func uploadRemoteData(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) auditRec.AddEventParameter("upload_id", c.Params.UploadId) - us, err := c.App.GetUploadSession(c.Params.UploadId) + c.AppContext.SetContext(app.WithMaster(c.AppContext.Context())) + us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId) if err != nil { c.Err = err return diff --git a/api4/upload.go b/api4/upload.go index a89bc1149c..37f5a1bb01 100644 --- a/api4/upload.go +++ b/api4/upload.go @@ -10,6 +10,7 @@ import ( "mime/multipart" "net/http" + "github.com/mattermost/mattermost-server/v6/app" "github.com/mattermost/mattermost-server/v6/audit" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -91,7 +92,7 @@ func getUpload(c *Context, w http.ResponseWriter, r *http.Request) { return } - us, err := c.App.GetUploadSession(c.Params.UploadId) + us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId) if err != nil { c.Err = err return @@ -123,7 +124,8 @@ func uploadData(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) auditRec.AddEventParameter("upload_id", c.Params.UploadId) - us, err := c.App.GetUploadSession(c.Params.UploadId) + c.AppContext.SetContext(app.WithMaster(c.AppContext.Context())) + us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId) if err != nil { c.Err = err return diff --git a/app/app_iface.go b/app/app_iface.go index c27ea210c1..0465d8f916 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -809,7 +809,7 @@ type AppIface interface { GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) GetTopThreadsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) GetTopThreadsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) - GetUploadSession(uploadId string) (*model.UploadSession, *model.AppError) + GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError) GetUser(userID string) (*model.User, *model.AppError) GetUserAccessToken(tokenID string, sanitize bool) (*model.UserAccessToken, *model.AppError) @@ -1147,7 +1147,7 @@ type AppIface interface { UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) UpdateUserRoles(c request.CTX, userID string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) UpdateUserRolesWithUser(c request.CTX, user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) - UploadData(c *request.Context, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) + UploadData(c request.CTX, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) UploadEmojiImage(c request.CTX, id string, imageData *multipart.FileHeader) *model.AppError UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 2d60825ebc..4e2f0fb783 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -10305,7 +10305,7 @@ func (a *OpenTracingAppLayer) GetTotalUsersStats(viewRestrictions *model.ViewUse return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetUploadSession(uploadId string) (*model.UploadSession, *model.AppError) { +func (a *OpenTracingAppLayer) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUploadSession") @@ -10317,7 +10317,7 @@ func (a *OpenTracingAppLayer) GetUploadSession(uploadId string) (*model.UploadSe }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetUploadSession(uploadId) + resultVar0, resultVar1 := a.app.GetUploadSession(c, uploadId) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -18135,7 +18135,7 @@ func (a *OpenTracingAppLayer) UpdateWebConnUserActivity(session model.Session, a a.app.UpdateWebConnUserActivity(session, activityAt) } -func (a *OpenTracingAppLayer) UploadData(c *request.Context, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) { +func (a *OpenTracingAppLayer) UploadData(c request.CTX, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadData") diff --git a/app/plugin_api.go b/app/plugin_api.go index accedf37ba..5c8ee9fad1 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -1255,7 +1255,9 @@ func (api *PluginAPI) UploadData(us *model.UploadSession, rd io.Reader) (*model. } func (api *PluginAPI) GetUploadSession(uploadID string) (*model.UploadSession, error) { - fi, err := api.app.GetUploadSession(uploadID) + // We want to fetch from master DB to avoid a potential read-after-write on the plugin side. + api.ctx.SetContext(WithMaster(api.ctx.Context())) + fi, err := api.app.GetUploadSession(api.ctx, uploadID) if err != nil { return nil, err } diff --git a/app/upload.go b/app/upload.go index 4da0b853b6..ef4bff2b71 100644 --- a/app/upload.go +++ b/app/upload.go @@ -48,7 +48,7 @@ func (a *App) genFileInfoFromReader(name string, file io.ReadSeeker, size int64) return info, nil } -func (a *App) runPluginsHook(c *request.Context, info *model.FileInfo, file io.Reader) *model.AppError { +func (a *App) runPluginsHook(c request.CTX, info *model.FileInfo, file io.Reader) *model.AppError { filePath := info.Path // using a pipe to avoid loading the whole file content in memory. r, w := io.Pipe() @@ -154,8 +154,8 @@ func (a *App) CreateUploadSession(c request.CTX, us *model.UploadSession) (*mode return us, nil } -func (a *App) GetUploadSession(uploadId string) (*model.UploadSession, *model.AppError) { - us, err := a.Srv().Store().UploadSession().Get(uploadId) +func (a *App) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) { + us, err := a.Srv().Store().UploadSession().Get(c.Context(), uploadId) if err != nil { var nfErr *store.ErrNotFound switch { @@ -179,7 +179,7 @@ func (a *App) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, * return uss, nil } -func (a *App) UploadData(c *request.Context, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) { +func (a *App) UploadData(c request.CTX, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) { // prevent more than one caller to upload data at the same time for a given upload session. // This is to avoid possible inconsistencies. a.ch.uploadLockMapMut.Lock() @@ -202,7 +202,8 @@ func (a *App) UploadData(c *request.Context, us *model.UploadSession, rd io.Read }() // fetch the session from store to check for inconsistencies. - if storedSession, err := a.GetUploadSession(us.Id); err != nil { + c.SetContext(WithMaster(c.Context())) + if storedSession, err := a.GetUploadSession(c, us.Id); err != nil { return nil, err } else if us.FileOffset != storedSession.FileOffset { return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error", diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index aa41c28ab5..241f3edf50 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -10612,7 +10612,7 @@ func (s *OpenTracingLayerUploadSessionStore) Delete(id string) error { return err } -func (s *OpenTracingLayerUploadSessionStore) Get(id string) (*model.UploadSession, error) { +func (s *OpenTracingLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Get") s.Root.Store.SetContext(newCtx) @@ -10621,7 +10621,7 @@ func (s *OpenTracingLayerUploadSessionStore) Get(id string) (*model.UploadSessio }() defer span.Finish() - result, err := s.UploadSessionStore.Get(id) + result, err := s.UploadSessionStore.Get(ctx, id) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 5aca1535cb..dec858bba9 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -12126,11 +12126,11 @@ func (s *RetryLayerUploadSessionStore) Delete(id string) error { } -func (s *RetryLayerUploadSessionStore) Get(id string) (*model.UploadSession, error) { +func (s *RetryLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) { tries := 0 for { - result, err := s.UploadSessionStore.Get(id) + result, err := s.UploadSessionStore.Get(ctx, id) if err == nil { return result, nil } diff --git a/store/sqlstore/upload_session_store.go b/store/sqlstore/upload_session_store.go index e22b2056f3..3efec8f239 100644 --- a/store/sqlstore/upload_session_store.go +++ b/store/sqlstore/upload_session_store.go @@ -4,6 +4,7 @@ package sqlstore import ( + "context" "database/sql" sq "github.com/mattermost/squirrel" @@ -78,7 +79,7 @@ func (us SqlUploadSessionStore) Update(session *model.UploadSession) error { return nil } -func (us SqlUploadSessionStore) Get(id string) (*model.UploadSession, error) { +func (us SqlUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) { if !model.IsValidId(id) { return nil, errors.New("SqlUploadSessionStore.Get: id is not valid") } @@ -91,7 +92,7 @@ func (us SqlUploadSessionStore) Get(id string) (*model.UploadSession, error) { return nil, errors.Wrap(err, "SqlUploadSessionStore.Get: failed to build query") } var session model.UploadSession - if err := us.GetReplicaX().Get(&session, query, args...); err != nil { + if err := us.DBXFromContext(ctx).Get(&session, query, args...); err != nil { if err == sql.ErrNoRows { return nil, store.NewErrNotFound("UploadSession", id) } diff --git a/store/store.go b/store/store.go index 6a2a7d6e1d..6a7f386553 100644 --- a/store/store.go +++ b/store/store.go @@ -714,7 +714,7 @@ type FileInfoStore interface { type UploadSessionStore interface { Save(session *model.UploadSession) (*model.UploadSession, error) Update(session *model.UploadSession) error - Get(id string) (*model.UploadSession, error) + Get(ctx context.Context, id string) (*model.UploadSession, error) GetForUser(userID string) ([]*model.UploadSession, error) Delete(id string) error } diff --git a/store/storetest/mocks/UploadSessionStore.go b/store/storetest/mocks/UploadSessionStore.go index dcb8129e9b..f27dbf6856 100644 --- a/store/storetest/mocks/UploadSessionStore.go +++ b/store/storetest/mocks/UploadSessionStore.go @@ -5,6 +5,8 @@ package mocks import ( + context "context" + model "github.com/mattermost/mattermost-server/v6/model" mock "github.com/stretchr/testify/mock" ) @@ -28,13 +30,13 @@ func (_m *UploadSessionStore) Delete(id string) error { return r0 } -// Get provides a mock function with given fields: id -func (_m *UploadSessionStore) Get(id string) (*model.UploadSession, error) { - ret := _m.Called(id) +// Get provides a mock function with given fields: ctx, id +func (_m *UploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) { + ret := _m.Called(ctx, id) var r0 *model.UploadSession - if rf, ok := ret.Get(0).(func(string) *model.UploadSession); ok { - r0 = rf(id) + if rf, ok := ret.Get(0).(func(context.Context, string) *model.UploadSession); ok { + r0 = rf(ctx, id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.UploadSession) @@ -42,8 +44,8 @@ func (_m *UploadSessionStore) Get(id string) (*model.UploadSession, error) { } var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(id) + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) } else { r1 = ret.Error(1) } diff --git a/store/storetest/upload_session_store.go b/store/storetest/upload_session_store.go index ab2178a27f..55d4f89927 100644 --- a/store/storetest/upload_session_store.go +++ b/store/storetest/upload_session_store.go @@ -4,6 +4,7 @@ package storetest import ( + "context" "testing" "time" @@ -52,13 +53,13 @@ func testUploadSessionStoreSaveGet(t *testing.T, ss store.Store) { }) t.Run("getting non-existing session should fail", func(t *testing.T) { - us, err := ss.UploadSession().Get("fake") + us, err := ss.UploadSession().Get(context.Background(), "fake") require.Error(t, err) require.Nil(t, us) }) t.Run("getting existing session should succeed", func(t *testing.T) { - us, err := ss.UploadSession().Get(session.Id) + us, err := ss.UploadSession().Get(context.Background(), session.Id) require.NoError(t, err) require.NotNil(t, us) require.Equal(t, session, us) @@ -100,7 +101,7 @@ func testUploadSessionStoreUpdate(t *testing.T, ss store.Store) { err = ss.UploadSession().Update(us) require.NoError(t, err) - updated, err := ss.UploadSession().Get(us.Id) + updated, err := ss.UploadSession().Get(context.Background(), us.Id) require.NoError(t, err) require.NotNil(t, us) require.Equal(t, us, updated) @@ -199,7 +200,7 @@ func testUploadSessionStoreDelete(t *testing.T, ss store.Store) { err = ss.UploadSession().Delete(session.Id) require.NoError(t, err) - us, err = ss.UploadSession().Get(us.Id) + us, err = ss.UploadSession().Get(context.Background(), us.Id) require.Error(t, err) require.Nil(t, us) require.IsType(t, &store.ErrNotFound{}, err) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index c4c89b935c..55415d1c47 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -9549,10 +9549,10 @@ func (s *TimerLayerUploadSessionStore) Delete(id string) error { return err } -func (s *TimerLayerUploadSessionStore) Get(id string) (*model.UploadSession, error) { +func (s *TimerLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) { start := time.Now() - result, err := s.UploadSessionStore.Get(id) + result, err := s.UploadSessionStore.Get(ctx, id) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { From a8fa3f29e98246dff03f87c88699afccd6628d22 Mon Sep 17 00:00:00 2001 From: Nathaniel Allred Date: Tue, 13 Dec 2022 13:36:18 -0600 Subject: [PATCH 24/43] Self-hosted in-product purchase (#21804) * Self-hosted admins can purchase licenses in-app when `ServiceSettings,SelfHostedPurchase` is true (the default) * Content Security Policy enables loading assets from `js.stripe.com/v3` when `ServiceSettings.SelfHostedPurchase` is true (the default). * Add `hosted_customer` API subpath * Add status of SelfHostedPurchase to telemetry config report. * Support showing admins self-hosted invoices when `ServiceSettings.SelfHostedPurchase` is true (the default) Co-authored-by: Conor Macpherson <116016004+ConorMacpherson@users.noreply.github.com> --- api4/hosted_customer.go | 213 ++++++++++++++++++++++++++- api4/hosted_customer_test.go | 8 +- app/app_iface.go | 1 + app/hosted_customer.go | 21 +++ app/opentracing/opentracing_layer.go | 15 ++ einterfaces/cloud.go | 8 + einterfaces/mocks/CloudInterface.go | 127 ++++++++++++++++ i18n/en.json | 4 + model/client4.go | 66 +++++++++ model/cloud.go | 19 +-- model/config.go | 6 +- model/hosted_customer.go | 58 ++++++++ model/websocket_message.go | 1 + services/telemetry/telemetry.go | 2 +- web/handlers.go | 2 +- web/handlers_test.go | 29 +++- 16 files changed, 551 insertions(+), 29 deletions(-) create mode 100644 app/hosted_customer.go create mode 100644 model/hosted_customer.go diff --git a/api4/hosted_customer.go b/api4/hosted_customer.go index 6e696967c6..ae11514ae4 100644 --- a/api4/hosted_customer.go +++ b/api4/hosted_customer.go @@ -4,18 +4,35 @@ package api4 import ( + "bytes" + "encoding/binary" "encoding/json" + "fmt" + "io" "net/http" + "reflect" + "time" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/shared/mlog" + "github.com/mattermost/mattermost-server/v6/utils" ) // APIs for self-hosted workspaces to communicate with the backing customer & payments system. // Endpoints for cloud installations should not go in this file. func (api *API) InitHostedCustomer() { - + // POST /api/v4/hosted_customer/available + api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods("GET") // POST /api/v4/hosted_customer/bootstrap api.BaseRoutes.HostedCustomer.Handle("/bootstrap", api.APISessionRequired(selfHostedBootstrap)).Methods("POST") + // POST /api/v4/hosted_customer/customer + api.BaseRoutes.HostedCustomer.Handle("/customer", api.APISessionRequired(selfHostedCustomer)).Methods("POST") + // POST /api/v4/hosted_customer/confirm + api.BaseRoutes.HostedCustomer.Handle("/confirm", api.APISessionRequired(selfHostedConfirm)).Methods("POST") + // GET /api/v4/hosted_customer/invoices + api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET") + // GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf + api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET") } func ensureSelfHostedAdmin(c *Context, where string) { @@ -32,21 +49,22 @@ func ensureSelfHostedAdmin(c *Context, where string) { } } -func checkSelfHostedFirstTimePurchaseEnabled(c *Context) bool { +func checkSelfHostedPurchaseEnabled(c *Context) bool { config := c.App.Config() if config == nil { return false } - enabled := config.ServiceSettings.SelfHostedFirstTimePurchase + enabled := config.ServiceSettings.SelfHostedPurchase return enabled != nil && *enabled } func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) { - where := "Api4.selfHostedBootstrap" - if !checkSelfHostedFirstTimePurchaseEnabled(c) { + const where = "Api4.selfHostedBootstrap" + if !checkSelfHostedPurchaseEnabled(c) { c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented) return } + reset := r.URL.Query().Get("reset") == "true" ensureSelfHostedAdmin(c, where) if c.Err != nil { return @@ -58,7 +76,7 @@ func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) { return } - signupProgress, err := c.App.Cloud().BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: user.Email}) + signupProgress, err := c.App.Cloud().BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: user.Email, Reset: reset}) if err != nil { c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError) return @@ -71,3 +89,186 @@ func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(json) } + +func selfHostedCustomer(c *Context, w http.ResponseWriter, r *http.Request) { + const where = "Api4.selfHostedCustomer" + ensureSelfHostedAdmin(c, where) + if c.Err != nil { + return + } + if !checkSelfHostedPurchaseEnabled(c) { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented) + return + } + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) + return + } + + var form *model.SelfHostedCustomerForm + if err = json.Unmarshal(bodyBytes, &form); err != nil { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) + return + } + + user, userErr := c.App.GetUser(c.AppContext.Session().UserId) + if userErr != nil { + c.Err = userErr + return + } + customerResponse, err := c.App.Cloud().CreateCustomerSelfHostedSignup(*form, user.Email) + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + json, err := json.Marshal(customerResponse) + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + w.Write(json) +} + +func selfHostedConfirm(c *Context, w http.ResponseWriter, r *http.Request) { + const where = "Api4.selfHostedConfirm" + ensureSelfHostedAdmin(c, where) + if c.Err != nil { + return + } + if !checkSelfHostedPurchaseEnabled(c) { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented) + return + } + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) + return + } + + var confirm model.SelfHostedConfirmPaymentMethodRequest + err = json.Unmarshal(bodyBytes, &confirm) + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err) + return + } + + user, userErr := c.App.GetUser(c.AppContext.Session().UserId) + if userErr != nil { + c.Err = userErr + return + } + confirmResponse, err := c.App.Cloud().ConfirmSelfHostedSignup(confirm, user.Email) + if err != nil { + if confirmResponse != nil { + c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id) + } + + if err.Error() == fmt.Sprintf("%d", http.StatusUnprocessableEntity) { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err) + return + } + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + license, err := c.App.Srv().Platform().SaveLicense([]byte(confirmResponse.License)) + // dealing with an AppError + if !(reflect.ValueOf(err).Kind() == reflect.Ptr && reflect.ValueOf(err).IsNil()) { + if confirmResponse != nil { + c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id) + } + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + clientResponse, err := json.Marshal(model.SelfHostedSignupConfirmClientResponse{ + License: utils.GetClientLicense(license), + Progress: confirmResponse.Progress, + }) + if err != nil { + if confirmResponse != nil { + c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id) + } + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + go func() { + err := c.App.Cloud().ConfirmSelfHostedSignupLicenseApplication() + if err != nil { + c.Logger.Warn("Unable to confirm license application", mlog.Err(err)) + } + }() + + _, _ = w.Write(clientResponse) +} + +func handleSignupAvailable(c *Context, w http.ResponseWriter, r *http.Request) { + const where = "Api4.handleSignupAvailable" + ensureSelfHostedAdmin(c, where) + if c.Err != nil { + return + } + if !checkSelfHostedPurchaseEnabled(c) { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented) + return + } + if err := c.App.Cloud().SelfHostedSignupAvailable(); err != nil { + c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented) + return + } + + ReturnStatusOK(w) +} + +func selfHostedInvoices(c *Context, w http.ResponseWriter, r *http.Request) { + const where = "Api4.selfHostedInvoices" + ensureSelfHostedAdmin(c, where) + if c.Err != nil { + return + } + + invoices, err := c.App.Cloud().GetSelfHostedInvoices() + + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + json, err := json.Marshal(invoices) + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + w.Write(json) +} + +func selfHostedInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) { + const where = "Api4.selfHostedInvoicePDF" + ensureSelfHostedAdmin(c, where) + if c.Err != nil { + return + } + + pdfData, filename, appErr := c.App.Cloud().GetSelfHostedInvoicePDF(c.Params.InvoiceId) + if appErr != nil { + c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError) + return + } + + writeFileResponse( + filename, + "application/pdf", + int64(binary.Size(pdfData)), + time.Now(), + *c.App.Config().ServiceSettings.WebserverMode, + bytes.NewReader(pdfData), + false, + w, + r, + ) +} diff --git a/api4/hosted_customer_test.go b/api4/hosted_customer_test.go index 6eff1e922d..dd895c5675 100644 --- a/api4/hosted_customer_test.go +++ b/api4/hosted_customer_test.go @@ -27,7 +27,7 @@ func TestSelfHostedBootstrap(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "false") defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE") - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valFalse }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valFalse }) th.App.ReloadConfig() _, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email}) @@ -45,7 +45,7 @@ func TestSelfHostedBootstrap(t *testing.T) { th.App.Srv().SetLicense(model.NewTestLicense("cloud")) os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true") defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE") - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue }) th.App.ReloadConfig() _, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email}) @@ -62,7 +62,7 @@ func TestSelfHostedBootstrap(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true") defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE") - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue }) th.App.ReloadConfig() _, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email}) @@ -79,7 +79,7 @@ func TestSelfHostedBootstrap(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true") defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE") - th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue }) + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue }) th.App.ReloadConfig() cloud := mocks.CloudInterface{} diff --git a/app/app_iface.go b/app/app_iface.go index 0465d8f916..44ea3d936a 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -916,6 +916,7 @@ type AppIface interface { Notification() einterfaces.NotificationInterface NotificationsLog() *mlog.Logger NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError + NotifySelfHostedSignupProgress(progress string, userId string) NotifySharedChannelUserUpdate(user *model.User) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError OriginChecker() func(*http.Request) bool diff --git a/app/hosted_customer.go b/app/hosted_customer.go new file mode 100644 index 0000000000..d27945e85d --- /dev/null +++ b/app/hosted_customer.go @@ -0,0 +1,21 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "github.com/mattermost/mattermost-server/v6/model" +) + +func (a *App) NotifySelfHostedSignupProgress(progress string, userId string) { + // this is an event only the relevant admin should receive. + // If there is no progress, there is nothing to report. + // If there is no userId, we do not want to mistakenly broadcast to all users. + if progress == "" || userId == "" { + return + } + message := model.NewWebSocketEvent(model.WebsocketEventHostedCustomerSignupProgressUpdated, "", "", userId, nil, "") + message.Add("progress", progress) + + a.Srv().Platform().Publish(message) +} diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 4e2f0fb783..1cea08e051 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -12632,6 +12632,21 @@ func (a *OpenTracingAppLayer) NotifyAndSetWarnMetricAck(warnMetricId string, sen return resultVar0 } +func (a *OpenTracingAppLayer) NotifySelfHostedSignupProgress(progress string, userId string) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySelfHostedSignupProgress") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + a.app.NotifySelfHostedSignupProgress(progress, userId) +} + func (a *OpenTracingAppLayer) NotifySessionsExpired() error { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySessionsExpired") diff --git a/einterfaces/cloud.go b/einterfaces/cloud.go index 46f5783225..d58e8c92dd 100644 --- a/einterfaces/cloud.go +++ b/einterfaces/cloud.go @@ -33,7 +33,15 @@ type CloudInterface interface { GetLicenseRenewalStatus(userID, token string) error InvalidateCaches() error + // hosted customer methods + SelfHostedSignupAvailable() error BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error) + CreateCustomerSelfHostedSignup(req model.SelfHostedCustomerForm, requesterEmail string) (*model.SelfHostedSignupCustomerResponse, error) + ConfirmSelfHostedSignup(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error) + ConfirmSelfHostedSignupLicenseApplication() error + GetSelfHostedInvoices() ([]*model.Invoice, error) + GetSelfHostedInvoicePDF(invoiceID string) ([]byte, string, error) + CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) HandleLicenseChange() error } diff --git a/einterfaces/mocks/CloudInterface.go b/einterfaces/mocks/CloudInterface.go index 141823c834..4b89135dc0 100644 --- a/einterfaces/mocks/CloudInterface.go +++ b/einterfaces/mocks/CloudInterface.go @@ -74,6 +74,43 @@ func (_m *CloudInterface) ConfirmCustomerPayment(userID string, confirmRequest * return r0 } +// ConfirmSelfHostedSignup provides a mock function with given fields: req, requesterEmail +func (_m *CloudInterface) ConfirmSelfHostedSignup(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error) { + ret := _m.Called(req, requesterEmail) + + var r0 *model.SelfHostedSignupConfirmResponse + if rf, ok := ret.Get(0).(func(model.SelfHostedConfirmPaymentMethodRequest, string) *model.SelfHostedSignupConfirmResponse); ok { + r0 = rf(req, requesterEmail) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.SelfHostedSignupConfirmResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(model.SelfHostedConfirmPaymentMethodRequest, string) error); ok { + r1 = rf(req, requesterEmail) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConfirmSelfHostedSignupLicenseApplication provides a mock function with given fields: +func (_m *CloudInterface) ConfirmSelfHostedSignupLicenseApplication() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + // CreateCustomerPayment provides a mock function with given fields: userID func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error) { ret := _m.Called(userID) @@ -97,6 +134,29 @@ func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSet return r0, r1 } +// CreateCustomerSelfHostedSignup provides a mock function with given fields: req, requesterEmail +func (_m *CloudInterface) CreateCustomerSelfHostedSignup(req model.SelfHostedCustomerForm, requesterEmail string) (*model.SelfHostedSignupCustomerResponse, error) { + ret := _m.Called(req, requesterEmail) + + var r0 *model.SelfHostedSignupCustomerResponse + if rf, ok := ret.Get(0).(func(model.SelfHostedCustomerForm, string) *model.SelfHostedSignupCustomerResponse); ok { + r0 = rf(req, requesterEmail) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.SelfHostedSignupCustomerResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(model.SelfHostedCustomerForm, string) error); ok { + r1 = rf(req, requesterEmail) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateOrUpdateSubscriptionHistoryEvent provides a mock function with given fields: userID, userCount func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) { ret := _m.Called(userID, userCount) @@ -279,6 +339,59 @@ func (_m *CloudInterface) GetLicenseRenewalStatus(userID string, token string) e return r0 } +// GetSelfHostedInvoicePDF provides a mock function with given fields: invoiceID +func (_m *CloudInterface) GetSelfHostedInvoicePDF(invoiceID string) ([]byte, string, error) { + ret := _m.Called(invoiceID) + + var r0 []byte + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(invoiceID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 string + if rf, ok := ret.Get(1).(func(string) string); ok { + r1 = rf(invoiceID) + } else { + r1 = ret.Get(1).(string) + } + + var r2 error + if rf, ok := ret.Get(2).(func(string) error); ok { + r2 = rf(invoiceID) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetSelfHostedInvoices provides a mock function with given fields: +func (_m *CloudInterface) GetSelfHostedInvoices() ([]*model.Invoice, error) { + ret := _m.Called() + + var r0 []*model.Invoice + if rf, ok := ret.Get(0).(func() []*model.Invoice); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Invoice) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetSelfHostedProducts provides a mock function with given fields: userID func (_m *CloudInterface) GetSelfHostedProducts(userID string) ([]*model.Product, error) { ret := _m.Called(userID) @@ -376,6 +489,20 @@ func (_m *CloudInterface) RequestCloudTrial(userID string, subscriptionID string return r0, r1 } +// SelfHostedSignupAvailable provides a mock function with given fields: +func (_m *CloudInterface) SelfHostedSignupAvailable() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) { ret := _m.Called(userID, customerInfo) diff --git a/i18n/en.json b/i18n/en.json index d1399190f6..25f8a93b61 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2543,6 +2543,10 @@ "id": "api.scheme.patch_scheme.license.error", "translation": "Your license does not support update permissions schemes" }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portal unavailable for self-hosted signup." + }, { "id": "api.server.license_up_for_renewal.error_generating_link", "translation": "Failed to generate the license renewal link" diff --git a/model/client4.go b/model/client4.go index 511bc44a3f..d52df99214 100644 --- a/model/client4.go +++ b/model/client4.go @@ -8558,6 +8558,72 @@ func (c *Client4) GetNewTeamMembersSince(teamID string, timeRange string, page i return newTeamMembersList, BuildResponse(r), nil } +func (c *Client4) SelfHostedSignupAvailable() (*Response, error) { + r, err := c.DoAPIGet(c.hostedCustomerRoute()+"/signup_available", "") + + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + + return BuildResponse(r), nil +} + +func (c *Client4) SelfHostedSignupCustomer(form *SelfHostedCustomerForm) (*Response, *SelfHostedSignupCustomerResponse, error) { + payloadBytes, err := json.Marshal(form) + if err != nil { + return nil, nil, NewAppError("SelfHostedSignupCustomer", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + r, err := c.DoAPIPost(c.hostedCustomerRoute()+"/customer", string(payloadBytes)) + + if err != nil { + return BuildResponse(r), nil, err + } + data, err := io.ReadAll(r.Body) + if err != nil { + return BuildResponse(r), nil, err + } + defer closeBody(r) + + response := SelfHostedSignupCustomerResponse{} + err = json.Unmarshal(data, &response) + if err != nil { + return BuildResponse(r), nil, err + } + + return BuildResponse(r), &response, nil +} + +func (c *Client4) SelfHostedSignupConfirm(form *SelfHostedConfirmPaymentMethodRequest) (*Response, *SelfHostedSignupConfirmClientResponse, error) { + payloadBytes, err := json.Marshal(form) + if err != nil { + return nil, nil, NewAppError("SelfHostedSignupConfirm", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + r, err := c.DoAPIPost(c.hostedCustomerRoute()+"/confirm", string(payloadBytes)) + + if err != nil { + return BuildResponse(r), nil, err + } + + data, err := io.ReadAll(r.Body) + if err != nil { + return BuildResponse(r), nil, err + } + defer closeBody(r) + + response := SelfHostedSignupConfirmClientResponse{} + err = json.Unmarshal(data, &response) + if err != nil { + return BuildResponse(r), nil, err + } + + defer closeBody(r) + + return BuildResponse(r), &response, nil +} + func (c *Client4) GetPostInfo(postId string) (*PostInfo, *Response, error) { r, err := c.DoAPIGet(c.postRoute(postId)+"/info", "") if err != nil { diff --git a/model/cloud.go b/model/cloud.go index bdbc39bfe7..7feae13e3d 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -290,17 +290,14 @@ type ProductLimits struct { Teams *TeamsLimits `json:"teams,omitempty"` } -type BootstrapSelfHostedSignupRequest struct { - Email string `json:"email"` -} - -type BootstrapSelfHostedSignupResponse struct { - Progress string `json:"progress"` -} - -type BootstrapSelfHostedSignupResponseInternal struct { - Progress string `json:"progress"` - License string `json:"license"` +// CreateSubscriptionRequest is the parameters for the API request to create a subscription. +type CreateSubscriptionRequest struct { + ProductID string `json:"product_id"` + AddOns []string `json:"add_ons"` + Seats int `json:"seats"` + Total float64 `json:"total"` + InternalPurchaseOrder string `json:"internal_purchase_order"` + DiscountID string `json:"discount_id"` } func (p *Product) IsYearly() bool { diff --git a/model/config.go b/model/config.go index 7b9913e976..7f9c807562 100644 --- a/model/config.go +++ b/model/config.go @@ -383,7 +383,7 @@ type ServiceSettings struct { CollapsedThreads *string `access:"experimental_features"` ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` EnableCustomGroups *bool `access:"site_users_and_teams"` - SelfHostedFirstTimePurchase *bool `access:"write_restrictable,cloud_restrictable"` + SelfHostedPurchase *bool `access:"write_restrictable,cloud_restrictable"` AllowSyncedDrafts *bool `access:"site_posts"` } @@ -854,8 +854,8 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { s.AllowSyncedDrafts = NewBool(true) } - if s.SelfHostedFirstTimePurchase == nil { - s.SelfHostedFirstTimePurchase = NewBool(false) + if s.SelfHostedPurchase == nil { + s.SelfHostedPurchase = NewBool(true) } } diff --git a/model/hosted_customer.go b/model/hosted_customer.go new file mode 100644 index 0000000000..0e1373a2bc --- /dev/null +++ b/model/hosted_customer.go @@ -0,0 +1,58 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type BootstrapSelfHostedSignupRequest struct { + Email string `json:"email"` + Reset bool `json:"reset"` +} + +type BootstrapSelfHostedSignupResponse struct { + Progress string `json:"progress"` +} + +type BootstrapSelfHostedSignupResponseInternal struct { + Progress string `json:"progress"` + License string `json:"license"` +} + +// email contained in token, so not in the request body. +type SelfHostedCustomerForm struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + BillingAddress *Address `json:"billing_address"` + Organization string `json:"organization"` +} + +type SelfHostedConfirmPaymentMethodRequest struct { + StripeSetupIntentID string `json:"stripe_setup_intent_id"` + Subscription CreateSubscriptionRequest `json:"subscription"` +} + +// SelfHostedSignupPaymentResponse contains feels needed for self hosted signup to confirm payment and receive license. +type SelfHostedSignupCustomerResponse struct { + CustomerId string `json:"customer_id"` + SetupIntentId string `json:"setup_intent_id"` + SetupIntentSecret string `json:"setup_intent_secret"` + Progress string `json:"progress"` +} + +// SelfHostedSignupConfirmResponse contains data received on successful self hosted signup +type SelfHostedSignupConfirmResponse struct { + License string `json:"license"` + Progress string `json:"progress"` +} + +type SelfHostedSignupConfirmClientResponse struct { + License map[string]string `json:"license"` + Progress string `json:"progress"` +} + +type SelfHostedBillingAccessRequest struct { + LicenseId string `json:"license_id"` +} + +type SelfHostedBillingAccessResponse struct { + Token string `json:"token"` +} diff --git a/model/websocket_message.go b/model/websocket_message.go index 1ad2a3b8b0..f519464d13 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -81,6 +81,7 @@ const ( WebsocketEventDraftDeleted = "draft_deleted" WebsocketEventAcknowledgementAdded = "post_acknowledgement_added" WebsocketEventAcknowledgementRemoved = "post_acknowledgement_removed" + WebsocketEventHostedCustomerSignupProgressUpdated = "hosted_customer_signup_progress_updated" ) type WebSocketMessage interface { diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index 9db3a36d00..71820f0d97 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -455,7 +455,7 @@ func (ts *TelemetryService) trackConfig() { "restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""), "enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups, "post_priority": *cfg.ServiceSettings.PostPriority, - "self_hosted_first_time_purchase": *cfg.ServiceSettings.SelfHostedFirstTimePurchase, + "self_hosted_purchase": *cfg.ServiceSettings.SelfHostedPurchase, "allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts, }) diff --git a/web/handlers.go b/web/handlers.go index 798831bc73..333e3906e5 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -236,7 +236,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } cloudCSP := "" - if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedFirstTimePurchase { + if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedPurchase { cloudCSP = " js.stripe.com/v3" } diff --git a/web/handlers_test.go b/web/handlers_test.go index e04be652da..ba49272c49 100644 --- a/web/handlers_test.go +++ b/web/handlers_test.go @@ -298,6 +298,29 @@ func TestHandlerServeCSPHeader(t *testing.T) { IsStatic: true, } + request := httptest.NewRequest("POST", "/", nil) + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + assert.Equal(t, 200, response.Code) + assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) + }) + + t.Run("static, without subpath or SelfHostedPurchase, does not allow Stripe in CSP", func(t *testing.T) { + th := Setup(t).InitBasic() + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SelfHostedPurchase = false }) + defer th.TearDown() + + web := New(th.Server) + + handler := Handler{ + Srv: web.srv, + HandleFunc: handlerForCSPHeader, + RequireSession: false, + TrustRequester: false, + RequireMfa: false, + IsStatic: true, + } + request := httptest.NewRequest("POST", "/", nil) response := httptest.NewRecorder() handler.ServeHTTP(response, request) @@ -343,7 +366,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) // TODO: It's hard to unit test this now that the CSP directive is effectively // decided in Setup(). Circle back to this in master once the memory store is @@ -358,7 +381,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response = httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) // TODO: See above. // assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed") }) @@ -388,7 +411,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline' http://localhost:9006"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3 'unsafe-eval' 'unsafe-inline' http://localhost:9006"}, response.Header()["Content-Security-Policy"]) }) } From 61317883bf8593104076643117ce5d486761919e Mon Sep 17 00:00:00 2001 From: Tim Scheuermann Date: Tue, 13 Dec 2022 20:36:40 +0100 Subject: [PATCH 25/43] Allow S3 uploads without a timeout for exports (#21774) --- app/app_iface.go | 1 + app/file.go | 28 +++++++++ app/opentracing/opentracing_layer.go | 22 +++++++ jobs/export_process/worker.go | 5 +- shared/filestore/filesstore_test.go | 88 ++++++++++++++++++++++++++++ shared/filestore/s3store.go | 29 ++++++--- 6 files changed, 163 insertions(+), 10 deletions(-) diff --git a/app/app_iface.go b/app/app_iface.go index 44ea3d936a..348e10e95d 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -1161,4 +1161,5 @@ type AppIface interface { VerifyUserEmail(userID, email string) *model.AppError ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) WriteFile(fr io.Reader, path string) (int64, *model.AppError) + WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) } diff --git a/app/file.go b/app/file.go index 06a516bc86..a47e817003 100644 --- a/app/file.go +++ b/app/file.go @@ -161,6 +161,10 @@ func (a *App) MoveFile(oldPath, newPath string) *model.AppError { return nil } +func (a *App) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) { + return a.Srv().writeFileContext(ctx, fr, path) +} + func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) { return a.Srv().writeFile(fr, path) } @@ -173,6 +177,30 @@ func (s *Server) writeFile(fr io.Reader, path string) (int64, *model.AppError) { return result, nil } +func (s *Server) writeFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) { + type ContextWriter interface { + WriteFileContext(context.Context, io.Reader, string) (int64, error) + } + + var ( + fileBackend = s.FileBackend() + written int64 + err error + ) + + // Check if we can provide a custom context, otherwise just use the default method. + if cw, ok := fileBackend.(ContextWriter); ok { + written, err = cw.WriteFileContext(ctx, fr, path) + } else { + written, err = fileBackend.WriteFile(fr, path) + } + if err != nil { + return written, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + return written, nil +} + func (a *App) AppendFile(fr io.Reader, path string) (int64, *model.AppError) { result, nErr := a.FileBackend().AppendFile(fr, path) if nErr != nil { diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 1cea08e051..30fc5b6d92 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -18514,6 +18514,28 @@ func (a *OpenTracingAppLayer) WriteFile(fr io.Reader, path string) (int64, *mode return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.WriteFileContext") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.WriteFileContext(ctx, fr, path) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func NewOpenTracingAppLayer(childApp app.AppIface, ctx context.Context) *OpenTracingAppLayer { newApp := OpenTracingAppLayer{ app: childApp, diff --git a/jobs/export_process/worker.go b/jobs/export_process/worker.go index a9deaa8634..2697b65980 100644 --- a/jobs/export_process/worker.go +++ b/jobs/export_process/worker.go @@ -4,6 +4,7 @@ package export_process import ( + "context" "io" "path/filepath" @@ -19,6 +20,7 @@ const jobName = "ExportProcess" type AppIface interface { configservice.ConfigService WriteFile(fr io.Reader, path string) (int64, *model.AppError) + WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts model.BulkExportOpts) *model.AppError Log() *mlog.Logger } @@ -45,7 +47,8 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { errCh := make(chan *model.AppError, 1) go func() { defer close(errCh) - _, appErr := app.WriteFile(rd, filepath.Join(outPath, exportFilename)) + // Try to write without a timeout + _, appErr := app.WriteFileContext(context.Background(), rd, filepath.Join(outPath, exportFilename)) errCh <- appErr }() diff --git a/shared/filestore/filesstore_test.go b/shared/filestore/filesstore_test.go index 9bc9e281d7..c17836558d 100644 --- a/shared/filestore/filesstore_test.go +++ b/shared/filestore/filesstore_test.go @@ -5,9 +5,12 @@ package filestore import ( "bytes" + "context" "fmt" + "io" "math/rand" "os" + "strings" "testing" "time" @@ -121,6 +124,91 @@ func (s *FileBackendTestSuite) TestReadWriteFile() { s.EqualValues(readString, "test") } +func (s *FileBackendTestSuite) TestReadWriteFileContext() { + type ContextWriter interface { + WriteFileContext(context.Context, io.Reader, string) (int64, error) + } + + data := "test" + + s.T().Run("no deadline", func(t *testing.T) { + var ( + written int64 + err error + ) + + path := "tests/" + randomString() + + ctx := context.Background() + if cw, ok := s.backend.(ContextWriter); ok { + written, err = cw.WriteFileContext(ctx, strings.NewReader(data), path) + } else { + written, err = s.backend.WriteFile(strings.NewReader(data), path) + } + s.NoError(err) + s.EqualValues(len(data), written, "expected given number of bytes to have been written") + defer s.backend.RemoveFile(path) + + read, err := s.backend.ReadFile(path) + s.NoError(err) + + readString := string(read) + s.Equal(readString, data) + }) + + s.T().Run("long deadline", func(t *testing.T) { + var ( + written int64 + err error + ) + + path := "tests/" + randomString() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if cw, ok := s.backend.(ContextWriter); ok { + written, err = cw.WriteFileContext(ctx, strings.NewReader(data), path) + } else { + written, err = s.backend.WriteFile(strings.NewReader(data), path) + } + s.NoError(err) + s.EqualValues(len(data), written, "expected given number of bytes to have been written") + defer s.backend.RemoveFile(path) + + read, err := s.backend.ReadFile(path) + s.NoError(err) + + readString := string(read) + s.Equal(readString, data) + }) + + s.T().Run("missed deadline", func(t *testing.T) { + var ( + written int64 + err error + ) + + path := "tests/" + randomString() + + r, w := io.Pipe() + go func() { + // close the writer after a short time + time.Sleep(500 * time.Millisecond) + w.Close() + }() + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + if cw, ok := s.backend.(ContextWriter); ok { + written, err = cw.WriteFileContext(ctx, r, path) + } else { + // this test works only with a context writer + return + } + s.Error(err) + s.Zero(written) + }) +} + func (s *FileBackendTestSuite) TestReadWriteFileImage() { b := []byte("testimage") path := "tests/" + randomString() + ".png" diff --git a/shared/filestore/s3store.go b/shared/filestore/s3store.go index 60132cd830..3dcbbe9b1a 100644 --- a/shared/filestore/s3store.go +++ b/shared/filestore/s3store.go @@ -369,6 +369,13 @@ func (b *S3FileBackend) MoveFile(oldPath, newPath string) error { } func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + + return b.WriteFileContext(ctx, fr, path) +} + +func (b *S3FileBackend) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, error) { var contentType string path = filepath.Join(b.pathPrefix, path) if ext := filepath.Ext(path); isFileExtImage(ext) { @@ -377,22 +384,26 @@ func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, error) { contentType = "binary/octet-stream" } - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) - defer cancel() options := s3PutOptions(b.encrypt, contentType) - objSize := -1 + objSize := int64(-1) isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != "" if isCloud { options.DisableContentSha256 = true - } - // We pass an object size only in situations where bifrost is not - // used. Bifrost needs to run in HTTPS, which is not yet deployed. - if buf, ok := fr.(*bytes.Buffer); ok && !isCloud { - objSize = buf.Len() + } else { + // We pass an object size only in situations where bifrost is not + // used. Bifrost needs to run in HTTPS, which is not yet deployed. + switch t := fr.(type) { + case *bytes.Buffer: + objSize = int64(t.Len()) + case *os.File: + if s, err := t.Stat(); err == nil { + objSize = s.Size() + } + } } - info, err := b.client.PutObject(ctx, b.bucket, path, fr, int64(objSize), options) + info, err := b.client.PutObject(ctx, b.bucket, path, fr, objSize, options) if err != nil { return info.Size, errors.Wrapf(err, "unable write the data in the file %s", path) } From 8d90c7042f93fc8d4d30e973d79c59e6973c2c1b Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 14 Dec 2022 15:24:04 +0300 Subject: [PATCH 26/43] [MM-48883] Remove app package dependency for products (#21853) --- app/channels.go | 53 ++++++++++--------- app/license.go | 4 +- app/notification_push_test.go | 14 +++--- app/product.go | 24 ++------- app/product_test.go | 95 ++++++++++++++++++----------------- app/server.go | 57 +++++++-------------- product/product.go | 24 +++++++++ product/service.go | 28 +++++++++++ 8 files changed, 162 insertions(+), 137 deletions(-) create mode 100644 product/product.go create mode 100644 product/service.go diff --git a/app/channels.go b/app/channels.go index e4a25f3749..31eca3a2ea 100644 --- a/app/channels.go +++ b/app/channels.go @@ -23,6 +23,8 @@ import ( "github.com/mattermost/mattermost-server/v6/shared/mlog" ) +const ServerKey product.ServiceKey = "server" + // licenseSvc is added to act as a starting point for future integrated products. // It has the same signature and functionality with the license related APIs of the plugin-api. type licenseSvc interface { @@ -86,19 +88,24 @@ type Channels struct { } func init() { - RegisterProduct("channels", ProductManifest{ - Initializer: func(s *Server, services map[ServiceKey]any) (Product, error) { - return NewChannels(s, services) + product.RegisterProduct("channels", product.Manifest{ + Initializer: func(services map[product.ServiceKey]any) (product.Product, error) { + return NewChannels(services) }, - Dependencies: map[ServiceKey]struct{}{ - ConfigKey: {}, - LicenseKey: {}, - FilestoreKey: {}, + Dependencies: map[product.ServiceKey]struct{}{ + ServerKey: {}, + product.ConfigKey: {}, + product.LicenseKey: {}, + product.FilestoreKey: {}, }, }) } -func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { +func NewChannels(services map[product.ServiceKey]any) (*Channels, error) { + s, ok := services[ServerKey].(*Server) + if !ok { + return nil, errors.New("server not passed") + } ch := &Channels{ srv: s, imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()), @@ -112,10 +119,10 @@ func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { // 2. Add the field to *Channels // 3. Add the service key to the slice. // 4. Add a new case in the switch statement. - requiredServices := []ServiceKey{ - ConfigKey, - LicenseKey, - FilestoreKey, + requiredServices := []product.ServiceKey{ + product.ConfigKey, + product.LicenseKey, + product.FilestoreKey, } for _, svcKey := range requiredServices { svc, ok := services[svcKey] @@ -124,19 +131,19 @@ func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { } switch svcKey { // Keep adding more services here - case ConfigKey: + case product.ConfigKey: cfgSvc, ok := svc.(product.ConfigService) if !ok { return nil, errors.New("Config service did not satisfy ConfigSvc interface") } ch.cfgSvc = cfgSvc - case FilestoreKey: + case product.FilestoreKey: filestore, ok := svc.(filestore.FileBackend) if !ok { return nil, errors.New("Filestore service did not satisfy FileBackend interface") } ch.filestore = filestore - case LicenseKey: + case product.LicenseKey: svc, ok := svc.(licenseSvc) if !ok { return nil, errors.New("License service did not satisfy licenseSvc interface") @@ -198,7 +205,7 @@ func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { } ch.routerSvc = newRouterService() - services[RouterKey] = ch.routerSvc + services[product.RouterKey] = ch.routerSvc // Setup routes. pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() @@ -206,29 +213,29 @@ func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest) pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest) - services[PostKey] = &postServiceWrapper{ + services[product.PostKey] = &postServiceWrapper{ app: &App{ch: ch}, } - services[PermissionsKey] = &permissionsServiceWrapper{ + services[product.PermissionsKey] = &permissionsServiceWrapper{ app: &App{ch: ch}, } - services[TeamKey] = &teamServiceWrapper{ + services[product.TeamKey] = &teamServiceWrapper{ app: &App{ch: ch}, } - services[BotKey] = &botServiceWrapper{ + services[product.BotKey] = &botServiceWrapper{ app: &App{ch: ch}, } - services[HooksKey] = &hooksService{ + services[product.HooksKey] = &hooksService{ ch: ch, } - services[UserKey] = &App{ch: ch} + services[product.UserKey] = &App{ch: ch} - services[PreferencesKey] = &preferencesServiceWrapper{ + services[product.PreferencesKey] = &preferencesServiceWrapper{ app: &App{ch: ch}, } diff --git a/app/license.go b/app/license.go index 9afff64349..61b4d2dc85 100644 --- a/app/license.go +++ b/app/license.go @@ -32,8 +32,8 @@ type licenseWrapper struct { srv *Server } -func (w *licenseWrapper) Name() ServiceKey { - return LicenseKey +func (w *licenseWrapper) Name() product.ServiceKey { + return product.LicenseKey } func (w *licenseWrapper) GetLicense() *model.License { diff --git a/app/notification_push_test.go b/app/notification_push_test.go index 73bf38e72b..7c8a6d78c1 100644 --- a/app/notification_push_test.go +++ b/app/notification_push_test.go @@ -20,6 +20,7 @@ import ( "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/product" fmocks "github.com/mattermost/mattermost-server/v6/shared/filestore/mocks" "github.com/mattermost/mattermost-server/v6/shared/i18n" "github.com/mattermost/mattermost-server/v6/store/storetest/mocks" @@ -1440,7 +1441,7 @@ func TestPushNotificationRace(t *testing.T) { Return(&model.Preference{Value: "test"}, nil) mockStore.On("Preference").Return(&mockPreferenceStore) s := &Server{ - products: make(map[string]Product), + products: make(map[string]product.Product), Router: mux.NewRouter(), } var err error @@ -1449,12 +1450,13 @@ func TestPushNotificationRace(t *testing.T) { }, platform.SetFileStore(&fmocks.FileBackend{})) s.SetStore(mockStore) require.NoError(t, err) - serviceMap := map[ServiceKey]any{ - ConfigKey: s.platform, - LicenseKey: &licenseWrapper{s}, - FilestoreKey: s.FileBackend(), + serviceMap := map[product.ServiceKey]any{ + ServerKey: s, + product.ConfigKey: s.platform, + product.LicenseKey: &licenseWrapper{s}, + product.FilestoreKey: s.FileBackend(), } - ch, err := NewChannels(s, serviceMap) + ch, err := NewChannels(serviceMap) require.NoError(t, err) s.products["channels"] = ch diff --git a/app/product.go b/app/product.go index a8c15a9e0a..9183e40ea8 100644 --- a/app/product.go +++ b/app/product.go @@ -6,27 +6,13 @@ package app import ( "fmt" "strings" + + "github.com/mattermost/mattermost-server/v6/product" ) -type Product interface { - Start() error - Stop() error -} - -type ProductManifest struct { - Initializer func(*Server, map[ServiceKey]any) (Product, error) - Dependencies map[ServiceKey]struct{} -} - -var products = make(map[string]ProductManifest) - -func RegisterProduct(name string, m ProductManifest) { - products[name] = m -} - func (s *Server) initializeProducts( - productMap map[string]ProductManifest, - serviceMap map[ServiceKey]any, + productMap map[string]product.Manifest, + serviceMap map[product.ServiceKey]any, ) error { // create a product map to consume pmap := make(map[string]struct{}) @@ -57,7 +43,7 @@ func (s *Server) initializeProducts( // some products can register themselves/their services initializer := manifest.Initializer - prod, err := initializer(s, serviceMap) + prod, err := initializer(serviceMap) if err != nil { return fmt.Errorf("error initializing product %q: %w", product, err) } diff --git a/app/product_test.go b/app/product_test.go index bafba19cbd..66d0ba6dcf 100644 --- a/app/product_test.go +++ b/app/product_test.go @@ -6,6 +6,7 @@ package app import ( "testing" + "github.com/mattermost/mattermost-server/v6/product" "github.com/stretchr/testify/require" ) @@ -16,7 +17,7 @@ const ( type productA struct{} -func newProductA(s *Server, m map[ServiceKey]any) (Product, error) { +func newProductA(m map[product.ServiceKey]any) (product.Product, error) { m[testSrvKey1] = nil return &productA{}, nil } @@ -26,7 +27,7 @@ func (p *productA) Stop() error { return nil } type productB struct{} -func newProductB(s *Server, m map[ServiceKey]any) (Product, error) { +func newProductB(m map[product.ServiceKey]any) (product.Product, error) { m[testSrvKey2] = nil return &productB{}, nil } @@ -36,35 +37,35 @@ func (p *productB) Stop() error { return nil } func TestInitializeProducts(t *testing.T) { t.Run("2 products and no circular dependency", func(t *testing.T) { - serviceMap := map[ServiceKey]any{ - ConfigKey: nil, - LicenseKey: nil, - FilestoreKey: nil, - ClusterKey: nil, + serviceMap := map[product.ServiceKey]any{ + product.ConfigKey: nil, + product.LicenseKey: nil, + product.FilestoreKey: nil, + product.ClusterKey: nil, } - products := map[string]ProductManifest{ + products := map[string]product.Manifest{ "productA": { Initializer: newProductA, - Dependencies: map[ServiceKey]struct{}{ - ConfigKey: {}, - LicenseKey: {}, - FilestoreKey: {}, - ClusterKey: {}, + Dependencies: map[product.ServiceKey]struct{}{ + product.ConfigKey: {}, + product.LicenseKey: {}, + product.FilestoreKey: {}, + product.ClusterKey: {}, }, }, "productB": { Initializer: newProductB, - Dependencies: map[ServiceKey]struct{}{ - ConfigKey: {}, - testSrvKey1: {}, - FilestoreKey: {}, - ClusterKey: {}, + Dependencies: map[product.ServiceKey]struct{}{ + product.ConfigKey: {}, + testSrvKey1: {}, + product.FilestoreKey: {}, + product.ClusterKey: {}, }, }, } server := &Server{ - products: make(map[string]Product), + products: make(map[string]product.Product), } err := server.initializeProducts(products, serviceMap) @@ -73,36 +74,36 @@ func TestInitializeProducts(t *testing.T) { }) t.Run("2 products and circular dependency", func(t *testing.T) { - serviceMap := map[ServiceKey]any{ - ConfigKey: nil, - LicenseKey: nil, - FilestoreKey: nil, - ClusterKey: nil, + serviceMap := map[product.ServiceKey]any{ + product.ConfigKey: nil, + product.LicenseKey: nil, + product.FilestoreKey: nil, + product.ClusterKey: nil, } - products := map[string]ProductManifest{ + products := map[string]product.Manifest{ "productA": { Initializer: newProductA, - Dependencies: map[ServiceKey]struct{}{ - ConfigKey: {}, - LicenseKey: {}, - FilestoreKey: {}, - ClusterKey: {}, - testSrvKey2: {}, + Dependencies: map[product.ServiceKey]struct{}{ + product.ConfigKey: {}, + product.LicenseKey: {}, + product.FilestoreKey: {}, + product.ClusterKey: {}, + testSrvKey2: {}, }, }, "productB": { Initializer: newProductB, - Dependencies: map[ServiceKey]struct{}{ - ConfigKey: {}, - testSrvKey1: {}, - FilestoreKey: {}, - ClusterKey: {}, + Dependencies: map[product.ServiceKey]struct{}{ + product.ConfigKey: {}, + testSrvKey1: {}, + product.FilestoreKey: {}, + product.ClusterKey: {}, }, }, } server := &Server{ - products: make(map[string]Product), + products: make(map[string]product.Product), } err := server.initializeProducts(products, serviceMap) @@ -110,19 +111,19 @@ func TestInitializeProducts(t *testing.T) { }) t.Run("2 products and one w/o any dependency", func(t *testing.T) { - serviceMap := map[ServiceKey]any{ - ConfigKey: nil, - LicenseKey: nil, - FilestoreKey: nil, - ClusterKey: nil, + serviceMap := map[product.ServiceKey]any{ + product.ConfigKey: nil, + product.LicenseKey: nil, + product.FilestoreKey: nil, + product.ClusterKey: nil, } - products := map[string]ProductManifest{ + products := map[string]product.Manifest{ "productA": { Initializer: newProductA, - Dependencies: map[ServiceKey]struct{}{ - ConfigKey: {}, - LicenseKey: {}, + Dependencies: map[product.ServiceKey]struct{}{ + product.ConfigKey: {}, + product.LicenseKey: {}, }, }, "productB": { @@ -130,7 +131,7 @@ func TestInitializeProducts(t *testing.T) { }, } server := &Server{ - products: make(map[string]Product), + products: make(map[string]product.Product), } err := server.initializeProducts(products, serviceMap) diff --git a/app/server.go b/app/server.go index d39e66705c..fbd433a6db 100644 --- a/app/server.go +++ b/app/server.go @@ -75,30 +75,6 @@ import ( // declaring this as var to allow overriding in tests var SentryDSN = "placeholder_sentry_dsn" -type ServiceKey string - -const ( - ChannelKey ServiceKey = "channel" - ConfigKey ServiceKey = "config" - LicenseKey ServiceKey = "license" - FilestoreKey ServiceKey = "filestore" - FileInfoStoreKey ServiceKey = "fileinfostore" - ClusterKey ServiceKey = "cluster" - CloudKey ServiceKey = "cloud" - PostKey ServiceKey = "post" - TeamKey ServiceKey = "team" - UserKey ServiceKey = "user" - PermissionsKey ServiceKey = "permissions" - RouterKey ServiceKey = "router" - BotKey ServiceKey = "bot" - LogKey ServiceKey = "log" - HooksKey ServiceKey = "hooks" - KVStoreKey ServiceKey = "kvstore" - StoreKey ServiceKey = "storekey" - SystemKey ServiceKey = "systemkey" - PreferencesKey ServiceKey = "preferenceskey" -) - type Server struct { // RootRouter is the starting point for all HTTP requests to the server. RootRouter *mux.Router @@ -160,7 +136,7 @@ type Server struct { tracer *tracing.Tracer - products map[string]Product + products map[string]product.Product hooksManager *product.HooksManager } @@ -187,7 +163,7 @@ func NewServer(options ...Option) (*Server, error) { RootRouter: rootRouter, LocalRouter: localRouter, timezones: timezones.New(), - products: make(map[string]Product), + products: make(map[string]product.Product), } for _, option := range options { @@ -262,24 +238,25 @@ func NewServer(options ...Option) (*Server, error) { // ensure app implements `product.UserService` var _ product.UserService = (*App)(nil) - serviceMap := map[ServiceKey]any{ - ChannelKey: &channelsWrapper{srv: s}, - ConfigKey: s.platform, - LicenseKey: s.licenseWrapper, - FilestoreKey: s.platform.FileBackend(), - FileInfoStoreKey: &fileInfoWrapper{srv: s}, - ClusterKey: s.platform, - UserKey: New(ServerConnector(s.Channels())), - LogKey: s.platform.Log(), - CloudKey: &cloudWrapper{cloud: s.Cloud}, - KVStoreKey: s.platform, - StoreKey: store.NewStoreServiceAdapter(s.Store()), - SystemKey: &systemServiceAdapter{server: s}, + serviceMap := map[product.ServiceKey]any{ + ServerKey: s, + product.ChannelKey: &channelsWrapper{srv: s}, + product.ConfigKey: s.platform, + product.LicenseKey: s.licenseWrapper, + product.FilestoreKey: s.platform.FileBackend(), + product.FileInfoStoreKey: &fileInfoWrapper{srv: s}, + product.ClusterKey: s.platform, + product.UserKey: New(ServerConnector(s.Channels())), + product.LogKey: s.platform.Log(), + product.CloudKey: &cloudWrapper{cloud: s.Cloud}, + product.KVStoreKey: s.platform, + product.StoreKey: store.NewStoreServiceAdapter(s.Store()), + product.SystemKey: &systemServiceAdapter{server: s}, } // Step 4: Initialize products. // Depends on s.httpService. - err = s.initializeProducts(products, serviceMap) + err = s.initializeProducts(product.GetProducts(), serviceMap) if err != nil { return nil, errors.Wrap(err, "failed to initialize products") } diff --git a/product/product.go b/product/product.go new file mode 100644 index 0000000000..df084ff015 --- /dev/null +++ b/product/product.go @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package product + +type Product interface { + Start() error + Stop() error +} + +type Manifest struct { + Initializer func(map[ServiceKey]any) (Product, error) + Dependencies map[ServiceKey]struct{} +} + +var products = make(map[string]Manifest) + +func RegisterProduct(name string, m Manifest) { + products[name] = m +} + +func GetProducts() map[string]Manifest { + return products +} diff --git a/product/service.go b/product/service.go new file mode 100644 index 0000000000..221fa1c168 --- /dev/null +++ b/product/service.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package product + +type ServiceKey string + +const ( + ChannelKey ServiceKey = "channel" + ConfigKey ServiceKey = "config" + LicenseKey ServiceKey = "license" + FilestoreKey ServiceKey = "filestore" + FileInfoStoreKey ServiceKey = "fileinfostore" + ClusterKey ServiceKey = "cluster" + CloudKey ServiceKey = "cloud" + PostKey ServiceKey = "post" + TeamKey ServiceKey = "team" + UserKey ServiceKey = "user" + PermissionsKey ServiceKey = "permissions" + RouterKey ServiceKey = "router" + BotKey ServiceKey = "bot" + LogKey ServiceKey = "log" + HooksKey ServiceKey = "hooks" + KVStoreKey ServiceKey = "kvstore" + StoreKey ServiceKey = "storekey" + SystemKey ServiceKey = "systemkey" + PreferencesKey ServiceKey = "preferenceskey" +) From f31380f5773c6cea2d6434fb656d012d5cc9b866 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Thu, 15 Dec 2022 12:29:33 +0300 Subject: [PATCH 27/43] product/hooks: hooks service no longer requires channels service start (#21861) --- app/channels.go | 4 ---- product/api.go | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/channels.go b/app/channels.go index 31eca3a2ea..513b02be08 100644 --- a/app/channels.go +++ b/app/channels.go @@ -325,10 +325,6 @@ type hooksService struct { } func (s *hooksService) RegisterHooks(productID string, hooks any) error { - if s.ch.pluginsEnvironment == nil { - return errors.New("could not find plugins environment") - } - return s.ch.srv.hooksManager.AddProduct(productID, hooks) } diff --git a/product/api.go b/product/api.go index 88eb9744e4..5067c20ce9 100644 --- a/product/api.go +++ b/product/api.go @@ -112,7 +112,7 @@ type ConfigService interface { } // HooksService is the API for adding exiting plugin hooks to the server so that they can be called as -// they were. This Service is required to be used after the products start. Otherwise it will return an error. +// they were. This Service is required to be accessed after the channels product initialized. // // The service shall be registered via app.HooksKey service key. type HooksService interface { From e10460675e257548a5189c5972de3997da433d80 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Thu, 15 Dec 2022 16:27:36 +0300 Subject: [PATCH 28/43] telemetry: add product hooks to daily telemetry (#21870) --- app/app_iface.go | 2 ++ app/channels.go | 4 ++++ app/opentracing/opentracing_layer.go | 18 ++++++++++++++++++ services/telemetry/mocks/ServerIface.go | 18 ++++++++++++++++++ services/telemetry/telemetry.go | 15 +++++++++++++++ services/telemetry/telemetry_test.go | 2 ++ 6 files changed, 59 insertions(+) diff --git a/app/app_iface.go b/app/app_iface.go index 348e10e95d..24f6871775 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -24,6 +24,7 @@ import ( "github.com/mattermost/mattermost-server/v6/einterfaces" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" + "github.com/mattermost/mattermost-server/v6/product" "github.com/mattermost/mattermost-server/v6/services/httpservice" "github.com/mattermost/mattermost-server/v6/services/imageproxy" "github.com/mattermost/mattermost-server/v6/services/remotecluster" @@ -868,6 +869,7 @@ type AppIface interface { HasPermissionToTeam(askingUserId string, teamID string, permission *model.Permission) bool HasPermissionToUser(askingUserId string, userID string) bool HasSharedChannel(channelID string) (bool, error) + HooksManager() *product.HooksManager ImageProxy() *imageproxy.ImageProxy ImageProxyAdder() func(string) string ImageProxyRemover() (f func(string) string) diff --git a/app/channels.go b/app/channels.go index 513b02be08..7ab024e45a 100644 --- a/app/channels.go +++ b/app/channels.go @@ -317,6 +317,10 @@ func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAcce receiveEmailsAccepted) } +func (a *App) HooksManager() *product.HooksManager { + return a.Srv().hooksManager +} + // Ensure hooksService implements `product.HooksService` var _ product.HooksService = (*hooksService)(nil) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 30fc5b6d92..c004aff52b 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -25,6 +25,7 @@ import ( "github.com/mattermost/mattermost-server/v6/einterfaces" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" + "github.com/mattermost/mattermost-server/v6/product" "github.com/mattermost/mattermost-server/v6/services/httpservice" "github.com/mattermost/mattermost-server/v6/services/imageproxy" "github.com/mattermost/mattermost-server/v6/services/remotecluster" @@ -11559,6 +11560,23 @@ func (a *OpenTracingAppLayer) HasSharedChannel(channelID string) (bool, error) { return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) HooksManager() *product.HooksManager { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HooksManager") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.HooksManager() + + return resultVar0 +} + func (a *OpenTracingAppLayer) HubRegister(webConn *platform.WebConn) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HubRegister") diff --git a/services/telemetry/mocks/ServerIface.go b/services/telemetry/mocks/ServerIface.go index c56cd87382..f06ce99181 100644 --- a/services/telemetry/mocks/ServerIface.go +++ b/services/telemetry/mocks/ServerIface.go @@ -13,6 +13,8 @@ import ( model "github.com/mattermost/mattermost-server/v6/model" plugin "github.com/mattermost/mattermost-server/v6/plugin" + + product "github.com/mattermost/mattermost-server/v6/product" ) // ServerIface is an autogenerated mock type for the ServerIface type @@ -118,6 +120,22 @@ func (_m *ServerIface) HTTPService() httpservice.HTTPService { return r0 } +// HooksManager provides a mock function with given fields: +func (_m *ServerIface) HooksManager() *product.HooksManager { + ret := _m.Called() + + var r0 *product.HooksManager + if rf, ok := ret.Get(0).(func() *product.HooksManager); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*product.HooksManager) + } + } + + return r0 +} + // IsLeader provides a mock function with given fields: func (_m *ServerIface) IsLeader() bool { ret := _m.Called() diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index 71820f0d97..ec88b868c0 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -15,6 +15,7 @@ import ( "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" + "github.com/mattermost/mattermost-server/v6/product" "github.com/mattermost/mattermost-server/v6/services/httpservice" "github.com/mattermost/mattermost-server/v6/services/marketplace" "github.com/mattermost/mattermost-server/v6/services/searchengine" @@ -91,6 +92,7 @@ type ServerIface interface { License() *model.License GetRoleByName(context.Context, string) (*model.Role, *model.AppError) GetSchemes(string, int, int) ([]*model.Scheme, *model.AppError) + HooksManager() *product.HooksManager } type TelemetryService struct { @@ -165,6 +167,7 @@ func (ts *TelemetryService) sendDailyTelemetry(override bool) { ts.trackGroups() ts.trackChannelModeration() ts.trackWarnMetrics() + ts.trackProducts() } } @@ -944,6 +947,18 @@ func (ts *TelemetryService) trackPlugins() { }, plugin.OnSendDailyTelemetryID) } +func (ts *TelemetryService) trackProducts() { + hm := ts.srv.HooksManager() + if hm == nil { + return + } + + hm.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.OnSendDailyTelemetry() + return true + }, plugin.OnSendDailyTelemetryID) +} + func (ts *TelemetryService) trackServer() { data := map[string]any{ "edition": model.BuildEnterpriseReady, diff --git a/services/telemetry/telemetry_test.go b/services/telemetry/telemetry_test.go index 389cea5f37..650794eb03 100644 --- a/services/telemetry/telemetry_test.go +++ b/services/telemetry/telemetry_test.go @@ -25,6 +25,7 @@ import ( "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" "github.com/mattermost/mattermost-server/v6/plugin/plugintest" + "github.com/mattermost/mattermost-server/v6/product" "github.com/mattermost/mattermost-server/v6/services/httpservice" "github.com/mattermost/mattermost-server/v6/services/searchengine" "github.com/mattermost/mattermost-server/v6/services/telemetry/mocks" @@ -189,6 +190,7 @@ func initializeMocks(cfg *model.Config, cloudLicense bool) (*mocks.ServerIface, serverIfaceMock.On("GetRoleByName", context.Background(), "channel_guest").Return(&model.Role{Permissions: []string{"cg-test1", "cg-test2"}}, nil) serverIfaceMock.On("GetSchemes", "team", 0, 100).Return([]*model.Scheme{}, nil) serverIfaceMock.On("HTTPService").Return(httpservice.MakeHTTPService(configService)) + serverIfaceMock.On("HooksManager").Return(product.NewHooksManager(nil)) storeMock := &storeMocks.Store{} storeMock.On("GetDbVersion", false).Return("5.24.0", nil) From 1fe284538d55a1c9510f543e6f8a2518b45f3121 Mon Sep 17 00:00:00 2001 From: Julien Tant <785518+JulienTant@users.noreply.github.com> Date: Thu, 15 Dec 2022 08:56:18 -0700 Subject: [PATCH 29/43] [MM-49087] Bump NPS to 1.3.1 (#21885) Co-authored-by: Mattermod --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ae391ac8e8..b8a0a51cc4 100644 --- a/Makefile +++ b/Makefile @@ -159,7 +159,7 @@ PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.34.0 PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0 PLUGIN_PACKAGES += mattermost-plugin-jira-v3.2.2 PLUGIN_PACKAGES += mattermost-plugin-jitsi-v2.0.1 -PLUGIN_PACKAGES += mattermost-plugin-nps-v1.3.0 +PLUGIN_PACKAGES += mattermost-plugin-nps-v1.3.1 PLUGIN_PACKAGES += mattermost-plugin-todo-v0.6.1 PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.2.0 PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0 From 40a98416a28c4d8b0739a22263a3105650ac2af4 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Thu, 15 Dec 2022 09:37:44 -0700 Subject: [PATCH 30/43] update pre-package boards to v7.5.2 (#21879) Co-authored-by: Mattermod --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b8a0a51cc4..ede4f447d0 100644 --- a/Makefile +++ b/Makefile @@ -163,7 +163,7 @@ PLUGIN_PACKAGES += mattermost-plugin-nps-v1.3.1 PLUGIN_PACKAGES += mattermost-plugin-todo-v0.6.1 PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.2.0 PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0 -PLUGIN_PACKAGES += focalboard-v7.5.2 +PLUGIN_PACKAGES += focalboard-v7.5.4 PLUGIN_PACKAGES += mattermost-plugin-apps-v1.1.0 # Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target From fae9bdf173bb50e4efca6ff8b6af42c21a728a6b Mon Sep 17 00:00:00 2001 From: Amy Blais <29708087+amyblais@users.noreply.github.com> Date: Thu, 15 Dec 2022 11:49:10 -0500 Subject: [PATCH 31/43] Update minor version to 7.7.0 (#21890) Automatic Merge --- model/version.go | 1 + 1 file changed, 1 insertion(+) diff --git a/model/version.go b/model/version.go index b2a37dcda6..6090666ca3 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{ + "7.7.0", "7.6.0", "7.5.0", "7.4.0", From 33b59e0e96ae4f58671df4f4285e4d5c9de8c72a Mon Sep 17 00:00:00 2001 From: cyrilzhang-mm <112951043+cyrilzhang-mm@users.noreply.github.com> Date: Thu, 15 Dec 2022 14:20:36 -0500 Subject: [PATCH 32/43] Add parameter to return group members in order of display name (#21775) --- api4/user.go | 23 ++++-- api4/user_test.go | 58 ++++++++++++++ app/app_iface.go | 1 + app/group.go | 8 +- app/opentracing/opentracing_layer.go | 22 ++++++ model/client4.go | 18 +++++ store/opentracinglayer/opentracinglayer.go | 18 +++++ store/retrylayer/retrylayer.go | 21 +++++ store/sqlstore/group_store.go | 52 +++++++++--- store/store.go | 1 + store/storetest/group_store.go | 92 ++++++++++++++++++++-- store/storetest/mocks/GroupStore.go | 23 ++++++ store/timerlayer/timerlayer.go | 16 ++++ 13 files changed, 332 insertions(+), 21 deletions(-) diff --git a/api4/user.go b/api4/user.go index e0e32708b3..a35d1d6597 100644 --- a/api4/user.go +++ b/api4/user.go @@ -662,13 +662,14 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { return } - if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" && sort != "admin" { + if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" && sort != "admin" && sort != "display_name" { c.SetInvalidURLParam("sort") return } // Currently only supports sorting on a team // or sort="status" on inChannelId + // or sort="display_name" on inGroupId if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "" || inGroupId != "" || notInGroupId != "") { c.SetInvalidURLParam("sort") return @@ -681,6 +682,10 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.SetInvalidURLParam("sort") return } + if sort == "display_name" && (inGroupId == "" || notInGroupId != "" || inTeamId != "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "") { + c.SetInvalidURLParam("sort") + return + } var ( withoutTeamBool, _ = strconv.ParseBool(withoutTeam) @@ -869,10 +874,18 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { return } - profiles, _, appErr = c.App.GetGroupMemberUsersPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions) - if appErr != nil { - c.Err = appErr - return + if sort == "display_name" { + var user *model.User + + user, appErr = c.App.GetUser(c.AppContext.Session().UserId) + if appErr != nil { + c.Err = appErr + return + } + + profiles, _, appErr = c.App.GetGroupMemberUsersSortedPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions, c.App.GetNotificationNameFormat(user)) + } else { + profiles, _, appErr = c.App.GetGroupMemberUsersPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions) } } else if notInGroupId != "" { appErr = requireGroupAccess(c, notInGroupId) diff --git a/api4/user_test.go b/api4/user_test.go index 871f2c56e8..affd768fd7 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -2837,6 +2837,64 @@ func TestGetUsersInGroup(t *testing.T) { } +func TestGetUsersInGroupByDisplayName(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + id := model.NewId() + group, appErr := th.App.CreateGroup(&model.Group{ + DisplayName: "dn-foo_" + id, + Name: model.NewString("name" + id), + Source: model.GroupSourceLdap, + Description: "description_" + id, + RemoteId: model.NewString(model.NewId()), + }) + assert.Nil(t, appErr) + + user1, err := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "aaa", Password: "test-password-1", Username: "zzz", Roles: model.SystemUserRoleId}) + assert.Nil(t, err) + + user2, err := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Password: "test-password-2", Username: "bbb", Roles: model.SystemUserRoleId}) + assert.Nil(t, err) + + _, err = th.App.UpsertGroupMember(group.Id, user1.Id) + assert.Nil(t, err) + _, err = th.App.UpsertGroupMember(group.Id, user2.Id) + assert.Nil(t, err) + + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PrivacySettings.ShowFullName = true + }) + + preference := model.Preference{ + UserId: th.SystemAdminUser.Id, + Category: model.PreferenceCategoryDisplaySettings, + Name: model.PreferenceNameNameFormat, + Value: model.ShowUsername, + } + + err = th.App.UpdatePreferences(th.SystemAdminUser.Id, model.Preferences{preference}) + assert.Nil(t, err) + + t.Run("Returns users in group in right order for username", func(t *testing.T) { + users, _, err := th.SystemAdminClient.GetUsersInGroupByDisplayName(group.Id, 0, 1, "") + require.NoError(t, err) + assert.Equal(t, users[0].Id, user2.Id) + }) + + preference.Value = model.ShowNicknameFullName + err = th.App.UpdatePreferences(th.SystemAdminUser.Id, model.Preferences{preference}) + assert.Nil(t, err) + + t.Run("Returns users in group in right order for nickname", func(t *testing.T) { + users, _, err := th.SystemAdminClient.GetUsersInGroupByDisplayName(group.Id, 0, 1, "") + require.NoError(t, err) + assert.Equal(t, users[0].Id, user1.Id) + }) + +} + func TestUpdateUserMfa(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/app_iface.go b/app/app_iface.go index 24f6871775..f10ca2d8b9 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -652,6 +652,7 @@ type AppIface interface { GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) + GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) diff --git a/app/group.go b/app/group.go index 6db059b33f..a01b369e41 100644 --- a/app/group.go +++ b/app/group.go @@ -250,8 +250,8 @@ func (a *App) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppErro return users, nil } -func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) { - members, err := a.Srv().Store().Group().GetMemberUsersPage(groupID, page, perPage, viewRestrictions) +func (a *App) GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) { + members, err := a.Srv().Store().Group().GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay) if err != nil { return nil, 0, model.NewAppError("GetGroupMemberUsersPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -263,6 +263,10 @@ func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int, vie return a.sanitizeProfiles(members, false), int(count), nil } +func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) { + return a.GetGroupMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, model.ShowUsername) +} + func (a *App) GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { members, err := a.Srv().Store().Group().GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions) if err != nil { diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index c004aff52b..367d98ec39 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -6438,6 +6438,28 @@ func (a *OpenTracingAppLayer) GetGroupMemberUsersPage(groupID string, page int, return resultVar0, resultVar1, resultVar2 } +func (a *OpenTracingAppLayer) GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberUsersSortedPage") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1, resultVar2 := a.app.GetGroupMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay) + + if resultVar2 != nil { + span.LogFields(spanlog.Error(resultVar2)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1, resultVar2 +} + func (a *OpenTracingAppLayer) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupSyncable") diff --git a/model/client4.go b/model/client4.go index d52df99214..48d63e959c 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1258,6 +1258,24 @@ func (c *Client4) GetUsersInGroup(groupID string, page int, perPage int, etag st return list, BuildResponse(r), nil } +// GetUsersInGroup returns a page of users in a group. Page counting starts at 0. +func (c *Client4) GetUsersInGroupByDisplayName(groupID string, page int, perPage int, etag string) ([]*User, *Response, error) { + query := fmt.Sprintf("?sort=display_name&in_group=%v&page=%v&per_page=%v", groupID, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if err := json.NewDecoder(r.Body).Decode(&list); err != nil { + return nil, nil, NewAppError("GetUsersInGroupByDisplayName", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + return list, BuildResponse(r), nil +} + // GetUsersByIds returns a list of users based on the provided user ids. func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response, error) { r, err := c.DoAPIPost(c.usersRoute()+"/ids", ArrayToJSON(userIds)) diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 241f3edf50..87b443640a 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -4501,6 +4501,24 @@ func (s *OpenTracingLayerGroupStore) GetMemberUsersPage(groupID string, page int return result, err } +func (s *OpenTracingLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersSortedPage") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetNonMemberUsersPage") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index dec858bba9..38a3f52dbf 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -5070,6 +5070,27 @@ func (s *RetryLayerGroupStore) GetMemberUsersPage(groupID string, page int, perP } +func (s *RetryLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) { + + tries := 0 + for { + result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { tries := 0 diff --git a/store/sqlstore/group_store.go b/store/sqlstore/group_store.go index 64737840b8..8d160ef7a8 100644 --- a/store/sqlstore/group_store.go +++ b/store/sqlstore/group_store.go @@ -421,22 +421,56 @@ func (s *SqlGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) { } func (s *SqlGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { + return s.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, model.ShowUsername) +} + +func (s *SqlGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) { groupMembers := []*model.User{} - query := s.getQueryBuilder(). - Select("u.*"). + userQuery := s.getQueryBuilder(). + Select(`u.*`). From("GroupMembers"). Join("Users u ON u.Id = GroupMembers.UserId"). Where(sq.Eq{"GroupMembers.DeleteAt": 0}). Where(sq.Eq{"u.DeleteAt": 0}). - Where(sq.Eq{"GroupId": groupID}). + Where(sq.Eq{"GroupId": groupID}) + + userQuery = applyViewRestrictionsFilter(userQuery, viewRestrictions, true) + queryString, args, err := userQuery.ToSql() + if err != nil { + return nil, errors.Wrap(err, "") + } + + orderQuery := s.getQueryBuilder(). + Select("u.*"). + From("(" + queryString + ") AS u") + + if teammateNameDisplay == model.ShowNicknameFullName { + orderQuery = orderQuery.OrderBy(` + CASE + WHEN u.Nickname != '' THEN u.Nickname + WHEN u.FirstName != '' AND u.LastName != '' THEN CONCAT(u.FirstName, ' ', u.LastName) + WHEN u.FirstName != '' THEN u.FirstName + WHEN u.LastName != '' THEN u.LastName + ELSE u.Username + END`) + } else if teammateNameDisplay == model.ShowFullName { + orderQuery = orderQuery.OrderBy(` + CASE + WHEN u.FirstName != '' AND u.LastName != '' THEN CONCAT(u.FirstName, ' ', u.LastName) + WHEN u.FirstName != '' THEN u.FirstName + WHEN u.LastName != '' THEN u.LastName + ELSE u.Username + END`) + } else { + orderQuery = orderQuery.OrderBy("u.Username") + } + + orderQuery = orderQuery. Limit(uint64(perPage)). - Offset(uint64(page * perPage)). - OrderBy("u.CreateAt DESC") + Offset(uint64(page * perPage)) - query = applyViewRestrictionsFilter(query, viewRestrictions, true) - - queryString, args, err := query.ToSql() + queryString, _, err = orderQuery.ToSql() if err != nil { return nil, errors.Wrap(err, "") } @@ -463,7 +497,7 @@ func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage Where("(GroupMembers.UserID IS NULL OR GroupMembers.DeleteAt != 0)"). Limit(uint64(perPage)). Offset(uint64(page * perPage)). - OrderBy("u.CreateAt DESC") + OrderBy("u.Username ASC") query = applyViewRestrictionsFilter(query, viewRestrictions, true) diff --git a/store/store.go b/store/store.go index 6a7f386553..49490afdbb 100644 --- a/store/store.go +++ b/store/store.go @@ -842,6 +842,7 @@ type GroupStore interface { GetMemberUsers(groupID string) ([]*model.User, error) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) + GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) GetMemberCount(groupID string) (int64, error) diff --git a/store/storetest/group_store.go b/store/storetest/group_store.go index 02d788765d..2b60e829f9 100644 --- a/store/storetest/group_store.go +++ b/store/storetest/group_store.go @@ -36,6 +36,7 @@ func TestGroupStore(t *testing.T, ss store.Store) { t.Run("GetMemberUsers", func(t *testing.T) { testGroupGetMemberUsers(t, ss) }) t.Run("GetMemberUsersPage", func(t *testing.T) { testGroupGetMemberUsersPage(t, ss) }) + t.Run("GetMemberUsersSortedPage", func(t *testing.T) { testGroupGetMemberUsersSortedPage(t, ss) }) t.Run("GetMemberUsersInTeam", func(t *testing.T) { testGroupGetMemberUsersInTeam(t, ss) }) t.Run("GetMemberUsersNotInChannel", func(t *testing.T) { testGroupGetMemberUsersNotInChannel(t, ss) }) @@ -861,7 +862,7 @@ func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) { u1 := &model.User{ Email: MakeEmail(), - Username: model.NewId(), + Username: "user1" + model.NewId(), } user1, nErr := ss.User().Save(u1) require.NoError(t, nErr) @@ -871,7 +872,7 @@ func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) { u2 := &model.User{ Email: MakeEmail(), - Username: model.NewId(), + Username: "user2" + model.NewId(), } user2, nErr := ss.User().Save(u2) require.NoError(t, nErr) @@ -881,7 +882,7 @@ func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) { u3 := &model.User{ Email: MakeEmail(), - Username: model.NewId(), + Username: "user3" + model.NewId(), } user3, nErr := ss.User().Save(u3) require.NoError(t, nErr) @@ -898,13 +899,13 @@ func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) { groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 2, nil) require.NoError(t, err) require.Equal(t, 2, len(groupMembers)) - require.ElementsMatch(t, []*model.User{user2, user3}, groupMembers) + require.ElementsMatch(t, []*model.User{user1, user2}, groupMembers) // Check page 2 groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 1, 2, nil) require.NoError(t, err) require.Equal(t, 1, len(groupMembers)) - require.ElementsMatch(t, []*model.User{user1}, groupMembers) + require.ElementsMatch(t, []*model.User{user3}, groupMembers) // Check madeup id groupMembers, err = ss.Group().GetMemberUsersPage(model.NewId(), 0, 100, nil) @@ -921,6 +922,87 @@ func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) { require.Equal(t, 2, len(groupMembers)) } +func testGroupGetMemberUsersSortedPage(t *testing.T, ss store.Store) { + // Save a group + g1 := &model.Group{ + Name: model.NewString(model.NewId()), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewString(model.NewId()), + } + group, err := ss.Group().Create(g1) + require.NoError(t, err) + + // First by nickname, third by full name, second by username + u1 := &model.User{ + Email: MakeEmail(), + Username: "y" + model.NewId(), + Nickname: "a" + model.NewId(), + FirstName: "z" + model.NewId(), + LastName: "z" + model.NewId(), + } + user1, nErr := ss.User().Save(u1) + require.NoError(t, nErr) + + _, err = ss.Group().UpsertMember(group.Id, user1.Id) + require.NoError(t, err) + + // Second by nickname, first by full name, third by username + u2 := &model.User{ + Email: MakeEmail(), + Username: "z" + model.NewId(), + FirstName: "b" + model.NewId(), + LastName: "b" + model.NewId(), + } + user2, nErr := ss.User().Save(u2) + require.NoError(t, nErr) + + _, err = ss.Group().UpsertMember(group.Id, user2.Id) + require.NoError(t, err) + + // Third by nickname, second by full name, first by username + u3 := &model.User{ + Email: MakeEmail(), + Username: "d" + model.NewId(), + } + user3, nErr := ss.User().Save(u3) + require.NoError(t, nErr) + + _, err = ss.Group().UpsertMember(group.Id, user3.Id) + require.NoError(t, err) + + // Check nickname ordering, paged + groupMembers, err := ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowNicknameFullName) + require.NoError(t, err) + require.Equal(t, 2, len(groupMembers)) + require.ElementsMatch(t, []*model.User{user1, user2}, groupMembers) + groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowNicknameFullName) + require.NoError(t, err) + require.Equal(t, 1, len(groupMembers)) + require.ElementsMatch(t, []*model.User{user3}, groupMembers) + + // Check full name ordering, paged + groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowFullName) + require.NoError(t, err) + require.Equal(t, 2, len(groupMembers)) + require.ElementsMatch(t, []*model.User{user2, user3}, groupMembers) + groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowFullName) + require.NoError(t, err) + require.Equal(t, 1, len(groupMembers)) + require.ElementsMatch(t, []*model.User{user1}, groupMembers) + + // Check username ordering + groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowUsername) + require.NoError(t, err) + require.Equal(t, 2, len(groupMembers)) + require.ElementsMatch(t, []*model.User{user3, user1}, groupMembers) + groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowUsername) + require.NoError(t, err) + require.Equal(t, 1, len(groupMembers)) + require.ElementsMatch(t, []*model.User{user2}, groupMembers) +} + func testGroupGetMemberUsersInTeam(t *testing.T, ss store.Store) { // Save a team team := &model.Team{ diff --git a/store/storetest/mocks/GroupStore.go b/store/storetest/mocks/GroupStore.go index 7f54c5487b..e77e2a8628 100644 --- a/store/storetest/mocks/GroupStore.go +++ b/store/storetest/mocks/GroupStore.go @@ -826,6 +826,29 @@ func (_m *GroupStore) GetMemberUsersPage(groupID string, page int, perPage int, return r0, r1 } +// GetMemberUsersSortedPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions, teammateNameDisplay +func (_m *GroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) { + ret := _m.Called(groupID, page, perPage, viewRestrictions, teammateNameDisplay) + + var r0 []*model.User + if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions, string) []*model.User); ok { + r0 = rf(groupID, page, perPage, viewRestrictions, teammateNameDisplay) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions, string) error); ok { + r1 = rf(groupID, page, perPage, viewRestrictions, teammateNameDisplay) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetNonMemberUsersPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions func (_m *GroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { ret := _m.Called(groupID, page, perPage, viewRestrictions) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 55415d1c47..02be7b110d 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -4098,6 +4098,22 @@ func (s *TimerLayerGroupStore) GetMemberUsersPage(groupID string, page int, perP return result, err } +func (s *TimerLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) { + start := time.Now() + + result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersSortedPage", success, elapsed) + } + return result, err +} + func (s *TimerLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { start := time.Now() From e58b6ffa3ed50f07f16eb5b0ae3d2061c04a5271 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Fri, 16 Dec 2022 13:00:15 +0300 Subject: [PATCH 33/43] app/product: block products to be initialized with the feature flag (#21875) --- app/product.go | 11 +++++++++++ app/product_test.go | 31 ++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/app/product.go b/app/product.go index 9183e40ea8..6ab3719c9a 100644 --- a/app/product.go +++ b/app/product.go @@ -17,6 +17,9 @@ func (s *Server) initializeProducts( // create a product map to consume pmap := make(map[string]struct{}) for name := range productMap { + if !s.shouldStart(name) { + continue + } pmap[name] = struct{}{} } @@ -64,3 +67,11 @@ func (s *Server) initializeProducts( return nil } + +func (s *Server) shouldStart(product string) bool { + if !s.Config().FeatureFlags.BoardsProduct && product == "boards" { + return false + } + + return true +} diff --git a/app/product_test.go b/app/product_test.go index 66d0ba6dcf..ad5c6c1be7 100644 --- a/app/product_test.go +++ b/app/product_test.go @@ -6,6 +6,8 @@ package app import ( "testing" + "github.com/mattermost/mattermost-server/v6/app/platform" + "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/product" "github.com/stretchr/testify/require" ) @@ -36,6 +38,9 @@ func (p *productB) Start() error { return nil } func (p *productB) Stop() error { return nil } func TestInitializeProducts(t *testing.T) { + ps, err := platform.New(platform.ServiceConfig{ConfigStore: config.NewTestMemoryStore()}) + require.NoError(t, err) + t.Run("2 products and no circular dependency", func(t *testing.T) { serviceMap := map[product.ServiceKey]any{ product.ConfigKey: nil, @@ -64,11 +69,13 @@ func TestInitializeProducts(t *testing.T) { }, }, } + server := &Server{ products: make(map[string]product.Product), + platform: ps, } - err := server.initializeProducts(products, serviceMap) + err = server.initializeProducts(products, serviceMap) require.NoError(t, err) require.Len(t, server.products, 2) }) @@ -104,6 +111,7 @@ func TestInitializeProducts(t *testing.T) { } server := &Server{ products: make(map[string]product.Product), + platform: ps, } err := server.initializeProducts(products, serviceMap) @@ -132,10 +140,31 @@ func TestInitializeProducts(t *testing.T) { } server := &Server{ products: make(map[string]product.Product), + platform: ps, } err := server.initializeProducts(products, serviceMap) require.NoError(t, err) require.Len(t, server.products, 2) }) + + t.Run("boards product to be blocked", func(t *testing.T) { + products := map[string]product.Manifest{ + "productA": { + Initializer: newProductA, + }, + "boards": { + Initializer: newProductB, + }, + } + + server := &Server{ + products: make(map[string]product.Product), + platform: ps, + } + + err := server.initializeProducts(products, map[product.ServiceKey]any{}) + require.NoError(t, err) + require.Len(t, server.products, 1) + }) } From f3e8a0b72fc7b7f1fe5a00700c7cdd3556fc41cb Mon Sep 17 00:00:00 2001 From: Tim Scheuermann Date: Fri, 16 Dec 2022 16:29:26 +0100 Subject: [PATCH 34/43] [MM-49003] Close the pipe reader when done reading (#21854) --- jobs/export_process/worker.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/jobs/export_process/worker.go b/jobs/export_process/worker.go index 2697b65980..ddf9462b14 100644 --- a/jobs/export_process/worker.go +++ b/jobs/export_process/worker.go @@ -44,26 +44,24 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { rd, wr := io.Pipe() - errCh := make(chan *model.AppError, 1) go func() { - defer close(errCh) - // Try to write without a timeout _, appErr := app.WriteFileContext(context.Background(), rd, filepath.Join(outPath, exportFilename)) - errCh <- appErr + if appErr != nil { + // we close the reader here to prevent a deadlock when the bulk exporter tries to + // write into the pipe while app.WriteFile has already returned. The error will be + // returned by the writer part of the pipe when app.BulkExport tries to call + // wr.Write() on it. + rd.CloseWithError(appErr) // CloseWithError never returns an error + } }() appErr := app.BulkExport(request.EmptyContext(app.Log()), wr, outPath, opts) - if err := wr.Close(); err != nil { - mlog.Warn("Worker: error closing writer") - } + wr.Close() // Close never returns an error if appErr != nil { return appErr } - if appErr := <-errCh; appErr != nil { - return appErr - } return nil } worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) From bef1f1cf601d0cd3d7bd0400bd1f8e34d1bc6ef7 Mon Sep 17 00:00:00 2001 From: Tim Scheuermann Date: Fri, 16 Dec 2022 16:44:51 +0100 Subject: [PATCH 35/43] Close pipes on file writer error (#21900) --- app/file.go | 1 + app/upload.go | 1 + 2 files changed, 2 insertions(+) diff --git a/app/file.go b/app/file.go index a47e817003..7a66d25010 100644 --- a/app/file.go +++ b/app/file.go @@ -824,6 +824,7 @@ func (t *UploadFileTask) postprocessImage(file io.Reader) { _, aerr := t.writeFile(r, path) if aerr != nil { mlog.Error("Unable to upload", mlog.String("path", path), mlog.Err(aerr)) + r.CloseWithError(aerr) // always returns nil return } } diff --git a/app/upload.go b/app/upload.go index ef4bff2b71..318e3ede89 100644 --- a/app/upload.go +++ b/app/upload.go @@ -93,6 +93,7 @@ func (a *App) runPluginsHook(c request.CTX, info *model.FileInfo, file io.Reader if fileErr := a.RemoveFile(tmpPath); fileErr != nil { mlog.Warn("Failed to remove file", mlog.Err(fileErr)) } + r.CloseWithError(err) // always returns nil return err } From 529fdd2643a32be5f67ea2b1d9b05d82b048658c Mon Sep 17 00:00:00 2001 From: Daniel Schalla Date: Fri, 16 Dec 2022 16:50:06 +0100 Subject: [PATCH 36/43] [MM-49170] Implement Auditable Interface for Patch Post Struct (#21898) * Implement Auditable Function for Patch Post Endpoint * Call auditable explicitly for patchPost patch parameter * Delete audit-config.json * Include HasReactions in Audit Log Entry for Patch --- api4/post.go | 3 ++- model/post.go | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api4/post.go b/api4/post.go index ff707c178c..34bcf21c2b 100644 --- a/api4/post.go +++ b/api4/post.go @@ -802,7 +802,8 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) { } auditRec := c.MakeAuditRecord("patchPost", audit.Fail) - auditRec.AddEventParameter("patch", post) + auditRec.AddEventParameter("id", c.Params.PostId) + auditRec.AddEventParameter("patch", post.Auditable()) defer c.LogAuditRecWithLevel(auditRec, app.LevelContent) // Updating the file_ids of a post is not a supported operation and will be ignored diff --git a/model/post.go b/model/post.go index 992188ac8e..2a7c4538e0 100644 --- a/model/post.go +++ b/model/post.go @@ -119,7 +119,7 @@ type Post struct { } func (o *Post) Auditable() map[string]interface{} { - return map[string]interface{}{ // TODO check this + return map[string]interface{}{ "id": o.Id, "create_at": o.CreateAt, "update_at": o.UpdateAt, @@ -195,6 +195,15 @@ func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { return © } +func (o *PostPatch) Auditable() map[string]interface{} { + return map[string]interface{}{ + "is_pinned": o.IsPinned, + "props": o.Props, + "file_ids": o.FileIds, + "has_reactions": o.HasReactions, + } +} + type PostForExport struct { Post TeamName string From f140b1863fd0674ba31e3516e5b60b2e141cf1c4 Mon Sep 17 00:00:00 2001 From: emmyni <44761757+emmyni@users.noreply.github.com> Date: Fri, 16 Dec 2022 16:34:40 -0500 Subject: [PATCH 37/43] [MM 48128]: Fix Spacing problem on "Payment Failed" email (#21881) --- templates/payment_failed_no_card_body.html | 27 ++++++++-------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/templates/payment_failed_no_card_body.html b/templates/payment_failed_no_card_body.html index 6966bf296b..ec7d4d5de6 100644 --- a/templates/payment_failed_no_card_body.html +++ b/templates/payment_failed_no_card_body.html @@ -1,7 +1,7 @@ {{define "payment_failed_no_card_body"}} -
- + {{.Props.SecondaryActionButtonText}}
@@ -18,23 +18,16 @@
- + style="border-collapse: collapse"> +
+ style="padding: 20px 0 0; text-align: center; margin: 0 auto; max-width: 443px"> -
- - - - -
-

- {{ .Props.Title }}

-
+
+

+ {{ .Props.Title }}

{{ .Props.Info1 }}

@@ -50,7 +43,7 @@
- +
@@ -65,7 +58,7 @@
- +
From 686c2cc84beb92a67db6d16fb89201cf7c73da18 Mon Sep 17 00:00:00 2001 From: MArtin Johnson Date: Mon, 19 Dec 2022 16:23:28 +0100 Subject: [PATCH 38/43] Translated using Weblate (Swedish) Currently translated at 100.0% (2429 of 2429 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/sv/ --- i18n/sv.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/i18n/sv.json b/i18n/sv.json index ec2aa98509..3d3d19a447 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -9725,5 +9725,9 @@ { "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", "translation": "Din arbetsyta {{.WorkspaceName}} har nu uppgraderats. Du kommer att faktureras från och med {{.Date}}" + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Ogiltiga attribut." } ] From f58740f23fb7d22341da18f34829c50da5698ef4 Mon Sep 17 00:00:00 2001 From: jprusch Date: Mon, 19 Dec 2022 16:23:29 +0100 Subject: [PATCH 39/43] Translated using Weblate (German) Currently translated at 100.0% (2430 of 2430 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ --- i18n/de.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/i18n/de.json b/i18n/de.json index d2ebc4bbd5..7daafc82da 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -9729,5 +9729,9 @@ { "id": "api.user.get_users.validation.app_error", "translation": "Fehler beim Abrufen von Rollen während der Validierung." + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Das Portal ist für selbst gehostete Anmeldungen nicht verfügbar." } ] From 43f89d75565b6e6aa828e8c8eb38b36577b88828 Mon Sep 17 00:00:00 2001 From: master7 Date: Mon, 19 Dec 2022 16:23:29 +0100 Subject: [PATCH 40/43] Translated using Weblate (Polish) Currently translated at 100.0% (2430 of 2430 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ --- i18n/pl.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/i18n/pl.json b/i18n/pl.json index 2489a694c5..cc23eeb202 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -9730,5 +9730,9 @@ { "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", "translation": "Twoja {{.WorkspaceName}} została zaktualizowana. Opłaty będą naliczane od {{.Date}}" + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portal niedostępny dla samodzielnej rejestracji." } ] From d9f339cb4c2acd78c418f22bf69e05b9efb136cd Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 19 Dec 2022 16:23:29 +0100 Subject: [PATCH 41/43] Translated using Weblate (Russian) Currently translated at 100.0% (2430 of 2430 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ru/ --- i18n/ru.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/i18n/ru.json b/i18n/ru.json index 406b4220b0..b31289ad9c 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -9730,5 +9730,9 @@ { "id": "api.user.get_users.validation.app_error", "translation": "Ошибка при получении ролей во время проверки." + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Портал недоступен для самостоятельной регистрации." } ] From 5f6fc3150d9faf57743bad526c69eb6474a82e21 Mon Sep 17 00:00:00 2001 From: kaakaa Date: Mon, 19 Dec 2022 16:23:30 +0100 Subject: [PATCH 42/43] Translated using Weblate (Japanese) Currently translated at 100.0% (2430 of 2430 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ja/ --- i18n/ja.json | 102 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/i18n/ja.json b/i18n/ja.json index 4df39843dd..8e854aa2e9 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -289,7 +289,7 @@ }, { "id": "api.channel.update_channel_member_roles.scheme_role.app_error", - "translation": "与えられた役割はスキームによって管理されているため、チャンネルメンバーへ直接適用することはできません。" + "translation": "与えられたロールはスキームによって管理されているため、チャンネルメンバーへ直接適用することはできません。" }, { "id": "api.channel.update_channel_scheme.license.error", @@ -301,7 +301,7 @@ }, { "id": "api.channel.update_team_member_roles.scheme_role.app_error", - "translation": "与えられた役割はスキームによって管理されているため、チームメンバーへ直接適用することはできません。" + "translation": "与えられたロールはスキームによって管理されているため、チームメンバーへ直接適用することはできません。" }, { "id": "api.command.admin_only.app_error", @@ -2657,19 +2657,19 @@ }, { "id": "app.import.validate_role_import_data.description_invalid.error", - "translation": "役割の説明が不正です。" + "translation": "ロールの説明が不正です。" }, { "id": "app.import.validate_role_import_data.display_name_invalid.error", - "translation": "役割の表示名が不正です。" + "translation": "ロールの表示名が不正です。" }, { "id": "app.import.validate_role_import_data.invalid_permission.error", - "translation": "権限もしくは役割が不正です。" + "translation": "ロールもしくは役割が不正です。" }, { "id": "app.import.validate_role_import_data.name_invalid.error", - "translation": "役割の名前が不正です。" + "translation": "ロールの名前が不正です。" }, { "id": "app.import.validate_scheme_import_data.description_invalid.error", @@ -2693,7 +2693,7 @@ }, { "id": "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", - "translation": "このスコープのスキームに誤った役割が与えられました。" + "translation": "このスコープのスキームに誤ったロールが与えられました。" }, { "id": "app.import.validate_team_import_data.description_length.error", @@ -2753,7 +2753,7 @@ }, { "id": "app.import.validate_user_channels_import_data.invalid_roles.error", - "translation": "ユーザーのチャネルメンバーシップの役割が不正です。" + "translation": "ユーザーのチャネルメンバーシップのロールが不正です。" }, { "id": "app.import.validate_user_import_data.auth_data_and_password.error", @@ -2825,7 +2825,7 @@ }, { "id": "app.import.validate_user_import_data.roles_invalid.error", - "translation": "ユーザーの役割が正しくありません。" + "translation": "ユーザーのロールが正しくありません。" }, { "id": "app.import.validate_user_import_data.username_invalid.error", @@ -2837,7 +2837,7 @@ }, { "id": "app.import.validate_user_teams_import_data.invalid_roles.error", - "translation": "ユーザーのチームメンバーシップの役割が不正です。" + "translation": "ユーザーのチームメンバーシップのロールが不正です。" }, { "id": "app.import.validate_user_teams_import_data.team_name_missing.error", @@ -2933,7 +2933,7 @@ }, { "id": "app.role.check_roles_exist.role_not_found", - "translation": "指定された役割は存在しません" + "translation": "指定されたロールは存在しません" }, { "id": "app.save_config.app_error", @@ -5709,7 +5709,7 @@ }, { "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", - "translation": "不正なチームメンバ更新: 手動でゲストの役割を追加/削除することはできません。" + "translation": "不正なチームメンバ更新: 手動でゲストのロールを追加/削除することはできません。" }, { "id": "api.channel.update_channel_privacy.default_channel_error", @@ -5721,7 +5721,7 @@ }, { "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", - "translation": "不正なチャンネルメンバー更新: 手動でゲストの役割を追加/削除することはできません。" + "translation": "不正なチャンネルメンバー更新: 手動でゲストのロールを追加/削除することはできません。" }, { "id": "api.channel.update_channel.typechange.app_error", @@ -6437,27 +6437,27 @@ }, { "id": "app.role.save.invalid_role.app_error", - "translation": "役割が不正です。" + "translation": "ロールが不正です。" }, { "id": "app.role.save.insert.app_error", - "translation": "新しい役割を保存できませんでした。" + "translation": "新しいロールを保存できませんでした。" }, { "id": "app.role.permanent_delete_all.app_error", - "translation": "すべての役割を完全に削除できませんでした。" + "translation": "すべてのロールを完全に削除できませんでした。" }, { "id": "app.role.get_by_names.app_error", - "translation": "役割を取得できませんでした。" + "translation": "ロールを取得できませんでした。" }, { "id": "app.role.get_by_name.app_error", - "translation": "役割を取得できませんでした。" + "translation": "ロールを取得できませんでした。" }, { "id": "app.role.get.app_error", - "translation": "役割を取得できませんでした。" + "translation": "ロールを取得できませんでした。" }, { "id": "model.config.is_valid.directory.app_error", @@ -7181,7 +7181,7 @@ }, { "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_body", - "translation": "チャンネルはコミュニケーションの改善をサポートするものですが、Mattermost全体でチャンネルの作成や参加が多くなるにつれ、システムを整理されたものにし続けることが課題になってきます。高度な権限設定により、どのユーザー、もしくはどの役割の人が何のアクションを実行可能かを設定することができます。例えば、チャンネル設定やメンバーの管理や、@channel、@hereなどのタグによるグループへの発信、新たなウェブフックの作成などを制限できます。\n\n詳しくは[高度な権限設定の利用に関する説明](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)を参照してください\n\nトライアル開始 をクリックすると、[Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/) と [プライバシーポリシー](https://mattermost.com/privacy-policy/)に同意したことになり、製品に関する電子メールを受信するようになります。" + "translation": "チャンネルはコミュニケーションの改善をサポートするものですが、Mattermost全体でチャンネルの作成や参加が多くなるにつれ、システムを整理されたものにし続けることが課題になってきます。高度な権限設定により、どのユーザー、もしくはどのロールの人が何のアクションを実行可能かを設定することができます。例えば、チャンネル設定やメンバーの管理や、@channel、@hereなどのタグによるグループへの発信、新たなウェブフックの作成などを制限できます。\n\n詳しくは[高度な権限設定の利用に関する説明](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)を参照してください\n\nトライアル開始 をクリックすると、[Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/) と [プライバシーポリシー](https://mattermost.com/privacy-policy/)に同意したことになり、製品に関する電子メールを受信するようになります。" }, { "id": "api.server.warn_metric.number_of_channels_50.contact_us.email_body", @@ -7189,7 +7189,7 @@ }, { "id": "api.server.warn_metric.number_of_channels_50.notification_body", - "translation": "チャンネルはコミュニケーションの改善をサポートするものですが、Mattermost全体でチャンネルの作成や参加が多くなるにつれ、システムを整理されたものにし続けることが課題になってきます。高度な権限設定により、どのユーザー、もしくはどの役割の人が何のアクションを実行可能かを設定することができます。例えば、チャンネル設定やメンバーの管理や、@channel、@hereなどのタグによるグループへの発信、新たなウェブフックの作成などを制限できます。\n\n詳しくは[高度な権限設定の利用に関する説明](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)を参照してください\n\n問い合わせ をクリックすると、あなたの情報が Mattermost, Inc. へ共有されます。詳しくは[説明文書](https://mattermost.com/pl/default-admin-advisory)を参照してください" + "translation": "チャンネルはコミュニケーションの改善をサポートするものですが、Mattermost全体でチャンネルの作成や参加が多くなるにつれ、システムを整理されたものにし続けることが課題になってきます。高度な権限設定により、どのユーザー、もしくはどのロールの人が何のアクションを実行可能かを設定することができます。例えば、チャンネル設定やメンバーの管理や、@channel、@hereなどのタグによるグループへの発信、新たなウェブフックの作成などを制限できます。\n\n詳しくは[高度な権限設定の利用に関する説明](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)を参照してください\n\n問い合わせ をクリックすると、あなたの情報が Mattermost, Inc. へ共有されます。詳しくは[説明文書](https://mattermost.com/pl/default-admin-advisory)を参照してください" }, { "id": "api.server.warn_metric.number_of_channels_50.notification_title", @@ -8885,11 +8885,11 @@ }, { "id": "model.user.is_valid.roles_limit.app_error", - "translation": "{{.Limit}}文字以上の不正なユーザーの役割です。" + "translation": "{{.Limit}}文字以上の不正なユーザーのロールです。" }, { "id": "model.team_member.is_valid.roles_limit.app_error", - "translation": "{{.Limit}} 文字より長い不正なチームメンバーの役割です。" + "translation": "{{.Limit}} 文字より長い不正なチームメンバーのロールです。" }, { "id": "model.session.is_valid.user_id.app_error", @@ -8897,7 +8897,7 @@ }, { "id": "model.channel_member.is_valid.roles_limit.app_error", - "translation": "{{.Limit}} 文字より長い不正なチャンネルメンバーの役割です。" + "translation": "{{.Limit}} 文字より長い不正なチャンネルメンバーのロールです。" }, { "id": "model.session.is_valid.roles_limit.app_error", @@ -8937,7 +8937,7 @@ }, { "id": "app.role.get_all.app_error", - "translation": "全ての役割を取得できませんでした。" + "translation": "全てのロールを取得できませんでした。" }, { "id": "api.user.view_archived_channels.get_users_in_channel.app_error", @@ -9674,5 +9674,57 @@ { "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "アーカイブされたチャンネルでは、確認応答を削除することはできません。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "開発チーム間で透明性の高いワークフローを作成し、機能開発プロセスをシームレスにすることができます。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Jira BotやGitHub Botと統合し、生産性を高めましょう。これらはあなたのためにダウンロードされます。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Boards、Playbooks、Botと簡単に接続できる Feature Release チャンネルでチームとチャットできます。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "スタンドアップなどの定期的なミーティングには Meeting Agenda ボードテンプレート、タスクの進捗管理には Project Task ボードをご利用ください。" + }, + { + "id": "worktemplate.category.product_teams", + "translation": "製品チーム" + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "不正な優先度" + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "作業テンプレートを取得できませんでした" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "作業テンプレートのカテゴリを取得できませんでした" + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "検証中のロール取得時にエラーが発生しました。" + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "請求書を見る" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "ワークスペース {{.WorkspaceName}} がアップグレードされました。" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "ワークスペース {{.WorkspaceName}} がアップグレードされました。{{.Date}} から課金されます" + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "セルフホスティングの利用登録では、ポータルは利用できません。" } ] From 79193240e9a5767ad2201bce3ef7f744320c3b7b Mon Sep 17 00:00:00 2001 From: Shivashis Padhi Date: Mon, 19 Dec 2022 22:31:59 +0530 Subject: [PATCH 43/43] [MM-44842] Add restore_group permission (#21806) * Add restore_group permission * Fix tests failing due to new permission in groups * Add new migration to add custom_group_restore permission * Add mock for new migration function * Fix tests Co-authored-by: Mattermod --- api4/group.go | 4 ++-- api4/group_test.go | 6 ++++++ app/app_test.go | 2 ++ app/permissions_migrations.go | 25 +++++++++++++++++++++++++ model/migration.go | 1 + model/permission.go | 9 +++++++++ model/role.go | 2 ++ testlib/store.go | 1 + 8 files changed, 48 insertions(+), 2 deletions(-) diff --git a/api4/group.go b/api4/group.go index 72c78e7179..59b9ad12c1 100644 --- a/api4/group.go +++ b/api4/group.go @@ -1185,8 +1185,8 @@ func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionDeleteCustomGroup) { - c.SetPermissionError(model.PermissionDeleteCustomGroup) + if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionRestoreCustomGroup) { + c.SetPermissionError(model.PermissionRestoreCustomGroup) return } diff --git a/api4/group_test.go b/api4/group_test.go index a612291d8c..7fdf38147e 100644 --- a/api4/group_test.go +++ b/api4/group_test.go @@ -231,7 +231,13 @@ func TestUndeleteGroup(t *testing.T) { _, response, err := th.Client.DeleteGroup(validGroup.Id) require.NoError(t, err) CheckOKStatus(t, response) + th.RemovePermissionFromRole(model.PermissionRestoreCustomGroup.Id, model.SystemUserRoleId) + // shouldn't allow restoring unless user has required permission + _, response, err = th.Client.RestoreGroup(validGroup.Id, "") + require.Error(t, err) + CheckForbiddenStatus(t, response) + th.AddPermissionToRole(model.PermissionRestoreCustomGroup.Id, model.SystemUserRoleId) _, response, err = th.Client.RestoreGroup(validGroup.Id, "") require.NoError(t, err) CheckOKStatus(t, response) diff --git a/app/app_test.go b/app/app_test.go index 9f4d2fa0a9..05e12792b0 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -168,6 +168,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PermissionCreateCustomGroup.Id, model.PermissionEditCustomGroup.Id, model.PermissionDeleteCustomGroup.Id, + model.PermissionRestoreCustomGroup.Id, model.PermissionManageCustomGroupMembers.Id, }, "system_post_all": { @@ -228,6 +229,7 @@ func TestDoEmojisPermissionsMigration(t *testing.T) { model.PermissionEditCustomGroup.Id, model.PermissionDeleteCustomGroup.Id, model.PermissionManageCustomGroupMembers.Id, + model.PermissionRestoreCustomGroup.Id, model.PermissionListPublicTeams.Id, model.PermissionJoinPublicTeams.Id, model.PermissionCreateDirectChannel.Id, diff --git a/app/permissions_migrations.go b/app/permissions_migrations.go index 34661b5d11..3bd85521fe 100644 --- a/app/permissions_migrations.go +++ b/app/permissions_migrations.go @@ -994,6 +994,30 @@ func (a *App) getAddCustomUserGroupsPermissions() (permissionsMap, error) { return t, nil } +func (a *App) getAddCustomUserGroupsPermissionRestore() (permissionsMap, error) { + t := []permissionTransformation{} + + customGroupPermissions := []string{ + model.PermissionRestoreCustomGroup.Id, + } + + t = append(t, permissionTransformation{ + On: isExactRole(model.SystemUserRoleId), + Add: customGroupPermissions, + }) + + t = append(t, permissionTransformation{ + On: isExactRole(model.SystemAdminRoleId), + Add: customGroupPermissions, + }) + + t = append(t, permissionTransformation{ + On: isExactRole(model.SystemCustomGroupAdminRoleId), + Add: customGroupPermissions, + }) + return t, nil +} + func (a *App) getAddPlaybooksPermissions() (permissionsMap, error) { transformations := []permissionTransformation{} @@ -1110,6 +1134,7 @@ func (s *Server) doPermissionsMigrations() error { {Key: model.MigrationKeyAddCustomUserGroupsPermissions, Migration: a.getAddCustomUserGroupsPermissions}, {Key: model.MigrationKeyAddPlayboosksManageRolesPermissions, Migration: a.getPlaybooksPermissionsAddManageRoles}, {Key: model.MigrationKeyAddProductsBoardsPermissions, Migration: a.getProductsBoardsPermissions}, + {Key: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Migration: a.getAddCustomUserGroupsPermissionRestore}, } roles, err := s.Store().Role().GetAll() diff --git a/model/migration.go b/model/migration.go index e0e9ae2267..766e51598a 100644 --- a/model/migration.go +++ b/model/migration.go @@ -39,4 +39,5 @@ const ( MigrationKeyAddCustomUserGroupsPermissions = "custom_groups_permissions" MigrationKeyAddPlayboosksManageRolesPermissions = "playbooks_manage_roles" MigrationKeyAddProductsBoardsPermissions = "products_boards" + MigrationKeyAddCustomUserGroupsPermissionRestore = "custom_groups_permission_restore" ) diff --git a/model/permission.go b/model/permission.go index 76cf07c872..a44a566964 100644 --- a/model/permission.go +++ b/model/permission.go @@ -366,6 +366,7 @@ var PermissionCreateCustomGroup *Permission var PermissionManageCustomGroupMembers *Permission var PermissionEditCustomGroup *Permission var PermissionDeleteCustomGroup *Permission +var PermissionRestoreCustomGroup *Permission var AllPermissions []*Permission var DeprecatedPermissions []*Permission @@ -1960,6 +1961,13 @@ func initializePermissions() { PermissionScopeGroup, } + PermissionRestoreCustomGroup = &Permission{ + "restore_custom_group", + "authentication.permissions.restore_custom_group.name", + "authentication.permissions.restore_custom_group.description", + PermissionScopeGroup, + } + // Playbooks PermissionPublicPlaybookCreate = &Permission{ "playbook_public_create", @@ -2340,6 +2348,7 @@ func initializePermissions() { PermissionManageCustomGroupMembers, PermissionEditCustomGroup, PermissionDeleteCustomGroup, + PermissionRestoreCustomGroup, } DeprecatedPermissions = []*Permission{ diff --git a/model/role.go b/model/role.go index ac3fa3204e..b4a1825537 100644 --- a/model/role.go +++ b/model/role.go @@ -348,6 +348,7 @@ func init() { PermissionCreateCustomGroup.Id, PermissionEditCustomGroup.Id, PermissionDeleteCustomGroup.Id, + PermissionRestoreCustomGroup.Id, PermissionManageCustomGroupMembers.Id, } @@ -953,6 +954,7 @@ func MakeDefaultRoles() map[string]*Role { PermissionCreateCustomGroup.Id, PermissionEditCustomGroup.Id, PermissionDeleteCustomGroup.Id, + PermissionRestoreCustomGroup.Id, PermissionManageCustomGroupMembers.Id, }, SchemeManaged: true, diff --git a/testlib/store.go b/testlib/store.go index 800764da07..9f70dfa8cd 100644 --- a/testlib/store.go +++ b/testlib/store.go @@ -68,6 +68,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("GetByName", model.MigrationKeyAddPlaybooksPermissions).Return(&model.System{Name: model.MigrationKeyAddPlaybooksPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissions).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddPlayboosksManageRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil) + systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissionRestore).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Value: "true"}, nil) systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil) systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil) systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once()