From 9ab1d8f805c6a7e9b560f3f71d051dc0fb51137e Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Mon, 12 Dec 2022 14:59:47 +0530 Subject: [PATCH 01/41] 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 02/41] 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 03/41] 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 04/41] [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 05/41] 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 06/41] 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 07/41] 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 08/41] 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 09/41] 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 10/41] 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 11/41] 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 12/41] 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 13/41] 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 14/41] [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 15/41] [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 16/41] 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 17/41] [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 18/41] 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 19/41] 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 20/41] [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 21/41] 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 22/41] 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 23/41] [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 24/41] 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 25/41] 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 26/41] 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 27/41] 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 28/41] [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 29/41] 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 30/41] [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 31/41] [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 32/41] 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 33/41] 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 34/41] 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 35/41] 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 36/41] 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 37/41] [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() From 0ce7c5e3da8b9a508f0467b31167cd65e50ed2a2 Mon Sep 17 00:00:00 2001 From: Tim Scheuermann Date: Tue, 20 Dec 2022 08:59:40 +0100 Subject: [PATCH 38/41] Use path rather than filepath for embedded files (#21896) Co-authored-by: Tim Scheuermann --- store/sqlstore/store.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 4c1bf1f05b..630e7705d3 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -9,7 +9,7 @@ import ( dbsql "database/sql" "fmt" "log" - "path/filepath" + "path" "strconv" "strings" "sync" @@ -1041,7 +1041,7 @@ func (ss *SqlStore) hasLicense() bool { func (ss *SqlStore) migrate(direction migrationDirection) error { assets := db.Assets() - assetsList, err := assets.ReadDir(filepath.Join("migrations", ss.DriverName())) + assetsList, err := assets.ReadDir(path.Join("migrations", ss.DriverName())) if err != nil { return err } @@ -1054,7 +1054,7 @@ func (ss *SqlStore) migrate(direction migrationDirection) error { src, err := mbindata.WithInstance(&mbindata.AssetSource{ Names: assetNamesForDriver, AssetFunc: func(name string) ([]byte, error) { - return assets.ReadFile(filepath.Join("migrations", ss.DriverName(), name)) + return assets.ReadFile(path.Join("migrations", ss.DriverName(), name)) }, }) if err != nil { From dcf499b51df70cdcb7133da13ddda3e3e6c3b740 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 21 Dec 2022 16:11:57 +0300 Subject: [PATCH 39/41] [MM-48626] Move plugins environment out of Channels (#21730) --- api4/plugin.go | 4 +- api4/plugin_test.go | 2 +- api4/websocket.go | 2 +- app/app_iface.go | 1 + app/channel.go | 10 +- app/channels.go | 74 +----- app/cluster_handlers.go | 11 +- app/collection.go | 16 +- app/download.go | 6 +- app/file.go | 2 +- app/integration_action.go | 6 +- app/login.go | 4 +- app/onboarding.go | 9 +- app/opentracing/opentracing_layer.go | 17 ++ app/plugin.go | 380 +++++++++++++++++---------- app/plugin_api.go | 4 +- app/plugin_api_test.go | 24 +- app/plugin_commands.go | 72 +++-- app/plugin_commands_test.go | 10 +- app/plugin_db_driver.go | 13 +- app/plugin_db_driver_test.go | 2 +- app/plugin_event.go | 6 +- app/plugin_hooks_test.go | 14 +- app/plugin_install.go | 118 ++++----- app/plugin_install_test.go | 8 +- app/plugin_requests.go | 28 +- app/plugin_requests_test.go | 2 +- app/plugin_shutdown_test.go | 2 +- app/plugin_signature.go | 8 +- app/plugin_statuses.go | 36 +-- app/plugin_test.go | 46 ++-- app/post.go | 8 +- app/reaction.go | 4 +- app/server.go | 14 +- app/team.go | 4 +- app/upload.go | 2 +- app/user.go | 2 +- app/web_conn.go | 2 +- cmd/mattermost/commands/init.go | 7 +- web/web_test.go | 4 +- 40 files changed, 537 insertions(+), 447 deletions(-) diff --git a/api4/plugin.go b/api4/plugin.go index be5b298d02..475aa62f21 100644 --- a/api4/plugin.go +++ b/api4/plugin.go @@ -155,7 +155,7 @@ func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request // https://mattermost.atlassian.net/browse/MM-41981 pluginRequest.Version = "" - manifest, appErr := c.App.Channels().InstallMarketplacePlugin(pluginRequest) + manifest, appErr := c.App.PluginService().InstallMarketplacePlugin(pluginRequest) if appErr != nil { c.Err = appErr return @@ -235,7 +235,7 @@ func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) { return } - err := c.App.Channels().RemovePlugin(c.Params.PluginId) + err := c.App.PluginService().RemovePlugin(c.Params.PluginId) if err != nil { c.Err = err return diff --git a/api4/plugin_test.go b/api4/plugin_test.go index 1967f9a617..3b656c209a 100644 --- a/api4/plugin_test.go +++ b/api4/plugin_test.go @@ -94,7 +94,7 @@ func TestPlugin(t *testing.T) { assert.Equal(t, "testplugin", manifest.Id) }) - th.App.Channels().RemovePlugin(manifest.Id) + th.App.PluginService().RemovePlugin(manifest.Id) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) diff --git a/api4/websocket.go b/api4/websocket.go index d2c1c10f44..d8236e49b5 100644 --- a/api4/websocket.go +++ b/api4/websocket.go @@ -61,7 +61,7 @@ func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) { } } - wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv().Channels()) + wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv()) if c.AppContext.Session().UserId != "" { c.App.Srv().Platform().HubRegister(wc) } diff --git a/app/app_iface.go b/app/app_iface.go index f10ca2d8b9..2674801101 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -936,6 +936,7 @@ type AppIface interface { PermanentDeleteTeamId(c request.CTX, teamID string) *model.AppError PermanentDeleteUser(c *request.Context, user *model.User) *model.AppError PluginCommandsForTeam(teamID string) []*model.Command + PluginService() *PluginService PostActionCookieSecret() []byte PostAddToChannelMessage(c request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch diff --git a/app/channel.go b/app/channel.go index fdc8d17da3..31a8673383 100644 --- a/app/channel.go +++ b/app/channel.go @@ -345,7 +345,7 @@ func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember boo a.Srv().Go(func() { pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ChannelHasBeenCreated(pluginContext, sc) return true }, plugin.ChannelHasBeenCreatedID) @@ -429,7 +429,7 @@ func (a *App) handleCreationEvent(c request.CTX, userID, otherUserID string, cha a.Srv().Go(func() { pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ChannelHasBeenCreated(pluginContext, channel) return true }, plugin.ChannelHasBeenCreatedID) @@ -1597,7 +1597,7 @@ func (a *App) AddChannelMember(c request.CTX, userID string, channel *model.Chan a.Srv().Go(func() { pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor) return true }, plugin.UserHasJoinedChannelID) @@ -2173,7 +2173,7 @@ func (a *App) JoinChannel(c request.CTX, channel *model.Channel, userID string) a.Srv().Go(func() { pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedChannel(pluginContext, cm, nil) return true }, plugin.UserHasJoinedChannelID) @@ -2483,7 +2483,7 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove a.Srv().Go(func() { pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLeftChannel(pluginContext, cm, actorUser) return true }, plugin.UserHasLeftChannelID) diff --git a/app/channels.go b/app/channels.go index 7ab024e45a..c9771f25d6 100644 --- a/app/channels.go +++ b/app/channels.go @@ -6,14 +6,11 @@ package app import ( "fmt" "runtime" - "strings" "sync" "github.com/pkg/errors" "github.com/mattermost/mattermost-server/v6/app/imaging" - "github.com/mattermost/mattermost-server/v6/app/request" - "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/einterfaces" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" @@ -42,12 +39,6 @@ type Channels struct { postActionCookieSecret []byte - pluginCommandsLock sync.RWMutex - pluginCommands []*PluginCommand - pluginsLock sync.RWMutex - pluginsEnvironment *plugin.Environment - pluginConfigListenerID string - imageProxy *imageproxy.ImageProxy // cached counts that are used during notice condition validation @@ -79,12 +70,6 @@ type Channels struct { postReminderMut sync.Mutex postReminderTask *model.ScheduledTask - - // collectionTypes maps from collection types to the registering plugin id - collectionTypes map[string]string - // topicTypes maps from topic types to collection types - topicTypes map[string]string - collectionAndTopicTypesMut sync.Mutex } func init() { @@ -107,11 +92,9 @@ func NewChannels(services map[product.ServiceKey]any) (*Channels, error) { return nil, errors.New("server not passed") } ch := &Channels{ - srv: s, - imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()), - uploadLockMap: map[string]bool{}, - collectionTypes: map[string]string{}, - topicTypes: map[string]string{}, + srv: s, + imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()), + uploadLockMap: map[string]bool{}, } // To get another service: @@ -208,10 +191,6 @@ func NewChannels(services map[product.ServiceKey]any) (*Channels, error) { services[product.RouterKey] = ch.routerSvc // Setup routes. - pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() - pluginsRoute.HandleFunc("", ch.ServePluginRequest) - pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest) - pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest) services[product.PostKey] = &postServiceWrapper{ app: &App{ch: ch}, @@ -243,39 +222,6 @@ func NewChannels(services map[product.ServiceKey]any) (*Channels, error) { } func (ch *Channels) Start() error { - // Start plugins - ctx := request.EmptyContext(ch.srv.Log()) - ch.initPlugins(ctx, *ch.cfgSvc.Config().PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory) - - ch.AddConfigListener(func(prevCfg, cfg *model.Config) { - // We compute the difference between configs - // to ensure we don't re-init plugins unnecessarily. - diffs, err := config.Diff(prevCfg, cfg) - if err != nil { - ch.srv.Log().Warn("Error in comparing configs", mlog.Err(err)) - return - } - - hasDiff := false - // TODO: This could be a method on ConfigDiffs itself - for _, diff := range diffs { - if strings.HasPrefix(diff.Path, "PluginSettings.") { - hasDiff = true - break - } - } - - // Do only if some plugin related settings has changed. - if hasDiff { - if *cfg.PluginSettings.Enable { - ch.initPlugins(ctx, *cfg.PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory) - } else { - ch.ShutDownPlugins() - } - } - - }) - // TODO: This should be moved to the platform service. if err := ch.srv.platform.EnsureAsymmetricSigningKey(); err != nil { return errors.Wrapf(err, "unable to ensure asymmetric signing key") @@ -289,8 +235,6 @@ func (ch *Channels) Start() error { } func (ch *Channels) Stop() error { - ch.ShutDownPlugins() - ch.dndTaskMut.Lock() if ch.dndTask != nil { ch.dndTask.Cancel() @@ -332,18 +276,18 @@ func (s *hooksService) RegisterHooks(productID string, hooks any) error { return s.ch.srv.hooksManager.AddProduct(productID, hooks) } -func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) { - if env := ch.GetPluginsEnvironment(); env != nil { +func (s *Server) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) { + if env := s.pluginService.GetPluginsEnvironment(); env != nil { env.RunMultiPluginHook(hookRunnerFunc, hookId) } // run hook for the products - ch.srv.hooksManager.RunMultiHook(hookRunnerFunc, hookId) + s.hooksManager.RunMultiHook(hookRunnerFunc, hookId) } -func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) { +func (s *Server) HooksForPluginOrProduct(id string) (plugin.Hooks, error) { var hooks plugin.Hooks - if env := ch.GetPluginsEnvironment(); env != nil { + if env := s.pluginService.GetPluginsEnvironment(); env != nil { // we intentionally ignore the error here, because the id can be a product id // we are going to check if we have the hooks or not hooks, _ = env.HooksForPlugin(id) @@ -352,7 +296,7 @@ func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) { } } - hooks = ch.srv.hooksManager.HooksForProduct(id) + hooks = s.hooksManager.HooksForProduct(id) if hooks != nil { return hooks, nil } diff --git a/app/cluster_handlers.go b/app/cluster_handlers.go index 3fa90abf1e..1cebe5596d 100644 --- a/app/cluster_handlers.go +++ b/app/cluster_handlers.go @@ -16,7 +16,7 @@ func (s *Server) clusterInstallPluginHandler(msg *model.ClusterMessage) { if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil { mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr)) } - s.Channels().installPluginFromData(data) + s.pluginService.installPluginFromData(data) } func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) { @@ -24,7 +24,7 @@ func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) { if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil { mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr)) } - s.Channels().removePluginFromData(data) + s.pluginService.removePluginFromData(data) } func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) { @@ -44,12 +44,7 @@ func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) { return } - channels, ok := s.products["channels"].(*Channels) - if !ok { - return - } - - hooks, err := channels.HooksForPluginOrProduct(pluginID) + hooks, err := s.HooksForPluginOrProduct(pluginID) if err != nil { mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err)) return diff --git a/app/collection.go b/app/collection.go index 9b895e3bc0..ff489a18db 100644 --- a/app/collection.go +++ b/app/collection.go @@ -10,26 +10,26 @@ import ( "github.com/mattermost/mattermost-server/v6/shared/mlog" ) -func (a *App) registerCollectionAndTopic(pluginID, collectionType, topicType string) error { +func (s *PluginService) registerCollectionAndTopic(pluginID, collectionType, topicType string) error { // we have a race condition due to multiple plugins calling this method - a.ch.collectionAndTopicTypesMut.Lock() - defer a.ch.collectionAndTopicTypesMut.Unlock() + s.collectionAndTopicTypesMut.Lock() + defer s.collectionAndTopicTypesMut.Unlock() // check if collectionType was already registered by other plugin - existingPluginID, ok := a.ch.collectionTypes[collectionType] + existingPluginID, ok := s.collectionTypes[collectionType] if ok && existingPluginID != pluginID { return model.NewAppError("registerCollectionAndTopic", "app.collection.add_collection.exists.app_error", nil, "", http.StatusBadRequest) } // check if topicType was already registered to other collection - existingCollectionType, ok := a.ch.topicTypes[topicType] + existingCollectionType, ok := s.topicTypes[topicType] if ok && existingCollectionType != collectionType { return model.NewAppError("registerCollectionAndTopic", "app.collection.add_topic.exists.app_error", nil, "", http.StatusBadRequest) } - a.ch.collectionTypes[collectionType] = pluginID - a.ch.topicTypes[topicType] = collectionType + s.collectionTypes[collectionType] = pluginID + s.topicTypes[topicType] = collectionType - a.ch.srv.Log().Info("registered collection and topic type", mlog.String("plugin_id", pluginID), mlog.String("collection_type", collectionType), mlog.String("topic_type", topicType)) + s.platform.Log().Info("registered collection and topic type", mlog.String("plugin_id", pluginID), mlog.String("collection_type", collectionType), mlog.String("topic_type", topicType)) return nil } diff --git a/app/download.go b/app/download.go index 449f787c46..56438507cc 100644 --- a/app/download.go +++ b/app/download.go @@ -22,10 +22,10 @@ const ( ) func (a *App) DownloadFromURL(downloadURL string) ([]byte, error) { - return a.Srv().downloadFromURL(downloadURL) + return a.Srv().pluginService.downloadFromURL(downloadURL) } -func (s *Server) downloadFromURL(downloadURL string) ([]byte, error) { +func (s *PluginService) downloadFromURL(downloadURL string) ([]byte, error) { if !model.IsValidHTTPURL(downloadURL) { return nil, errors.Errorf("invalid url %s", downloadURL) } @@ -38,7 +38,7 @@ func (s *Server) downloadFromURL(downloadURL string) ([]byte, error) { return nil, errors.Errorf("insecure url not allowed %s", downloadURL) } - client := s.HTTPService().MakeClient(true) + client := s.httpService.MakeClient(true) client.Timeout = HTTPRequestTimeout var resp *http.Response diff --git a/app/file.go b/app/file.go index 7a66d25010..e1acafa827 100644 --- a/app/file.go +++ b/app/file.go @@ -926,7 +926,7 @@ func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTe var rejectionError *model.AppError pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { var newBytes bytes.Buffer replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes) if rejectionReason != "" { diff --git a/app/integration_action.go b/app/integration_action.go index 4ae7e97e07..51bef2b86f 100644 --- a/app/integration_action.go +++ b/app/integration_action.go @@ -375,10 +375,10 @@ func (w *LocalResponseWriter) WriteHeader(statusCode int) { } func (a *App) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) { - return a.ch.doPluginRequest(c, method, rawURL, values, body) + return a.ch.srv.pluginService.doPluginRequest(c, method, rawURL, values, body) } -func (ch *Channels) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) { +func (s *PluginService) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) { rawURL = strings.TrimPrefix(rawURL, "/") inURL, err := url.Parse(rawURL) if err != nil { @@ -427,7 +427,7 @@ func (ch *Channels) doPluginRequest(c *request.Context, method, rawURL string, v params["plugin_id"] = pluginID r = mux.SetURLVars(r, params) - ch.ServePluginRequest(w, r) + s.ServePluginRequest(w, r) resp := &http.Response{ StatusCode: w.status, diff --git a/app/login.go b/app/login.go index e0854bef37..af322bd13b 100644 --- a/app/login.go +++ b/app/login.go @@ -159,7 +159,7 @@ func (a *App) GetUserForLogin(id, loginId string) (*model.User, *model.AppError) func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile, isOAuthUser, isSaml bool) *model.AppError { var rejectionReason string pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { rejectionReason = hooks.UserWillLogIn(pluginContext, user) return rejectionReason == "" }, plugin.UserWillLogInID) @@ -225,7 +225,7 @@ func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request } a.Srv().Go(func() { - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLoggedIn(pluginContext, user) return true }, plugin.UserHasLoggedInID) diff --git a/app/onboarding.go b/app/onboarding.go index d76525f017..fe40e9d997 100644 --- a/app/onboarding.go +++ b/app/onboarding.go @@ -28,11 +28,6 @@ func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError { } func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError { - pluginsEnvironment := a.Channels().GetPluginsEnvironment() - if pluginsEnvironment == nil { - return a.markAdminOnboardingComplete(c) - } - pluginContext := pluginContext(c) for _, pluginID := range request.InstallPlugins { @@ -41,7 +36,7 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo installRequest := &model.InstallMarketplacePluginRequest{ Id: id, } - _, appErr := a.Channels().InstallMarketplacePlugin(installRequest) + _, appErr := a.Srv().pluginService.InstallMarketplacePlugin(installRequest) if appErr != nil { mlog.Error("Failed to install plugin for onboarding", mlog.String("id", id), mlog.Err(appErr)) return @@ -53,7 +48,7 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo return } - hooks, err := a.ch.HooksForPluginOrProduct(id) + hooks, err := a.Srv().HooksForPluginOrProduct(id) if err != nil { mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", id), mlog.Err(err)) return diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 367d98ec39..3d6c2b5ad5 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -13125,6 +13125,23 @@ func (a *OpenTracingAppLayer) PluginCommandsForTeam(teamID string) []*model.Comm return resultVar0 } +func (a *OpenTracingAppLayer) PluginService() *app.PluginService { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PluginService") + + 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.PluginService() + + return resultVar0 +} + func (a *OpenTracingAppLayer) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PopulateWebConnConfig") diff --git a/app/plugin.go b/app/plugin.go index 679576e723..d985455e5b 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -20,16 +20,37 @@ import ( svg "github.com/h2non/go-is-svg" "github.com/pkg/errors" + "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/app/request" + "github.com/mattermost/mattermost-server/v6/config" "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/shared/filestore" "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/utils/fileutils" ) +type PluginService struct { + platform *platform.PlatformService + channels *Channels + fileStore filestore.FileBackend + httpService httpservice.HTTPService + + pluginCommandsLock sync.RWMutex + pluginCommands []*PluginCommand + pluginsLock sync.RWMutex + pluginsEnvironment *plugin.Environment + pluginConfigListenerID string + // collectionTypes maps from collection types to the registering plugin id + collectionTypes map[string]string + // topicTypes maps from topic types to collection types + topicTypes map[string]string + collectionAndTopicTypesMut sync.Mutex +} + const prepackagedPluginsDir = "prepackaged_plugins" type pluginSignaturePath struct { @@ -63,20 +84,91 @@ func (rs *routerService) getHandler(productID string) (http.Handler, bool) { return handler, ok } +func (a *App) PluginService() *PluginService { + return a.ch.srv.pluginService +} + +func (s *Server) InitializePluginService() error { + product, ok := s.products["channels"] + if !ok { + return errors.New("unable to find channels product") + } + channels, ok := product.(*Channels) + if !ok { + return errors.New("unable to cast product to channels product") + } + + ps := &PluginService{ + platform: s.platform, + channels: channels, + fileStore: s.platform.FileBackend(), + httpService: s.httpService, + collectionTypes: make(map[string]string), + topicTypes: make(map[string]string), + } + s.pluginService = ps + + pluginsRoute := s.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() + pluginsRoute.HandleFunc("", ps.ServePluginRequest) + pluginsRoute.HandleFunc("/public/{public_file:.*}", ps.ServePluginPublicRequest) + pluginsRoute.HandleFunc("/{anything:.*}", ps.ServePluginRequest) + + ps.initPlugins(request.EmptyContext(s.platform.Log()), *s.platform.Config().PluginSettings.Directory, *s.platform.Config().PluginSettings.ClientDirectory) + + // Start plugins + ctx := request.EmptyContext(s.platform.Log()) + + // Add the config listener to enable/disable plugins + s.platform.AddConfigListener(func(prevCfg, cfg *model.Config) { + // We compute the difference between configs + // to ensure we don't re-init plugins unnecessarily. + diffs, err := config.Diff(prevCfg, cfg) + if err != nil { + s.platform.Log().Warn("Error in comparing configs", mlog.Err(err)) + return + } + + hasDiff := false + // TODO: This could be a method on ConfigDiffs itself + for _, diff := range diffs { + if strings.HasPrefix(diff.Path, "PluginSettings.") { + hasDiff = true + break + } + } + + // Do only if some plugin related settings has changed. + if hasDiff { + if *cfg.PluginSettings.Enable { + s.pluginService.initPlugins(ctx, *cfg.PluginSettings.Directory, *s.Config().PluginSettings.ClientDirectory) + } else { + s.pluginService.ShutDownPlugins() + } + } + + }) + + return nil +} + +func (s *Server) GetPluginsEnvironment() *plugin.Environment { + return s.pluginService.GetPluginsEnvironment() +} + // GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and // initialized. // // To get the plugins environment when the plugins are disabled, manually acquire the plugins // lock instead. -func (ch *Channels) GetPluginsEnvironment() *plugin.Environment { - if !*ch.cfgSvc.Config().PluginSettings.Enable { +func (s *PluginService) GetPluginsEnvironment() *plugin.Environment { + if !*s.platform.Config().PluginSettings.Enable { return nil } - ch.pluginsLock.RLock() - defer ch.pluginsLock.RUnlock() + s.pluginsLock.RLock() + defer s.pluginsLock.RUnlock() - return ch.pluginsEnvironment + return s.pluginsEnvironment } // GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and @@ -85,33 +177,39 @@ func (ch *Channels) GetPluginsEnvironment() *plugin.Environment { // To get the plugins environment when the plugins are disabled, manually acquire the plugins // lock instead. func (a *App) GetPluginsEnvironment() *plugin.Environment { - return a.ch.GetPluginsEnvironment() + // TODO: Telemetry service starts before products start, so we need to check if the plugin service is initialized. + // Move the telemetry service to start after products start. + if a.ch.srv.pluginService == nil { + return nil + } + + return a.ch.srv.pluginService.GetPluginsEnvironment() } -func (ch *Channels) SetPluginsEnvironment(pluginsEnvironment *plugin.Environment) { - ch.pluginsLock.Lock() - defer ch.pluginsLock.Unlock() +func (s *PluginService) SetPluginsEnvironment(pluginsEnvironment *plugin.Environment) { + s.pluginsLock.Lock() + defer s.pluginsLock.Unlock() - ch.pluginsEnvironment = pluginsEnvironment - ch.srv.Platform().SetPluginsEnvironment(ch) + s.pluginsEnvironment = pluginsEnvironment + s.platform.SetPluginsEnvironment(s.channels.srv) } -func (ch *Channels) syncPluginsActiveState() { +func (s *PluginService) syncPluginsActiveState() { // Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment. - ch.pluginsLock.RLock() - pluginsEnvironment := ch.pluginsEnvironment - ch.pluginsLock.RUnlock() + s.pluginsLock.RLock() + pluginsEnvironment := s.pluginsEnvironment + s.pluginsLock.RUnlock() if pluginsEnvironment == nil { return } - config := ch.cfgSvc.Config().PluginSettings + config := s.platform.Config().PluginSettings if *config.Enable { availablePlugins, err := pluginsEnvironment.Available() if err != nil { - ch.srv.Log().Error("Unable to get available plugins", mlog.Err(err)) + s.platform.Log().Error("Unable to get available plugins", mlog.Err(err)) return } @@ -125,24 +223,24 @@ func (ch *Channels) syncPluginsActiveState() { pluginEnabled = state.Enable } - if hasOverride, value := ch.getPluginStateOverride(pluginID); hasOverride { + if hasOverride, value := s.getPluginStateOverride(pluginID); hasOverride { pluginEnabled = value } if pluginEnabled { // Disable focalboard in product mode. - if pluginID == model.PluginIdFocalboard && ch.cfgSvc.Config().FeatureFlags.BoardsProduct { + if pluginID == model.PluginIdFocalboard && s.platform.Config().FeatureFlags.BoardsProduct { msg := "Plugin cannot run in product mode. Disabling." mlog.Warn(msg, mlog.String("plugin_id", model.PluginIdFocalboard)) // This is a mini-version of ch.disablePlugin. // We don't call that directly, because that will recursively call // this method. - ch.cfgSvc.UpdateConfig(func(cfg *model.Config) { + s.platform.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[pluginID] = &model.PluginState{Enable: false} }) pluginsEnvironment.SetPluginError(pluginID, msg) - ch.unregisterPluginCommands(pluginID) + s.unregisterPluginCommands(pluginID) disabledPlugins = append(disabledPlugins, plugin) continue } @@ -166,7 +264,7 @@ func (ch *Channels) syncPluginsActiveState() { if deactivated && plugin.Manifest.HasClient() { message := model.NewWebSocketEvent(model.WebsocketEventPluginDisabled, "", "", "", nil, "") message.Add("manifest", plugin.Manifest.ClientManifest()) - ch.srv.platform.Publish(message) + s.platform.Publish(message) } }(plugin) } @@ -180,14 +278,14 @@ func (ch *Channels) syncPluginsActiveState() { pluginID := plugin.Manifest.Id updatedManifest, activated, err := pluginsEnvironment.Activate(pluginID) if err != nil { - plugin.WrapLogger(ch.srv.Log()).Error("Unable to activate plugin", mlog.Err(err)) + plugin.WrapLogger(s.platform.Log().(*mlog.Logger)).Error("Unable to activate plugin", mlog.Err(err)) return } if activated { // Notify all cluster clients if ready - if err := ch.notifyPluginEnabled(updatedManifest); err != nil { - ch.srv.Log().Error("Failed to notify cluster on plugin enable", mlog.Err(err)) + if err := s.notifyPluginEnabled(updatedManifest); err != nil { + s.platform.Log().Error("Failed to notify cluster on plugin enable", mlog.Err(err)) } } }(plugin) @@ -197,7 +295,7 @@ func (ch *Channels) syncPluginsActiveState() { pluginsEnvironment.Shutdown() } - if err := ch.notifyPluginStatusesChanged(); err != nil { + if err := s.notifyPluginStatusesChanged(); err != nil { mlog.Warn("failed to notify plugin status changed", mlog.Err(err)) } } @@ -207,27 +305,29 @@ func (a *App) NewPluginAPI(c *request.Context, manifest *model.Manifest) plugin. } func (a *App) InitPlugins(c *request.Context, pluginDir, webappPluginDir string) { - a.ch.initPlugins(c, pluginDir, webappPluginDir) + a.ch.srv.pluginService.initPlugins(c, pluginDir, webappPluginDir) } -func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir string) { +func (s *PluginService) initPlugins(c *request.Context, pluginDir, webappPluginDir string) { // Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment. defer func() { - ch.srv.Platform().SetPluginsEnvironment(ch) + // platform service requires plugins environment to be initialized + // so that it can use it in cluster service initialization + s.platform.SetPluginsEnvironment(s.channels.srv) }() - ch.pluginsLock.RLock() - pluginsEnvironment := ch.pluginsEnvironment - ch.pluginsLock.RUnlock() - if pluginsEnvironment != nil || !*ch.cfgSvc.Config().PluginSettings.Enable { - ch.syncPluginsActiveState() + s.pluginsLock.RLock() + pluginsEnvironment := s.pluginsEnvironment + s.pluginsLock.RUnlock() + if pluginsEnvironment != nil || !*s.platform.Config().PluginSettings.Enable { + s.syncPluginsActiveState() if pluginsEnvironment != nil { - pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck) + pluginsEnvironment.TogglePluginHealthCheckJob(*s.platform.Config().PluginSettings.EnableHealthCheck) } return } - ch.srv.Log().Info("Starting up plugins") + s.platform.Log().Info("Starting up plugins") if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) { mlog.Error("Failed to start up plugins", mlog.Err(err)) @@ -240,77 +340,77 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s } newAPIFunc := func(manifest *model.Manifest) plugin.API { - return New(ServerConnector(ch)).NewPluginAPI(c, manifest) + return New(ServerConnector(s.channels)).NewPluginAPI(c, manifest) } env, err := plugin.NewEnvironment( newAPIFunc, - NewDriverImpl(ch.srv), + NewDriverImpl(s.platform), pluginDir, webappPluginDir, - *ch.cfgSvc.Config().ExperimentalSettings.PatchPluginsReactDOM, - ch.srv.Log(), - ch.srv.GetMetrics(), + *s.platform.Config().ExperimentalSettings.PatchPluginsReactDOM, + s.platform.Logger(), + s.platform.Metrics(), ) if err != nil { mlog.Error("Failed to start up plugins", mlog.Err(err)) return } - ch.pluginsLock.Lock() - ch.pluginsEnvironment = env - ch.pluginsLock.Unlock() + s.pluginsLock.Lock() + s.pluginsEnvironment = env + s.pluginsLock.Unlock() - ch.pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck) + s.pluginsEnvironment.TogglePluginHealthCheckJob(*s.platform.Config().PluginSettings.EnableHealthCheck) - if err := ch.syncPlugins(); err != nil { + if err := s.syncPlugins(); err != nil { mlog.Error("Failed to sync plugins from the file store", mlog.Err(err)) } - plugins := ch.processPrepackagedPlugins(prepackagedPluginsDir) - pluginsEnvironment = ch.GetPluginsEnvironment() + plugins := s.processPrepackagedPlugins(prepackagedPluginsDir) + pluginsEnvironment = s.GetPluginsEnvironment() if pluginsEnvironment == nil { mlog.Info("Plugins environment not found, server is likely shutting down") return } pluginsEnvironment.SetPrepackagedPlugins(plugins) - ch.installFeatureFlagPlugins() + s.installFeatureFlagPlugins() // Sync plugin active state when config changes. Also notify plugins. - ch.pluginsLock.Lock() - ch.RemoveConfigListener(ch.pluginConfigListenerID) - ch.pluginConfigListenerID = ch.AddConfigListener(func(old, new *model.Config) { + s.pluginsLock.Lock() + s.platform.RemoveConfigListener(s.pluginConfigListenerID) + s.pluginConfigListenerID = s.platform.AddConfigListener(func(old, new *model.Config) { // If plugin status remains unchanged, only then run this. // Because (*App).InitPlugins is already run as a config change hook. if *old.PluginSettings.Enable == *new.PluginSettings.Enable { - ch.installFeatureFlagPlugins() - ch.syncPluginsActiveState() + s.installFeatureFlagPlugins() + s.syncPluginsActiveState() } - ch.RunMultiHook(func(hooks plugin.Hooks) bool { + s.pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { if err := hooks.OnConfigurationChange(); err != nil { - ch.srv.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err)) + s.platform.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err)) } return true }, plugin.OnConfigurationChangeID) }) - ch.pluginsLock.Unlock() + s.pluginsLock.Unlock() - ch.syncPluginsActiveState() + s.syncPluginsActiveState() } // SyncPlugins synchronizes the plugins installed locally // with the plugin bundles available in the file store. func (a *App) SyncPlugins() *model.AppError { - return a.ch.syncPlugins() + return a.ch.srv.pluginService.syncPlugins() } // SyncPlugins synchronizes the plugins installed locally // with the plugin bundles available in the file store. -func (ch *Channels) syncPlugins() *model.AppError { +func (s *PluginService) syncPlugins() *model.AppError { mlog.Info("Syncing plugins from the file store") - pluginsEnvironment := ch.GetPluginsEnvironment() + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("SyncPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -326,14 +426,14 @@ func (ch *Channels) syncPlugins() *model.AppError { go func(pluginID string) { defer wg.Done() // Only handle managed plugins with .filestore flag file. - _, err := os.Stat(filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, pluginID, managedPluginFileName)) + _, err := os.Stat(filepath.Join(*s.platform.Config().PluginSettings.Directory, pluginID, managedPluginFileName)) if os.IsNotExist(err) { mlog.Warn("Skipping sync for unmanaged plugin", mlog.String("plugin_id", pluginID)) } else if err != nil { mlog.Error("Skipping sync for plugin after failure to check if managed", mlog.String("plugin_id", pluginID), mlog.Err(err)) } else { mlog.Debug("Removing local installation of managed plugin before sync", mlog.String("plugin_id", pluginID)) - if err := ch.removePluginLocally(pluginID); err != nil { + if err := s.removePluginLocally(pluginID); err != nil { mlog.Error("Failed to remove local installation of managed plugin before sync", mlog.String("plugin_id", pluginID), mlog.Err(err)) } } @@ -342,7 +442,7 @@ func (ch *Channels) syncPlugins() *model.AppError { wg.Wait() // Install plugins from the file store. - pluginSignaturePathMap, appErr := ch.getPluginsFromFolder() + pluginSignaturePathMap, appErr := s.getPluginsFromFolder() if appErr != nil { return appErr } @@ -351,7 +451,7 @@ func (ch *Channels) syncPlugins() *model.AppError { wg.Add(1) go func(plugin *pluginSignaturePath) { defer wg.Done() - reader, appErr := ch.srv.fileReader(plugin.path) + reader, appErr := s.fileStore.Reader(plugin.path) if appErr != nil { mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr)) return @@ -359,8 +459,8 @@ func (ch *Channels) syncPlugins() *model.AppError { defer reader.Close() var signature filestore.ReadCloseSeeker - if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature { - signature, appErr = ch.srv.fileReader(plugin.signaturePath) + if *s.platform.Config().PluginSettings.RequirePluginSignature { + signature, appErr = s.fileStore.Reader(plugin.signaturePath) if appErr != nil { mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr)) return @@ -369,7 +469,7 @@ func (ch *Channels) syncPlugins() *model.AppError { } mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path)) - if _, err := ch.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil { + if _, err := s.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil { mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err)) } }(plugin) @@ -379,11 +479,11 @@ func (ch *Channels) syncPlugins() *model.AppError { return nil } -func (ch *Channels) ShutDownPlugins() { +func (s *PluginService) ShutDownPlugins() { // Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment. - ch.pluginsLock.RLock() - pluginsEnvironment := ch.pluginsEnvironment - ch.pluginsLock.RUnlock() + s.pluginsLock.RLock() + pluginsEnvironment := s.pluginsEnvironment + s.pluginsLock.RUnlock() if pluginsEnvironment == nil { return } @@ -392,14 +492,14 @@ func (ch *Channels) ShutDownPlugins() { pluginsEnvironment.Shutdown() - ch.RemoveConfigListener(ch.pluginConfigListenerID) - ch.pluginConfigListenerID = "" + s.platform.RemoveConfigListener(s.pluginConfigListenerID) + s.pluginConfigListenerID = "" // Acquiring lock manually before cleaning up PluginsEnvironment. - ch.pluginsLock.Lock() - defer ch.pluginsLock.Unlock() - if ch.pluginsEnvironment == pluginsEnvironment { - ch.pluginsEnvironment = nil + s.pluginsLock.Lock() + defer s.pluginsLock.Unlock() + if s.pluginsEnvironment == pluginsEnvironment { + s.pluginsEnvironment = nil } else { mlog.Warn("Another PluginsEnvironment detected while shutting down plugins.") } @@ -425,11 +525,11 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) { // activation if inactive anywhere in the cluster. // Notifies cluster peers through config change. func (a *App) EnablePlugin(id string) *model.AppError { - return a.ch.enablePlugin(id) + return a.PluginService().enablePlugin(id) } -func (ch *Channels) enablePlugin(id string) *model.AppError { - pluginsEnvironment := ch.GetPluginsEnvironment() +func (s *PluginService) enablePlugin(id string) *model.AppError { + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -453,16 +553,16 @@ func (ch *Channels) enablePlugin(id string) *model.AppError { return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound) } - if id == model.PluginIdFocalboard && ch.cfgSvc.Config().FeatureFlags.BoardsProduct { + if id == model.PluginIdFocalboard && s.platform.Config().FeatureFlags.BoardsProduct { return model.NewAppError("EnablePlugin", "app.plugin.product_mode.app_error", map[string]any{"Name": model.PluginIdFocalboard}, "", http.StatusBadRequest) } - ch.cfgSvc.UpdateConfig(func(cfg *model.Config) { + s.platform.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true} }) // This call will implicitly invoke SyncPluginsActiveState which will activate enabled plugins. - if _, _, err := ch.cfgSvc.SaveConfig(ch.cfgSvc.Config(), true); err != nil { + if _, _, err := s.platform.SaveConfig(s.platform.Config(), true); err != nil { if err.Id == "ent.cluster.save_config.error" { return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError) } @@ -475,7 +575,7 @@ func (ch *Channels) enablePlugin(id string) *model.AppError { // DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active. // Notifies cluster peers through config change. func (a *App) DisablePlugin(id string) *model.AppError { - appErr := a.ch.disablePlugin(id) + appErr := a.ch.srv.pluginService.disablePlugin(id) if appErr != nil { return appErr } @@ -483,22 +583,22 @@ func (a *App) DisablePlugin(id string) *model.AppError { return nil } -func (ch *Channels) disablePlugin(id string) *model.AppError { +func (s *PluginService) disablePlugin(id string) *model.AppError { // find all collectionTypes registered by plugin - for collectionTypeToRemove, existingPluginId := range ch.collectionTypes { + for collectionTypeToRemove, existingPluginId := range s.collectionTypes { if existingPluginId != id { continue } // find all topicTypes for existing collectionType - for topicTypeToRemove, existingCollectionType := range ch.topicTypes { + for topicTypeToRemove, existingCollectionType := range s.topicTypes { if existingCollectionType == collectionTypeToRemove { - delete(ch.topicTypes, topicTypeToRemove) + delete(s.topicTypes, topicTypeToRemove) } } - delete(ch.collectionTypes, collectionTypeToRemove) + delete(s.collectionTypes, collectionTypeToRemove) } - pluginsEnvironment := ch.GetPluginsEnvironment() + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -522,13 +622,13 @@ func (ch *Channels) disablePlugin(id string) *model.AppError { return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound) } - ch.cfgSvc.UpdateConfig(func(cfg *model.Config) { + s.platform.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false} }) - ch.unregisterPluginCommands(id) + s.unregisterPluginCommands(id) // This call will implicitly invoke SyncPluginsActiveState which will deactivate disabled plugins. - if _, _, err := ch.cfgSvc.SaveConfig(ch.cfgSvc.Config(), true); err != nil { + if _, _, err := s.platform.SaveConfig(s.platform.Config(), true); err != nil { return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -615,8 +715,8 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m // getPrepackagedPlugin returns a pre-packaged plugin. // // If version is empty, the first matching plugin is returned. -func (ch *Channels) getPrepackagedPlugin(pluginID, version string) (*plugin.PrepackagedPlugin, *model.AppError) { - pluginsEnvironment := ch.GetPluginsEnvironment() +func (s *PluginService) getPrepackagedPlugin(pluginID, version string) (*plugin.PrepackagedPlugin, *model.AppError) { + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.config.app_error", nil, "plugin environment is nil", http.StatusInternalServerError) } @@ -634,16 +734,16 @@ func (ch *Channels) getPrepackagedPlugin(pluginID, version string) (*plugin.Prep // getRemoteMarketplacePlugin returns plugin from marketplace-server. // // If version is empty, the latest compatible version is used. -func (ch *Channels) getRemoteMarketplacePlugin(pluginID, version string) (*model.BaseMarketplacePlugin, *model.AppError) { +func (s *PluginService) getRemoteMarketplacePlugin(pluginID, version string) (*model.BaseMarketplacePlugin, *model.AppError) { marketplaceClient, err := marketplace.NewClient( - *ch.cfgSvc.Config().PluginSettings.MarketplaceURL, - ch.srv.HTTPService(), + *s.platform.Config().PluginSettings.MarketplaceURL, + s.httpService, ) if err != nil { return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - filter := ch.getBaseMarketplaceFilter() + filter := s.getBaseMarketplaceFilter() filter.PluginId = pluginID var plugin *model.BaseMarketplacePlugin @@ -798,15 +898,15 @@ func (a *App) mergeLocalPlugins(remoteMarketplacePlugins map[string]*model.Marke } func (a *App) getBaseMarketplaceFilter() *model.MarketplacePluginFilter { - return a.ch.getBaseMarketplaceFilter() + return a.ch.srv.pluginService.getBaseMarketplaceFilter() } -func (ch *Channels) getBaseMarketplaceFilter() *model.MarketplacePluginFilter { +func (s *PluginService) getBaseMarketplaceFilter() *model.MarketplacePluginFilter { filter := &model.MarketplacePluginFilter{ ServerVersion: model.CurrentVersion, } - license := ch.srv.License() + license := s.platform.License() if license != nil && license.HasEnterpriseMarketplacePlugins() { filter.EnterprisePlugins = true } @@ -853,8 +953,8 @@ func pluginMatchesFilter(manifest *model.Manifest, filter string) bool { // it will notify all connected websocket clients (across all peers) to trigger the (re-)installation. // There is a small chance that this never occurs, because the last server to finish installing dies before it can announce. // There is also a chance that multiple servers notify, but the webapp handles this idempotently. -func (ch *Channels) notifyPluginEnabled(manifest *model.Manifest) error { - pluginsEnvironment := ch.GetPluginsEnvironment() +func (s *PluginService) notifyPluginEnabled(manifest *model.Manifest) error { + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return errors.New("pluginsEnvironment is nil") } @@ -864,15 +964,15 @@ func (ch *Channels) notifyPluginEnabled(manifest *model.Manifest) error { var statuses model.PluginStatuses - if ch.srv.platform.Cluster() != nil { + if s.platform.Cluster() != nil { var err *model.AppError - statuses, err = ch.srv.platform.Cluster().GetPluginStatuses() + statuses, err = s.platform.Cluster().GetPluginStatuses() if err != nil { return err } } - localStatus, err := ch.GetPluginStatus(manifest.Id) + localStatus, err := s.GetPluginStatus(manifest.Id) if err != nil { return err } @@ -892,26 +992,26 @@ func (ch *Channels) notifyPluginEnabled(manifest *model.Manifest) error { // Notify all cluster peer clients. message := model.NewWebSocketEvent(model.WebsocketEventPluginEnabled, "", "", "", nil, "") message.Add("manifest", manifest.ClientManifest()) - ch.srv.platform.Publish(message) + s.platform.Publish(message) return nil } -func (ch *Channels) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.AppError) { - fileStorePaths, appErr := ch.srv.listDirectory(fileStorePluginFolder, false) +func (s *PluginService) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.AppError) { + fileStorePaths, appErr := s.fileStore.ListDirectory(fileStorePluginFolder) if appErr != nil { return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) } - return ch.getPluginsFromFilePaths(fileStorePaths), nil + return s.getPluginsFromFilePaths(fileStorePaths), nil } -func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath { +func (s *PluginService) getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath { pluginSignaturePathMap := make(map[string]*pluginSignaturePath) fsPrefix := "" - if *ch.cfgSvc.Config().FileSettings.DriverName == model.ImageDriverS3 { - ptr := ch.cfgSvc.Config().FileSettings.AmazonS3PathPrefix + if *s.platform.Config().FileSettings.DriverName == model.ImageDriverS3 { + ptr := s.platform.Config().FileSettings.AmazonS3PathPrefix if ptr != nil && *ptr != "" { fsPrefix = *ptr + "/" } @@ -944,7 +1044,7 @@ func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string] return pluginSignaturePathMap } -func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin { +func (s *PluginService) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin { prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir) if !found { return nil @@ -960,7 +1060,7 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa return nil } - pluginSignaturePathMap := ch.getPluginsFromFilePaths(fileStorePaths) + pluginSignaturePathMap := s.getPluginsFromFilePaths(fileStorePaths) plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap)) prepackagedPlugins := make(chan *plugin.PrepackagedPlugin, len(pluginSignaturePathMap)) @@ -969,7 +1069,7 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa wg.Add(1) go func(psPath *pluginSignaturePath) { defer wg.Done() - p, err := ch.processPrepackagedPlugin(psPath) + p, err := s.processPrepackagedPlugin(psPath) if err != nil { mlog.Error("Failed to install prepackaged plugin", mlog.String("path", psPath.path), mlog.Err(err)) return @@ -990,7 +1090,7 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa // processPrepackagedPlugin will return the prepackaged plugin metadata and will also // install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true. -func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) { +func (s *PluginService) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) { mlog.Debug("Processing prepackaged plugin", mlog.String("path", pluginPath.path)) fileReader, err := os.Open(pluginPath.path) @@ -1011,18 +1111,18 @@ func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (* } // Skip installing the plugin at all if automatic prepackaged plugins is disabled - if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins { + if !*s.platform.Config().PluginSettings.AutomaticPrepackagedPlugins { return plugin, nil } // Skip installing if the plugin is has not been previously enabled. - pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[plugin.Manifest.Id] + pluginState := s.platform.Config().PluginSettings.PluginStates[plugin.Manifest.Id] if pluginState == nil || !pluginState.Enable { return plugin, nil } mlog.Debug("Installing prepackaged plugin", mlog.String("path", pluginPath.path)) - if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil { + if _, err := s.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil { return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.path) } @@ -1030,24 +1130,24 @@ func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (* } // installFeatureFlagPlugins handles the automatic installation/upgrade of plugins from feature flags -func (ch *Channels) installFeatureFlagPlugins() { - ffControledPlugins := ch.cfgSvc.Config().FeatureFlags.Plugins() +func (s *PluginService) installFeatureFlagPlugins() { + ffControledPlugins := s.platform.Config().FeatureFlags.Plugins() // Respect the automatic prepackaged disable setting - if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins { + if !*s.platform.Config().PluginSettings.AutomaticPrepackagedPlugins { return } for pluginID, version := range ffControledPlugins { // Skip installing if the plugin has been previously disabled. - pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[pluginID] + pluginState := s.platform.Config().PluginSettings.PluginStates[pluginID] if pluginState != nil && !pluginState.Enable { - ch.srv.Log().Debug("Not auto installing/upgrade because plugin was disabled", mlog.String("plugin_id", pluginID), mlog.String("version", version)) + s.platform.Log().Debug("Not auto installing/upgrade because plugin was disabled", mlog.String("plugin_id", pluginID), mlog.String("version", version)) continue } // Check if we already installed this version as InstallMarketplacePlugin can't handle re-installs well. - pluginStatus, err := ch.GetPluginStatus(pluginID) + pluginStatus, err := s.GetPluginStatus(pluginID) pluginExists := err == nil if pluginExists && pluginStatus.Version == version { continue @@ -1055,37 +1155,37 @@ func (ch *Channels) installFeatureFlagPlugins() { if version != "" && version != "control" { // If we are on-prem skip installation if this is a downgrade - license := ch.srv.License() + license := s.platform.License() inCloud := license != nil && *license.Features.Cloud if !inCloud && pluginExists { parsedVersion, err := semver.Parse(version) if err != nil { - ch.srv.Log().Debug("Bad version from feature flag", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) + s.platform.Log().Debug("Bad version from feature flag", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) return } parsedExistingVersion, err := semver.Parse(pluginStatus.Version) if err != nil { - ch.srv.Log().Debug("Bad version from plugin manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version)) + s.platform.Log().Debug("Bad version from plugin manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version)) return } if parsedVersion.LTE(parsedExistingVersion) { - ch.srv.Log().Debug("Skip installation because given version was a downgrade and on-prem installations should not downgrade.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version)) + s.platform.Log().Debug("Skip installation because given version was a downgrade and on-prem installations should not downgrade.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version)) return } } - _, err := ch.InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{ + _, err := s.InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{ Id: pluginID, Version: version, }) if err != nil { - ch.srv.Log().Debug("Unable to install plugin from FF manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) + s.platform.Log().Debug("Unable to install plugin from FF manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) } else { - if err := ch.enablePlugin(pluginID); err != nil { - ch.srv.Log().Debug("Unable to enable plugin installed from feature flag.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) + if err := s.enablePlugin(pluginID); err != nil { + s.platform.Log().Debug("Unable to enable plugin installed from feature flag.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) } else { - ch.srv.Log().Debug("Installed and enabled plugin.", mlog.String("plugin_id", pluginID), mlog.String("version", version)) + s.platform.Log().Debug("Installed and enabled plugin.", mlog.String("plugin_id", pluginID), mlog.String("version", version)) } } } @@ -1140,15 +1240,15 @@ func getIcon(iconPath string) (string, error) { return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil } -func (ch *Channels) getPluginStateOverride(pluginID string) (bool, bool) { +func (s *PluginService) getPluginStateOverride(pluginID string) (bool, bool) { switch pluginID { case model.PluginIdApps: // Tie Apps proxy disabled status to the feature flag. - if !ch.cfgSvc.Config().FeatureFlags.AppsEnabled { + if !s.platform.Config().FeatureFlags.AppsEnabled { return true, false } case model.PluginIdCalls: - if !ch.cfgSvc.Config().FeatureFlags.CallsEnabled { + if !s.platform.Config().FeatureFlags.CallsEnabled { return true, false } } diff --git a/app/plugin_api.go b/app/plugin_api.go index 5c8ee9fad1..57ee5b4192 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -886,7 +886,7 @@ func (api *PluginAPI) DisablePlugin(id string) *model.AppError { } func (api *PluginAPI) RemovePlugin(id string) *model.AppError { - return api.app.Channels().RemovePlugin(id) + return api.app.Srv().pluginService.RemovePlugin(id) } func (api *PluginAPI) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { @@ -1235,7 +1235,7 @@ func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) { // RegisterCollectionAndTopic informs the server that this plugin handles // the given collection and topic types. func (api *PluginAPI) RegisterCollectionAndTopic(collectionType, topicType string) error { - return api.app.registerCollectionAndTopic(api.id, collectionType, topicType) + return api.app.Srv().pluginService.registerCollectionAndTopic(api.id, collectionType, topicType) } func (api *PluginAPI) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) { diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 759613781b..cec5736eb3 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -92,7 +92,7 @@ func setupMultiPluginAPITest(t *testing.T, pluginCodes []string, pluginManifests return app.NewPluginAPI(c, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv().Platform()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) require.Equal(t, len(pluginCodes), len(pluginIDs)) @@ -119,7 +119,7 @@ func setupMultiPluginAPITest(t *testing.T, pluginCodes []string, pluginManifests }) } - app.ch.SetPluginsEnvironment(env) + app.PluginService().SetPluginsEnvironment(env) return pluginDir } @@ -849,7 +849,7 @@ func TestPluginAPIGetPlugins(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), nil) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) pluginIDs := []string{"pluginid1", "pluginid2", "pluginid3"} @@ -866,7 +866,7 @@ func TestPluginAPIGetPlugins(t *testing.T) { require.True(t, activated) pluginManifests = append(pluginManifests, manifest) } - th.App.ch.SetPluginsEnvironment(env) + th.App.PluginService().SetPluginsEnvironment(env) // Deactivate the last one for testing success := env.Deactivate(pluginIDs[len(pluginIDs)-1]) @@ -937,10 +937,10 @@ func TestInstallPlugin(t *testing.T) { return app.NewPluginAPI(c, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv().Platform()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) - app.ch.SetPluginsEnvironment(env) + app.PluginService().SetPluginsEnvironment(env) backend := filepath.Join(pluginDir, pluginID, "backend.exe") utils.CompileGo(t, pluginCode, backend) @@ -1632,10 +1632,10 @@ func TestAPIMetrics(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) require.NoError(t, err) - th.App.ch.SetPluginsEnvironment(env) + th.App.PluginService().SetPluginsEnvironment(env) pluginID := model.NewId() backend := filepath.Join(pluginDir, pluginID, "backend.exe") @@ -2079,10 +2079,10 @@ func TestRegisterCollectionAndTopic(t *testing.T) { return th.App.NewPluginAPI(th.Context, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, false, th.App.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) - th.App.ch.SetPluginsEnvironment(env) + th.App.PluginService().SetPluginsEnvironment(env) pluginID := "testplugin" pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}` @@ -2179,10 +2179,10 @@ func TestPluginUploadsAPI(t *testing.T) { newPluginAPI := func(manifest *model.Manifest) plugin.API { return th.App.NewPluginAPI(th.Context, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, false, th.App.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) - th.App.ch.SetPluginsEnvironment(env) + th.App.PluginService().SetPluginsEnvironment(env) pluginID := "testplugin" pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}` diff --git a/app/plugin_commands.go b/app/plugin_commands.go index e70c69a38a..8d034931e9 100644 --- a/app/plugin_commands.go +++ b/app/plugin_commands.go @@ -22,6 +22,10 @@ type PluginCommand struct { } func (a *App) RegisterPluginCommand(pluginID string, command *model.Command) error { + return a.Srv().pluginService.registerPluginCommand(pluginID, command) +} + +func (s *PluginService) registerPluginCommand(pluginID string, command *model.Command) error { if command.Trigger == "" { return errors.New("invalid command") } @@ -55,10 +59,10 @@ func (a *App) RegisterPluginCommand(pluginID string, command *model.Command) err AutocompleteIconData: command.AutocompleteIconData, } - a.ch.pluginCommandsLock.Lock() - defer a.ch.pluginCommandsLock.Unlock() + s.pluginCommandsLock.Lock() + defer s.pluginCommandsLock.Unlock() - for _, pc := range a.ch.pluginCommands { + for _, pc := range s.pluginCommands { if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId { if pc.PluginId == pluginID { pc.Command = command @@ -67,7 +71,7 @@ func (a *App) RegisterPluginCommand(pluginID string, command *model.Command) err } } - a.ch.pluginCommands = append(a.ch.pluginCommands, &PluginCommand{ + s.pluginCommands = append(s.pluginCommands, &PluginCommand{ Command: command, PluginId: pluginID, }) @@ -75,39 +79,47 @@ func (a *App) RegisterPluginCommand(pluginID string, command *model.Command) err } func (a *App) UnregisterPluginCommand(pluginID, teamID, trigger string) { + a.Srv().pluginService.unregisterPluginCommand(pluginID, teamID, trigger) +} + +func (s *PluginService) unregisterPluginCommand(pluginID, teamID, trigger string) { trigger = strings.ToLower(trigger) - a.ch.pluginCommandsLock.Lock() - defer a.ch.pluginCommandsLock.Unlock() + s.pluginCommandsLock.Lock() + defer s.pluginCommandsLock.Unlock() var remaining []*PluginCommand - for _, pc := range a.ch.pluginCommands { + for _, pc := range s.pluginCommands { if pc.Command.TeamId != teamID || pc.Command.Trigger != trigger { remaining = append(remaining, pc) } } - a.ch.pluginCommands = remaining + s.pluginCommands = remaining } -func (ch *Channels) unregisterPluginCommands(pluginID string) { - ch.pluginCommandsLock.Lock() - defer ch.pluginCommandsLock.Unlock() +func (s *PluginService) unregisterPluginCommands(pluginID string) { + s.pluginCommandsLock.Lock() + defer s.pluginCommandsLock.Unlock() var remaining []*PluginCommand - for _, pc := range ch.pluginCommands { + for _, pc := range s.pluginCommands { if pc.PluginId != pluginID { remaining = append(remaining, pc) } } - ch.pluginCommands = remaining + s.pluginCommands = remaining } func (a *App) PluginCommandsForTeam(teamID string) []*model.Command { - a.ch.pluginCommandsLock.RLock() - defer a.ch.pluginCommandsLock.RUnlock() + return a.Srv().pluginService.PluginCommandsForTeam(teamID) +} + +func (s *PluginService) PluginCommandsForTeam(teamID string) []*model.Command { + s.pluginCommandsLock.RLock() + defer s.pluginCommandsLock.RUnlock() var commands []*model.Command - for _, pc := range a.ch.pluginCommands { + for _, pc := range s.pluginCommands { if pc.Command.TeamId == "" || pc.Command.TeamId == teamID { commands = append(commands, pc.Command) } @@ -115,6 +127,24 @@ func (a *App) PluginCommandsForTeam(teamID string) []*model.Command { return commands } +func (s *PluginService) getPluginCommandFromArgs(args *model.CommandArgs) *PluginCommand { + parts := strings.Split(args.Command, " ") + trigger := parts[0][1:] + trigger = strings.ToLower(trigger) + + var matched *PluginCommand + s.pluginCommandsLock.RLock() + for _, pc := range s.pluginCommands { + if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger { + matched = pc + break + } + } + s.pluginCommandsLock.RUnlock() + + return matched +} + // tryExecutePluginCommand attempts to run a command provided by a plugin based on the given arguments. If no such // command can be found, returns nil for all arguments. func (a *App) tryExecutePluginCommand(c request.CTX, args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) { @@ -122,15 +152,7 @@ func (a *App) tryExecutePluginCommand(c request.CTX, args *model.CommandArgs) (* trigger := parts[0][1:] trigger = strings.ToLower(trigger) - var matched *PluginCommand - a.ch.pluginCommandsLock.RLock() - for _, pc := range a.ch.pluginCommands { - if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger { - matched = pc - break - } - } - a.ch.pluginCommandsLock.RUnlock() + matched := a.Srv().pluginService.getPluginCommandFromArgs(args) if matched == nil { return nil, nil, nil } diff --git a/app/plugin_commands_test.go b/app/plugin_commands_test.go index e56cf74718..1cf9751d9a 100644 --- a/app/plugin_commands_test.go +++ b/app/plugin_commands_test.go @@ -106,7 +106,7 @@ func TestPluginCommand(t *testing.T) { require.NotEqual(t, "plugin", commands.Trigger) } - th.App.ch.RemovePlugin(pluginIDs[0]) + th.App.PluginService().RemovePlugin(pluginIDs[0]) }) t.Run("re-entrant command registration on config change", func(t *testing.T) { @@ -207,7 +207,7 @@ func TestPluginCommand(t *testing.T) { killed = true } - th.App.ch.RemovePlugin(pluginIDs[0]) + th.App.PluginService().RemovePlugin(pluginIDs[0]) require.False(t, killed, "execute command appears to have deadlocked") }) @@ -285,7 +285,7 @@ func TestPluginCommand(t *testing.T) { require.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType) require.Equal(t, "text", resp.Text) - th.App.ch.RemovePlugin(pluginIDs[0]) + th.App.PluginService().RemovePlugin(pluginIDs[0]) }) t.Run("plugin has crashed before execution of command", func(t *testing.T) { tearDown, pluginIDs, activationErrors := SetAppEnvironmentWithPlugins(t, []string{` @@ -329,7 +329,7 @@ func TestPluginCommand(t *testing.T) { require.Nil(t, resp) require.NotNil(t, err) require.Equal(t, err.Id, "model.plugin_command_error.error.app_error") - th.App.ch.RemovePlugin(pluginIDs[0]) + th.App.PluginService().RemovePlugin(pluginIDs[0]) }) t.Run("plugin has crashed due to the execution of the command", func(t *testing.T) { @@ -374,7 +374,7 @@ func TestPluginCommand(t *testing.T) { require.Nil(t, resp) require.NotNil(t, err) require.Equal(t, err.Id, "model.plugin_command_crash.error.app_error") - th.App.ch.RemovePlugin(pluginIDs[0]) + th.App.PluginService().RemovePlugin(pluginIDs[0]) }) t.Run("plugin returning status code 0", func(t *testing.T) { diff --git a/app/plugin_db_driver.go b/app/plugin_db_driver.go index a29fa7467f..0b74577f36 100644 --- a/app/plugin_db_driver.go +++ b/app/plugin_db_driver.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" ) @@ -19,7 +20,7 @@ import ( // a new entry tracked centrally in a map. Further requests operate on the // object ID. type DriverImpl struct { - s *Server + ps *platform.PlatformService connMut sync.RWMutex connMap map[string]*sql.Conn txMut sync.Mutex @@ -30,9 +31,9 @@ type DriverImpl struct { rowsMap map[string]driver.Rows } -func NewDriverImpl(s *Server) *DriverImpl { +func NewDriverImpl(s *platform.PlatformService) *DriverImpl { return &DriverImpl{ - s: s, + ps: s, connMap: make(map[string]*sql.Conn), txMap: make(map[string]driver.Tx), stMap: make(map[string]driver.Stmt), @@ -41,11 +42,11 @@ func NewDriverImpl(s *Server) *DriverImpl { } func (d *DriverImpl) Conn(isMaster bool) (string, error) { - dbFunc := d.s.Platform().Store.GetInternalMasterDB + dbFunc := d.ps.Store.GetInternalMasterDB if !isMaster { - dbFunc = d.s.Platform().Store.GetInternalReplicaDB + dbFunc = d.ps.Store.GetInternalReplicaDB } - timeout := time.Duration(*d.s.Config().SqlSettings.QueryTimeout) * time.Second + timeout := time.Duration(*d.ps.Config().SqlSettings.QueryTimeout) * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() conn, err := dbFunc().Conn(ctx) diff --git a/app/plugin_db_driver_test.go b/app/plugin_db_driver_test.go index a2c428fb6f..797f677d59 100644 --- a/app/plugin_db_driver_test.go +++ b/app/plugin_db_driver_test.go @@ -15,7 +15,7 @@ func TestConnCreateTimeout(t *testing.T) { *th.App.Config().SqlSettings.QueryTimeout = 0 - d := NewDriverImpl(th.Server) + d := NewDriverImpl(th.Server.platform) _, err := d.Conn(true) require.Error(t, err) } diff --git a/app/plugin_event.go b/app/plugin_event.go index c30e2d1af5..19d9f1dabc 100644 --- a/app/plugin_event.go +++ b/app/plugin_event.go @@ -9,10 +9,10 @@ import ( "github.com/mattermost/mattermost-server/v6/model" ) -func (ch *Channels) notifyClusterPluginEvent(event model.ClusterEvent, data model.PluginEventData) { +func (s *PluginService) notifyClusterPluginEvent(event model.ClusterEvent, data model.PluginEventData) { buf, _ := json.Marshal(data) - if ch.srv.platform.Cluster() != nil { - ch.srv.platform.Cluster().SendClusterMessage(&model.ClusterMessage{ + if s.platform.Cluster() != nil { + s.platform.Cluster().SendClusterMessage(&model.ClusterMessage{ Event: event, SendType: model.ClusterSendReliable, WaitForAllToSend: true, diff --git a/app/plugin_hooks_test.go b/app/plugin_hooks_test.go index 161994a1ab..105982a7b2 100644 --- a/app/plugin_hooks_test.go +++ b/app/plugin_hooks_test.go @@ -33,10 +33,10 @@ func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, a webappPluginDir, err := os.MkdirTemp("", "") require.NoError(t, err) - env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) + env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv().Platform()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) - app.ch.SetPluginsEnvironment(env) + app.PluginService().SetPluginsEnvironment(env) pluginIDs := []string{} activationErrors := []error{} for _, code := range pluginCode { @@ -1030,10 +1030,10 @@ func TestHookMetrics(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) require.NoError(t, err) - th.App.ch.SetPluginsEnvironment(env) + th.App.PluginService().SetPluginsEnvironment(env) pluginID := model.NewId() backend := filepath.Join(pluginDir, pluginID, "backend.exe") @@ -1234,7 +1234,7 @@ func TestHookRunDataRetention(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { + th.App.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { n, _ := hooks.RunDataRetention(0, 0) // Ensure return it correct assert.Equal(t, int64(100), n) @@ -1278,7 +1278,7 @@ func TestHookOnSendDailyTelemetry(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { + th.App.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.OnSendDailyTelemetry() hookCalled = true @@ -1322,7 +1322,7 @@ func TestHookOnCloudLimitsUpdated(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { + th.App.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.OnCloudLimitsUpdated(nil) hookCalled = true diff --git a/app/plugin_install.go b/app/plugin_install.go index 431c3abebe..f0c54a0da4 100644 --- a/app/plugin_install.go +++ b/app/plugin_install.go @@ -58,10 +58,10 @@ const managedPluginFileName = ".filestore" // fileStorePluginFolder is the folder name in the file store of the plugin bundles installed. const fileStorePluginFolder = "plugins" -func (ch *Channels) installPluginFromData(data model.PluginEventData) { +func (s *PluginService) installPluginFromData(data model.PluginEventData) { mlog.Debug("Installing plugin as per cluster message", mlog.String("plugin_id", data.Id)) - pluginSignaturePathMap, appErr := ch.getPluginsFromFolder() + pluginSignaturePathMap, appErr := s.getPluginsFromFolder() if appErr != nil { mlog.Error("Failed to get plugin signatures from filestore. Can't install plugin from data.", mlog.Err(appErr)) return @@ -72,53 +72,53 @@ func (ch *Channels) installPluginFromData(data model.PluginEventData) { return } - reader, appErr := ch.srv.fileReader(plugin.path) - if appErr != nil { - mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr)) + reader, err := s.fileStore.Reader(plugin.path) + if err != nil { + mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(err)) return } defer reader.Close() var signature filestore.ReadCloseSeeker - if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature { - signature, appErr = ch.srv.fileReader(plugin.signaturePath) - if appErr != nil { - mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr)) + if *s.platform.Config().PluginSettings.RequirePluginSignature { + signature, err = s.fileStore.Reader(plugin.signaturePath) + if err != nil { + mlog.Error("Failed to open plugin signature from file store.", mlog.Err(err)) return } defer signature.Close() } - manifest, appErr := ch.installPluginLocally(reader, signature, installPluginLocallyAlways) + manifest, appErr := s.installPluginLocally(reader, signature, installPluginLocallyAlways) if appErr != nil { mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr)) return } - if err := ch.notifyPluginEnabled(manifest); err != nil { - mlog.Error("Failed notify plugin enabled", mlog.Err(err)) + if err2 := s.notifyPluginEnabled(manifest); err2 != nil { + mlog.Error("Failed notify plugin enabled", mlog.Err(err2)) } - if err := ch.notifyPluginStatusesChanged(); err != nil { - mlog.Error("Failed to notify plugin status changed", mlog.Err(err)) + if err2 := s.notifyPluginStatusesChanged(); err2 != nil { + mlog.Error("Failed to notify plugin status changed", mlog.Err(err2)) } } -func (ch *Channels) removePluginFromData(data model.PluginEventData) { +func (s *PluginService) removePluginFromData(data model.PluginEventData) { mlog.Debug("Removing plugin as per cluster message", mlog.String("plugin_id", data.Id)) - if err := ch.removePluginLocally(data.Id); err != nil { + if err := s.removePluginLocally(data.Id); err != nil { mlog.Warn("Failed to remove plugin locally", mlog.Err(err), mlog.String("id", data.Id)) } - if err := ch.notifyPluginStatusesChanged(); err != nil { + if err := s.notifyPluginStatusesChanged(); err != nil { mlog.Warn("failed to notify plugin status changed", mlog.Err(err)) } } // InstallPluginWithSignature verifies and installs plugin. -func (ch *Channels) installPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) { - return ch.installPlugin(pluginFile, signature, installPluginLocallyAlways) +func (s *PluginService) installPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) { + return s.installPlugin(pluginFile, signature, installPluginLocallyAlways) } // InstallPlugin unpacks and installs a plugin but does not enable or activate it. @@ -132,40 +132,40 @@ func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Mani } func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - return a.ch.installPlugin(pluginFile, signature, installationStrategy) + return a.ch.srv.pluginService.installPlugin(pluginFile, signature, installationStrategy) } -func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - manifest, appErr := ch.installPluginLocally(pluginFile, signature, installationStrategy) +func (s *PluginService) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { + manifest, appErr := s.installPluginLocally(pluginFile, signature, installationStrategy) if appErr != nil { return nil, appErr } if signature != nil { signature.Seek(0, 0) - if _, appErr = ch.srv.writeFile(signature, getSignatureStorePath(manifest.Id)); appErr != nil { - return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) + if _, err := s.fileStore.WriteFile(signature, getSignatureStorePath(manifest.Id)); err != nil { + return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } // Store bundle in the file store to allow access from other servers. pluginFile.Seek(0, 0) - if _, appErr := ch.srv.writeFile(pluginFile, getBundleStorePath(manifest.Id)); appErr != nil { + if _, appErr := s.fileStore.WriteFile(pluginFile, getBundleStorePath(manifest.Id)); appErr != nil { return nil, model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) } - ch.notifyClusterPluginEvent( + s.notifyClusterPluginEvent( model.ClusterEventInstallPlugin, model.PluginEventData{ Id: manifest.Id, }, ) - if err := ch.notifyPluginEnabled(manifest); err != nil { + if err := s.notifyPluginEnabled(manifest); err != nil { mlog.Warn("Failed notify plugin enabled", mlog.Err(err)) } - if err := ch.notifyPluginStatusesChanged(); err != nil { + if err := s.notifyPluginStatusesChanged(); err != nil { mlog.Warn("Failed to notify plugin status changed", mlog.Err(err)) } @@ -174,10 +174,10 @@ func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installat // InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle // from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true. -func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) { +func (s *PluginService) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) { var pluginFile, signatureFile io.ReadSeeker - prepackagedPlugin, appErr := ch.getPrepackagedPlugin(request.Id, request.Version) + prepackagedPlugin, appErr := s.getPrepackagedPlugin(request.Id, request.Version) if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" { return nil, appErr } @@ -192,9 +192,9 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl signatureFile = bytes.NewReader(prepackagedPlugin.Signature) } - if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace { + if *s.platform.Config().PluginSettings.EnableRemoteMarketplace { var plugin *model.BaseMarketplacePlugin - plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version) + plugin, appErr = s.getRemoteMarketplacePlugin(request.Id, request.Version) if appErr != nil { return nil, appErr } @@ -214,7 +214,7 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl } if prepackagedVersion.LT(marketplaceVersion) { // Always true if no prepackaged plugin was found - downloadedPluginBytes, err := ch.srv.downloadFromURL(plugin.DownloadURL) + downloadedPluginBytes, err := s.downloadFromURL(plugin.DownloadURL) if err != nil { return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -234,7 +234,7 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError) } - manifest, appErr := ch.installPluginWithSignature(pluginFile, signatureFile) + manifest, appErr := s.installPluginWithSignature(pluginFile, signatureFile) if appErr != nil { return nil, appErr } @@ -253,15 +253,15 @@ const ( installPluginLocallyAlways ) -func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - pluginsEnvironment := ch.GetPluginsEnvironment() +func (s *PluginService) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } // verify signature if signature != nil { - if err := ch.verifyPlugin(pluginFile, signature); err != nil { + if err := s.verifyPlugin(pluginFile, signature); err != nil { return nil, err } } @@ -277,7 +277,7 @@ func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, in return nil, appErr } - manifest, appErr = ch.installExtractedPlugin(manifest, pluginDir, installationStrategy) + manifest, appErr = s.installExtractedPlugin(manifest, pluginDir, installationStrategy) if appErr != nil { return nil, appErr } @@ -312,8 +312,8 @@ func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest return manifest, extractDir, nil } -func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - pluginsEnvironment := ch.GetPluginsEnvironment() +func (s *PluginService) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -360,12 +360,12 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD // Otherwise remove the existing installation prior to install below. mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version)) - if err := ch.removePluginLocally(existingManifest.Id); err != nil { + if err := s.removePluginLocally(existingManifest.Id); err != nil { return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest) } } - pluginPath := filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, manifest.Id) + pluginPath := filepath.Join(*s.platform.Config().PluginSettings.Directory, manifest.Id) err = utils.CopyDir(fromPluginDir, pluginPath) if err != nil { return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -387,9 +387,9 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD } // Activate the plugin if enabled. - pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[manifest.Id] + pluginState := s.platform.Config().PluginSettings.PluginStates[manifest.Id] if pluginState != nil && pluginState.Enable { - if hasOverride, enabled := ch.getPluginStateOverride(manifest.Id); hasOverride && !enabled { + if hasOverride, enabled := s.getPluginStateOverride(manifest.Id); hasOverride && !enabled { return manifest, nil } @@ -405,49 +405,49 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD return manifest, nil } -func (ch *Channels) RemovePlugin(id string) *model.AppError { +func (s *PluginService) RemovePlugin(id string) *model.AppError { // Disable plugin before removal to make sure this // plugin remains disabled on re-install. - if err := ch.disablePlugin(id); err != nil { + if err := s.disablePlugin(id); err != nil { return err } - if err := ch.removePluginLocally(id); err != nil { + if err := s.removePluginLocally(id); err != nil { return err } // Remove bundle from the file store. storePluginFileName := getBundleStorePath(id) - bundleExist, err := ch.srv.fileExists(storePluginFileName) + bundleExist, err := s.fileStore.FileExists(storePluginFileName) if err != nil { return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if !bundleExist { return nil } - if err = ch.srv.removeFile(storePluginFileName); err != nil { + if err = s.fileStore.RemoveFile(storePluginFileName); err != nil { return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - if err = ch.removeSignature(id); err != nil { - mlog.Warn("Can't remove signature", mlog.Err(err)) + if err2 := s.removeSignature(id); err2 != nil { + mlog.Warn("Can't remove signature", mlog.Err(err2)) } - ch.notifyClusterPluginEvent( + s.notifyClusterPluginEvent( model.ClusterEventRemovePlugin, model.PluginEventData{ Id: id, }, ) - if err := ch.notifyPluginStatusesChanged(); err != nil { + if err := s.notifyPluginStatusesChanged(); err != nil { mlog.Warn("Failed to notify plugin status changed", mlog.Err(err)) } return nil } -func (ch *Channels) removePluginLocally(id string) *model.AppError { - pluginsEnvironment := ch.GetPluginsEnvironment() +func (s *PluginService) removePluginLocally(id string) *model.AppError { + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -473,7 +473,7 @@ func (ch *Channels) removePluginLocally(id string) *model.AppError { pluginsEnvironment.Deactivate(id) pluginsEnvironment.RemovePlugin(id) - ch.unregisterPluginCommands(id) + s.unregisterPluginCommands(id) if err := os.RemoveAll(pluginPath); err != nil { return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -482,9 +482,9 @@ func (ch *Channels) removePluginLocally(id string) *model.AppError { return nil } -func (ch *Channels) removeSignature(pluginID string) *model.AppError { +func (s *PluginService) removeSignature(pluginID string) *model.AppError { filePath := getSignatureStorePath(pluginID) - exists, err := ch.srv.fileExists(filePath) + exists, err := s.fileStore.FileExists(filePath) if err != nil { return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -492,7 +492,7 @@ func (ch *Channels) removeSignature(pluginID string) *model.AppError { mlog.Debug("no plugin signature to remove", mlog.String("plugin_id", pluginID)) return nil } - if err = ch.srv.removeFile(filePath); err != nil { + if err = s.fileStore.RemoveFile(filePath); err != nil { return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil diff --git a/app/plugin_install_test.go b/app/plugin_install_test.go index a3eb6cfb2d..ca15747865 100644 --- a/app/plugin_install_test.go +++ b/app/plugin_install_test.go @@ -73,7 +73,7 @@ func TestInstallPluginLocally(t *testing.T) { th := Setup(t) defer th.TearDown() - actualManifest, appErr := th.App.ch.installPluginLocally(&nilReadSeeker{}, nil, installPluginLocallyOnlyIfNew) + actualManifest, appErr := th.App.PluginService().installPluginLocally(&nilReadSeeker{}, nil, installPluginLocallyOnlyIfNew) require.NotNil(t, appErr) assert.Equal(t, "app.plugin.extract.app_error", appErr.Id, appErr.Error()) require.Nil(t, actualManifest) @@ -87,7 +87,7 @@ func TestInstallPluginLocally(t *testing.T) { {"test", "test file"}, }) - actualManifest, appErr := th.App.ch.installPluginLocally(reader, nil, installPluginLocallyOnlyIfNew) + actualManifest, appErr := th.App.PluginService().installPluginLocally(reader, nil, installPluginLocallyOnlyIfNew) require.NotNil(t, appErr) assert.Equal(t, "app.plugin.manifest.app_error", appErr.Id, appErr.Error()) require.Nil(t, actualManifest) @@ -106,7 +106,7 @@ func TestInstallPluginLocally(t *testing.T) { {"plugin.json", string(manifestJSON)}, }) - actualManifest, appError := th.App.ch.installPluginLocally(reader, nil, installationStrategy) + actualManifest, appError := th.App.PluginService().installPluginLocally(reader, nil, installationStrategy) if actualManifest != nil { require.Equal(t, manifest, actualManifest) } @@ -134,7 +134,7 @@ func TestInstallPluginLocally(t *testing.T) { require.NoError(t, err) for _, bundleInfo := range bundleInfos { - err := th.App.ch.removePluginLocally(bundleInfo.Manifest.Id) + err := th.App.PluginService().removePluginLocally(bundleInfo.Manifest.Id) require.Nilf(t, err, "failed to remove existing plugin %s", bundleInfo.Manifest.Id) } } diff --git a/app/plugin_requests.go b/app/plugin_requests.go index 1ccc966822..208adb11f4 100644 --- a/app/plugin_requests.go +++ b/app/plugin_requests.go @@ -20,16 +20,16 @@ import ( "github.com/mattermost/mattermost-server/v6/utils" ) -func (ch *Channels) ServePluginRequest(w http.ResponseWriter, r *http.Request) { +func (s *PluginService) ServePluginRequest(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - if handler, ok := ch.routerSvc.getHandler(params["plugin_id"]); ok { - ch.servePluginRequest(w, r, func(*plugin.Context, http.ResponseWriter, *http.Request) { + if handler, ok := s.channels.routerSvc.getHandler(params["plugin_id"]); ok { + s.servePluginRequest(w, r, func(*plugin.Context, http.ResponseWriter, *http.Request) { handler.ServeHTTP(w, r) }) return } - pluginsEnvironment := ch.GetPluginsEnvironment() + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented) mlog.Error(err.Error()) @@ -49,11 +49,11 @@ func (ch *Channels) ServePluginRequest(w http.ResponseWriter, r *http.Request) { return } - ch.servePluginRequest(w, r, hooks.ServeHTTP) + s.servePluginRequest(w, r, hooks.ServeHTTP) } func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) { - pluginsEnvironment := a.ch.GetPluginsEnvironment() + pluginsEnvironment := a.ch.srv.pluginService.GetPluginsEnvironment() if pluginsEnvironment == nil { err := model.NewAppError("ServeInterPluginRequest", "app.plugin.disabled.app_error", nil, "Plugin environment not found.", http.StatusNotImplemented) a.Log().Error(err.Error()) @@ -87,7 +87,7 @@ func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, so // ServePluginPublicRequest serves public plugin files // at the URL http(s)://$SITE_URL/plugins/$PLUGIN_ID/public/{anything} -func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Request) { +func (s *PluginService) ServePluginPublicRequest(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "/") { http.NotFound(w, r) return @@ -97,7 +97,7 @@ func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Requ vars := mux.Vars(r) pluginID := vars["plugin_id"] - pluginsEnv := ch.GetPluginsEnvironment() + pluginsEnv := s.GetPluginsEnvironment() // Check if someone has nullified the pluginsEnv in the meantime if pluginsEnv == nil { @@ -121,11 +121,11 @@ func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Requ http.ServeFile(w, r, publicFile) } -func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) { +func (s *PluginService) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) { token := "" context := &plugin.Context{ RequestId: model.NewId(), - IPAddress: utils.GetIPAddress(r, ch.cfgSvc.Config().ServiceSettings.TrustedProxyIPHeader), + IPAddress: utils.GetIPAddress(r, s.platform.Config().ServiceSettings.TrustedProxyIPHeader), AcceptLanguage: r.Header.Get("Accept-Language"), UserAgent: r.UserAgent(), } @@ -148,8 +148,8 @@ func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, h r.Header.Del("Mattermost-User-Id") if token != "" { - session, err := New(ServerConnector(ch)).GetSession(token) - defer ch.srv.platform.ReturnSessionToPool(session) + session, err := New(ServerConnector(s.channels)).GetSession(token) + defer s.platform.ReturnSessionToPool(session) csrfCheckPassed := false @@ -190,7 +190,7 @@ func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, h mlog.String("user_id", userID), } - if *ch.cfgSvc.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement { + if *s.platform.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement { mlog.Warn(csrfErrorMessage, fields...) } else { mlog.Debug(csrfErrorMessage, fields...) @@ -219,7 +219,7 @@ func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, h params := mux.Vars(r) - subpath, _ := utils.GetSubpathFromConfig(ch.cfgSvc.Config()) + subpath, _ := utils.GetSubpathFromConfig(s.platform.Config()) newQuery := r.URL.Query() newQuery.Del("access_token") diff --git a/app/plugin_requests_test.go b/app/plugin_requests_test.go index c41c70be6d..e457d8e5f1 100644 --- a/app/plugin_requests_test.go +++ b/app/plugin_requests_test.go @@ -24,7 +24,7 @@ func TestServePluginPublicRequest(t *testing.T) { require.NoError(t, err) rr := httptest.NewRecorder() - handler := http.HandlerFunc(th.App.ch.ServePluginPublicRequest) + handler := http.HandlerFunc(th.App.PluginService().ServePluginPublicRequest) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusNotFound, rr.Code) diff --git a/app/plugin_shutdown_test.go b/app/plugin_shutdown_test.go index 293d882f1f..1c77fc3814 100644 --- a/app/plugin_shutdown_test.go +++ b/app/plugin_shutdown_test.go @@ -63,7 +63,7 @@ func TestPluginShutdownTest(t *testing.T) { done := make(chan bool) go func() { defer close(done) - th.App.ch.ShutDownPlugins() + th.App.PluginService().ShutDownPlugins() }() select { diff --git a/app/plugin_signature.go b/app/plugin_signature.go index 0903aa08fc..928a9687b8 100644 --- a/app/plugin_signature.go +++ b/app/plugin_signature.go @@ -73,16 +73,16 @@ func (a *App) DeletePublicKey(name string) *model.AppError { // VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate. func (a *App) VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError { - return a.ch.verifyPlugin(plugin, signature) + return a.ch.srv.pluginService.verifyPlugin(plugin, signature) } -func (ch *Channels) verifyPlugin(plugin, signature io.ReadSeeker) *model.AppError { +func (s *PluginService) verifyPlugin(plugin, signature io.ReadSeeker) *model.AppError { if err := verifySignature(bytes.NewReader(mattermostPluginPublicKey), plugin, signature); err == nil { return nil } - publicKeys := ch.cfgSvc.Config().PluginSettings.SignaturePublicKeyFiles + publicKeys := s.platform.Config().PluginSettings.SignaturePublicKeyFiles for _, pk := range publicKeys { - pkBytes, appErr := ch.srv.getPublicKey(pk) + pkBytes, appErr := s.platform.GetConfigFile(pk) if appErr != nil { mlog.Warn("Unable to get public key for ", mlog.String("filename", pk)) continue diff --git a/app/plugin_statuses.go b/app/plugin_statuses.go index 399d58e5b2..2b27d7520c 100644 --- a/app/plugin_statuses.go +++ b/app/plugin_statuses.go @@ -10,8 +10,8 @@ import ( ) // GetPluginStatus returns the status for a plugin installed on this server. -func (ch *Channels) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { - pluginsEnvironment := ch.GetPluginsEnvironment() +func (s *PluginService) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("GetPluginStatus", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -24,8 +24,8 @@ func (ch *Channels) GetPluginStatus(id string) (*model.PluginStatus, *model.AppE for _, status := range pluginStatuses { if status.PluginId == id { // Add our cluster ID - if ch.srv.platform.Cluster() != nil { - status.ClusterId = ch.srv.platform.Cluster().GetClusterId() + if s.platform.Cluster() != nil { + status.ClusterId = s.platform.Cluster().GetClusterId() } return status, nil @@ -37,12 +37,12 @@ func (ch *Channels) GetPluginStatus(id string) (*model.PluginStatus, *model.AppE // GetPluginStatus returns the status for a plugin installed on this server. func (a *App) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { - return a.ch.GetPluginStatus(id) + return a.ch.srv.pluginService.GetPluginStatus(id) } // GetPluginStatuses returns the status for plugins installed on this server. -func (ch *Channels) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { - pluginsEnvironment := ch.GetPluginsEnvironment() +func (s *PluginService) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { + pluginsEnvironment := s.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -54,8 +54,8 @@ func (ch *Channels) GetPluginStatuses() (model.PluginStatuses, *model.AppError) // Add our cluster ID for _, status := range pluginStatuses { - if ch.srv.platform.Cluster() != nil { - status.ClusterId = ch.srv.platform.Cluster().GetClusterId() + if s.platform.Cluster() != nil { + status.ClusterId = s.platform.Cluster().GetClusterId() } else { status.ClusterId = "" } @@ -66,22 +66,22 @@ func (ch *Channels) GetPluginStatuses() (model.PluginStatuses, *model.AppError) // GetPluginStatuses returns the status for plugins installed on this server. func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { - return a.ch.GetPluginStatuses() + return a.ch.srv.pluginService.GetPluginStatuses() } // GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster. func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) { - return a.ch.getClusterPluginStatuses() + return a.ch.srv.pluginService.getClusterPluginStatuses() } -func (ch *Channels) getClusterPluginStatuses() (model.PluginStatuses, *model.AppError) { - pluginStatuses, err := ch.GetPluginStatuses() +func (s *PluginService) getClusterPluginStatuses() (model.PluginStatuses, *model.AppError) { + pluginStatuses, err := s.GetPluginStatuses() if err != nil { return nil, err } - if ch.srv.platform.Cluster() != nil && *ch.cfgSvc.Config().ClusterSettings.Enable { - clusterPluginStatuses, err := ch.srv.platform.Cluster().GetPluginStatuses() + if s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable { + clusterPluginStatuses, err := s.platform.Cluster().GetPluginStatuses() if err != nil { return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -92,8 +92,8 @@ func (ch *Channels) getClusterPluginStatuses() (model.PluginStatuses, *model.App return pluginStatuses, nil } -func (ch *Channels) notifyPluginStatusesChanged() error { - pluginStatuses, err := ch.getClusterPluginStatuses() +func (s *PluginService) notifyPluginStatusesChanged() error { + pluginStatuses, err := s.getClusterPluginStatuses() if err != nil { return err } @@ -102,7 +102,7 @@ func (ch *Channels) notifyPluginStatusesChanged() error { message := model.NewWebSocketEvent(model.WebsocketEventPluginStatusesChanged, "", "", "", nil, "") message.Add("plugin_statuses", pluginStatuses) message.GetBroadcast().ContainsSensitiveData = true - ch.srv.platform.Publish(message) + s.platform.Publish(message) return nil } diff --git a/app/plugin_test.go b/app/plugin_test.go index 57802c67ac..0d3ec65431 100644 --- a/app/plugin_test.go +++ b/app/plugin_test.go @@ -346,7 +346,7 @@ func TestServePluginRequest(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/plugins/foo/bar", nil) - th.App.ch.ServePluginRequest(w, r) + th.App.PluginService().ServePluginRequest(w, r) assert.Equal(t, http.StatusNotImplemented, w.Result().StatusCode) } @@ -390,7 +390,7 @@ func TestPrivateServePluginRequest(t *testing.T) { request = mux.SetURLVars(request, map[string]string{"plugin_id": "id"}) - th.App.ch.servePluginRequest(recorder, request, handler) + th.App.PluginService().servePluginRequest(recorder, request, handler) }) } @@ -413,7 +413,7 @@ func TestHandlePluginRequest(t *testing.T) { var assertions func(*http.Request) router := mux.NewRouter() router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}/{anything:.*}", func(_ http.ResponseWriter, r *http.Request) { - th.App.ch.servePluginRequest(nil, r, func(_ *plugin.Context, _ http.ResponseWriter, r *http.Request) { + th.App.PluginService().servePluginRequest(nil, r, func(_ *plugin.Context, _ http.ResponseWriter, r *http.Request) { assertions(r) }) }) @@ -625,7 +625,7 @@ func TestPluginSync(t *testing.T) { appErr = th.App.DeletePublicKey("pub_key") checkNoError(t, appErr) - appErr = th.App.ch.RemovePlugin("testplugin") + appErr = th.App.PluginService().RemovePlugin("testplugin") checkNoError(t, appErr) }) }) @@ -642,7 +642,7 @@ func TestChannelsPluginsInit(t *testing.T) { path, _ := fileutils.FindDir("tests") require.NotPanics(t, func() { - th.Server.Channels().initPlugins(ctx, path, path) + th.Server.pluginService.initPlugins(ctx, path, path) }) } @@ -763,7 +763,7 @@ func TestPluginPanicLogs(t *testing.T) { th.TestLogger.Flush() // We shutdown plugins first so that the read on the log buffer is race-free. - th.App.ch.ShutDownPlugins() + th.App.PluginService().ShutDownPlugins() tearDown() testlib.AssertLog(t, th.LogBuffer, mlog.LvlDebug.Name, "panic: some text from panic") @@ -831,7 +831,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { require.NoError(t, err) require.NotNil(t, pluginBytes) - manifest, appErr := th.App.ch.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) + manifest, appErr := th.App.PluginService().installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) require.Nil(t, appErr) require.Equal(t, "testplugin", manifest.Id) @@ -848,7 +848,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { *cfg.PluginSettings.EnableRemoteMarketplace = false }) - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 1) require.Equal(t, plugins[0].Manifest.Id, "testplugin") require.Empty(t, plugins[0].Signature, 0) @@ -858,7 +858,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { require.Len(t, pluginStatus, 1) require.Equal(t, pluginStatus[0].PluginId, "testplugin") - appErr = th.App.ch.RemovePlugin("testplugin") + appErr = th.App.PluginService().RemovePlugin("testplugin") checkNoError(t, appErr) pluginStatus, err = env.Statuses() @@ -875,7 +875,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { env := th.App.GetPluginsEnvironment() - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 1) require.Equal(t, plugins[0].Manifest.Id, "testplugin") require.Empty(t, plugins[0].Signature, 0) @@ -908,7 +908,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) require.NoError(t, err) - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 2) require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) require.NotEmpty(t, plugins[0].Signature) @@ -939,7 +939,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { require.NoError(t, err) require.NotNil(t, pluginBytes) - manifest, appErr := th.App.ch.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) + manifest, appErr := th.App.PluginService().installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) require.Nil(t, appErr) require.Equal(t, "testplugin", manifest.Id) @@ -957,7 +957,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) require.NoError(t, err) - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 2) require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) require.NotEmpty(t, plugins[0].Signature) @@ -969,7 +969,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { require.Len(t, pluginStatus, 1) require.Equal(t, pluginStatus[0].PluginId, "testplugin") - appErr = th.App.ch.RemovePlugin("testplugin") + appErr = th.App.PluginService().RemovePlugin("testplugin") checkNoError(t, appErr) pluginStatus, err = env.Statuses() @@ -994,7 +994,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) require.NoError(t, err) - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 2) require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) require.NotEmpty(t, plugins[0].Signature) @@ -1071,14 +1071,14 @@ func TestGetPluginStateOverride(t *testing.T) { defer th.TearDown() t.Run("no override", func(t *testing.T) { - overrides, value := th.App.ch.getPluginStateOverride("focalboard") + overrides, value := th.App.PluginService().getPluginStateOverride("focalboard") require.False(t, overrides) require.False(t, value) }) t.Run("calls override", func(t *testing.T) { t.Run("on-prem", func(t *testing.T) { - overrides, value := th.App.ch.getPluginStateOverride("com.mattermost.calls") + overrides, value := th.App.PluginService().getPluginStateOverride("com.mattermost.calls") require.False(t, overrides) require.False(t, value) }) @@ -1086,7 +1086,7 @@ func TestGetPluginStateOverride(t *testing.T) { t.Run("Cloud, without enabled flag", func(t *testing.T) { os.Setenv("MM_CLOUD_INSTALLATION_ID", "test") defer os.Unsetenv("MM_CLOUD_INSTALLATION_ID") - overrides, value := th.App.ch.getPluginStateOverride("com.mattermost.calls") + overrides, value := th.App.PluginService().getPluginStateOverride("com.mattermost.calls") require.False(t, overrides) require.False(t, value) }) @@ -1100,7 +1100,7 @@ func TestGetPluginStateOverride(t *testing.T) { th2 := Setup(t) defer th2.TearDown() - overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.calls") + overrides, value := th2.App.PluginService().getPluginStateOverride("com.mattermost.calls") require.False(t, overrides) require.False(t, value) }) @@ -1114,7 +1114,7 @@ func TestGetPluginStateOverride(t *testing.T) { th2 := Setup(t) defer th2.TearDown() - overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.calls") + overrides, value := th2.App.PluginService().getPluginStateOverride("com.mattermost.calls") require.True(t, overrides) require.False(t, value) }) @@ -1126,7 +1126,7 @@ func TestGetPluginStateOverride(t *testing.T) { th2 := Setup(t) defer th2.TearDown() - overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.calls") + overrides, value := th2.App.PluginService().getPluginStateOverride("com.mattermost.calls") require.True(t, overrides) require.False(t, value) }) @@ -1134,7 +1134,7 @@ func TestGetPluginStateOverride(t *testing.T) { t.Run("apps override", func(t *testing.T) { t.Run("without enabled flag", func(t *testing.T) { - overrides, value := th.App.ch.getPluginStateOverride("com.mattermost.apps") + overrides, value := th.App.PluginService().getPluginStateOverride("com.mattermost.apps") require.False(t, overrides) require.False(t, value) }) @@ -1146,7 +1146,7 @@ func TestGetPluginStateOverride(t *testing.T) { th2 := Setup(t) defer th2.TearDown() - overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.apps") + overrides, value := th2.App.PluginService().getPluginStateOverride("com.mattermost.apps") require.True(t, overrides) require.False(t, value) }) diff --git a/app/post.go b/app/post.go index 095494d080..b98cb13759 100644 --- a/app/post.go +++ b/app/post.go @@ -269,7 +269,7 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel } var rejectionError *model.AppError pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { replacementPost, rejectionReason := hooks.MessageWillBePosted(pluginContext, post.ForPlugin()) if rejectionReason != "" { id := "Post rejected by plugin. " + rejectionReason @@ -328,7 +328,7 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel // and to remove the non-GOB-encodable Metadata from it. pluginPost := rpost.ForPlugin() a.Srv().Go(func() { - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.MessageHasBeenPosted(pluginContext, pluginPost) return true }, plugin.MessageHasBeenPostedID) @@ -655,7 +655,7 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) var rejectionReason string pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost.ForPlugin(), oldPost.ForPlugin()) return post != nil }, plugin.MessageWillBeUpdatedID) @@ -680,7 +680,7 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) pluginOldPost := oldPost.ForPlugin() pluginNewPost := newPost.ForPlugin() a.Srv().Go(func() { - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.MessageHasBeenUpdated(pluginContext, pluginNewPost, pluginOldPost) return true }, plugin.MessageHasBeenUpdatedID) diff --git a/app/reaction.go b/app/reaction.go index fc6d54699f..c79036b443 100644 --- a/app/reaction.go +++ b/app/reaction.go @@ -45,7 +45,7 @@ func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction) pluginContext := pluginContext(c) a.Srv().Go(func() { - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ReactionHasBeenAdded(pluginContext, reaction) return true }, plugin.ReactionHasBeenAddedID) @@ -142,7 +142,7 @@ func (a *App) DeleteReactionForPost(c *request.Context, reaction *model.Reaction pluginContext := pluginContext(c) a.Srv().Go(func() { - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ReactionHasBeenRemoved(pluginContext, reaction) return true }, plugin.ReactionHasBeenRemovedID) diff --git a/app/server.go b/app/server.go index fbd433a6db..22f9d24c1c 100644 --- a/app/server.go +++ b/app/server.go @@ -119,6 +119,7 @@ type Server struct { telemetryService *telemetry.TelemetryService userService *users.UserService teamService *teams.TeamService + pluginService *PluginService serviceMux sync.RWMutex remoteClusterService remotecluster.RemoteClusterServiceIFace @@ -718,6 +719,10 @@ func (s *Server) Shutdown() { } } + // Stop the plugin service, we need to stop plugin service before stopping the + // product as products are being consumed by this service. + s.pluginService.ShutDownPlugins() + // Stop products. // This needs to happen last because products are dependent // on parent services. @@ -824,11 +829,18 @@ func stripPort(hostport string) string { func (s *Server) Start() error { // Start products. // This needs to happen before because products are dependent on the HTTP server. - // make sure channels starts first if err := s.products["channels"].Start(); err != nil { return errors.Wrap(err, "Unable to start channels") } + + // This should actually be started after products, but we have a product hooks + // dependency for now, once that get sorted out, this should be moved to the appropriate + // order. + if err := s.InitializePluginService(); err != nil { + return errors.Wrap(err, "Unable to start plugin service") + } + for name, product := range s.products { if name == "channels" { continue diff --git a/app/team.go b/app/team.go index 222d10e442..5724f683c2 100644 --- a/app/team.go +++ b/app/team.go @@ -853,7 +853,7 @@ func (a *App) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, a.Srv().Go(func() { pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedTeam(pluginContext, teamMember, actor) return true }, plugin.UserHasJoinedTeamID) @@ -1225,7 +1225,7 @@ func (a *App) postProcessTeamMemberLeave(c request.CTX, teamMember *model.TeamMe a.Srv().Go(func() { pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLeftTeam(pluginContext, teamMember, actor) return true }, plugin.UserHasLeftTeamID) diff --git a/app/upload.go b/app/upload.go index 318e3ede89..b60f9be57b 100644 --- a/app/upload.go +++ b/app/upload.go @@ -62,7 +62,7 @@ func (a *App) runPluginsHook(c request.CTX, info *model.FileInfo, file io.Reader var rejErr *model.AppError var once sync.Once pluginContext := pluginContext(c) - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { once.Do(func() { hookHasRunCh <- struct{}{} }) diff --git a/app/user.go b/app/user.go index 6e12bda34b..81faa5b57d 100644 --- a/app/user.go +++ b/app/user.go @@ -310,7 +310,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m pluginContext := pluginContext(c) a.Srv().Go(func() { - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasBeenCreated(pluginContext, ruser) return true }, plugin.UserHasBeenCreatedID) diff --git a/app/web_conn.go b/app/web_conn.go index cdf59eb31e..1db5866e0a 100644 --- a/app/web_conn.go +++ b/app/web_conn.go @@ -16,5 +16,5 @@ func (a *App) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfi // NewWebConn returns a new WebConn instance. func (a *App) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn { - return a.Srv().Platform().NewWebConn(cfg, a, a.ch) + return a.Srv().Platform().NewWebConn(cfg, a, a.Srv()) } diff --git a/cmd/mattermost/commands/init.go b/cmd/mattermost/commands/init.go index e93d9640fe..a90b4b02b3 100644 --- a/cmd/mattermost/commands/init.go +++ b/cmd/mattermost/commands/init.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" "github.com/mattermost/mattermost-server/v6/app" - "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/i18n" @@ -21,7 +20,11 @@ func initDBCommandContextCobra(command *cobra.Command, readOnlyConfigStore bool) panic(err) } - a.InitPlugins(request.EmptyContext(a.Log()), *a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory) + err = a.Srv().InitializePluginService() + if err != nil { + return nil, err + } + a.DoAppMigrations() return a, nil diff --git a/web/web_test.go b/web/web_test.go index 4214649031..b57e3888bd 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -282,7 +282,7 @@ func TestPublicFilesRequest(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, app.NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), nil) + env, err := plugin.NewEnvironment(th.NewPluginAPI, app.NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) pluginID := "com.mattermost.sample" @@ -329,7 +329,7 @@ func TestPublicFilesRequest(t *testing.T) { require.NotNil(t, manifest) require.True(t, activated) - th.App.Channels().SetPluginsEnvironment(env) + th.App.PluginService().SetPluginsEnvironment(env) req, _ := http.NewRequest("GET", "/plugins/com.mattermost.sample/public/hello.html", nil) res := httptest.NewRecorder() From 45159a6b09765e46668bd2124b04170dd080691c Mon Sep 17 00:00:00 2001 From: mattermod Date: Wed, 21 Dec 2022 15:28:13 +0000 Subject: [PATCH 40/41] Update latest version to 7.5.2 --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index d373e54fdc..aaa82b99ca 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV PATH="/mattermost/bin:${PATH}" ARG PUID=2000 ARG PGID=2000 -ARG MM_PACKAGE="https://releases.mattermost.com/7.5.1/mattermost-7.5.1-linux-amd64.tar.gz?src=docker" +ARG MM_PACKAGE="https://releases.mattermost.com/7.5.2/mattermost-7.5.2-linux-amd64.tar.gz?src=docker" # # Install needed packages and indirect dependencies RUN apt-get update \ From c25270414086fc4ae1be89b09db0cd714a954962 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 21 Dec 2022 22:10:26 +0300 Subject: [PATCH 41/41] Revert "[MM-48626] Move plugins environment out of Channels (#21730)" (#21934) --- api4/plugin.go | 4 +- api4/plugin_test.go | 2 +- api4/websocket.go | 2 +- app/app_iface.go | 1 - app/channel.go | 10 +- app/channels.go | 74 +++++- app/cluster_handlers.go | 11 +- app/collection.go | 16 +- app/download.go | 6 +- app/file.go | 2 +- app/integration_action.go | 6 +- app/login.go | 4 +- app/onboarding.go | 9 +- app/opentracing/opentracing_layer.go | 17 -- app/plugin.go | 380 ++++++++++----------------- app/plugin_api.go | 4 +- app/plugin_api_test.go | 24 +- app/plugin_commands.go | 72 ++--- app/plugin_commands_test.go | 10 +- app/plugin_db_driver.go | 13 +- app/plugin_db_driver_test.go | 2 +- app/plugin_event.go | 6 +- app/plugin_hooks_test.go | 14 +- app/plugin_install.go | 118 ++++----- app/plugin_install_test.go | 8 +- app/plugin_requests.go | 28 +- app/plugin_requests_test.go | 2 +- app/plugin_shutdown_test.go | 2 +- app/plugin_signature.go | 8 +- app/plugin_statuses.go | 36 +-- app/plugin_test.go | 46 ++-- app/post.go | 8 +- app/reaction.go | 4 +- app/server.go | 14 +- app/team.go | 4 +- app/upload.go | 2 +- app/user.go | 2 +- app/web_conn.go | 2 +- cmd/mattermost/commands/init.go | 7 +- web/web_test.go | 4 +- 40 files changed, 447 insertions(+), 537 deletions(-) diff --git a/api4/plugin.go b/api4/plugin.go index 475aa62f21..be5b298d02 100644 --- a/api4/plugin.go +++ b/api4/plugin.go @@ -155,7 +155,7 @@ func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request // https://mattermost.atlassian.net/browse/MM-41981 pluginRequest.Version = "" - manifest, appErr := c.App.PluginService().InstallMarketplacePlugin(pluginRequest) + manifest, appErr := c.App.Channels().InstallMarketplacePlugin(pluginRequest) if appErr != nil { c.Err = appErr return @@ -235,7 +235,7 @@ func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) { return } - err := c.App.PluginService().RemovePlugin(c.Params.PluginId) + err := c.App.Channels().RemovePlugin(c.Params.PluginId) if err != nil { c.Err = err return diff --git a/api4/plugin_test.go b/api4/plugin_test.go index 3b656c209a..1967f9a617 100644 --- a/api4/plugin_test.go +++ b/api4/plugin_test.go @@ -94,7 +94,7 @@ func TestPlugin(t *testing.T) { assert.Equal(t, "testplugin", manifest.Id) }) - th.App.PluginService().RemovePlugin(manifest.Id) + th.App.Channels().RemovePlugin(manifest.Id) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) diff --git a/api4/websocket.go b/api4/websocket.go index d8236e49b5..d2c1c10f44 100644 --- a/api4/websocket.go +++ b/api4/websocket.go @@ -61,7 +61,7 @@ func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) { } } - wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv()) + wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv().Channels()) if c.AppContext.Session().UserId != "" { c.App.Srv().Platform().HubRegister(wc) } diff --git a/app/app_iface.go b/app/app_iface.go index 2674801101..f10ca2d8b9 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -936,7 +936,6 @@ type AppIface interface { PermanentDeleteTeamId(c request.CTX, teamID string) *model.AppError PermanentDeleteUser(c *request.Context, user *model.User) *model.AppError PluginCommandsForTeam(teamID string) []*model.Command - PluginService() *PluginService PostActionCookieSecret() []byte PostAddToChannelMessage(c request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch diff --git a/app/channel.go b/app/channel.go index 31a8673383..fdc8d17da3 100644 --- a/app/channel.go +++ b/app/channel.go @@ -345,7 +345,7 @@ func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember boo a.Srv().Go(func() { pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ChannelHasBeenCreated(pluginContext, sc) return true }, plugin.ChannelHasBeenCreatedID) @@ -429,7 +429,7 @@ func (a *App) handleCreationEvent(c request.CTX, userID, otherUserID string, cha a.Srv().Go(func() { pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ChannelHasBeenCreated(pluginContext, channel) return true }, plugin.ChannelHasBeenCreatedID) @@ -1597,7 +1597,7 @@ func (a *App) AddChannelMember(c request.CTX, userID string, channel *model.Chan a.Srv().Go(func() { pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor) return true }, plugin.UserHasJoinedChannelID) @@ -2173,7 +2173,7 @@ func (a *App) JoinChannel(c request.CTX, channel *model.Channel, userID string) a.Srv().Go(func() { pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedChannel(pluginContext, cm, nil) return true }, plugin.UserHasJoinedChannelID) @@ -2483,7 +2483,7 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove a.Srv().Go(func() { pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLeftChannel(pluginContext, cm, actorUser) return true }, plugin.UserHasLeftChannelID) diff --git a/app/channels.go b/app/channels.go index c9771f25d6..7ab024e45a 100644 --- a/app/channels.go +++ b/app/channels.go @@ -6,11 +6,14 @@ package app import ( "fmt" "runtime" + "strings" "sync" "github.com/pkg/errors" "github.com/mattermost/mattermost-server/v6/app/imaging" + "github.com/mattermost/mattermost-server/v6/app/request" + "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/einterfaces" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" @@ -39,6 +42,12 @@ type Channels struct { postActionCookieSecret []byte + pluginCommandsLock sync.RWMutex + pluginCommands []*PluginCommand + pluginsLock sync.RWMutex + pluginsEnvironment *plugin.Environment + pluginConfigListenerID string + imageProxy *imageproxy.ImageProxy // cached counts that are used during notice condition validation @@ -70,6 +79,12 @@ type Channels struct { postReminderMut sync.Mutex postReminderTask *model.ScheduledTask + + // collectionTypes maps from collection types to the registering plugin id + collectionTypes map[string]string + // topicTypes maps from topic types to collection types + topicTypes map[string]string + collectionAndTopicTypesMut sync.Mutex } func init() { @@ -92,9 +107,11 @@ func NewChannels(services map[product.ServiceKey]any) (*Channels, error) { return nil, errors.New("server not passed") } ch := &Channels{ - srv: s, - imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()), - uploadLockMap: map[string]bool{}, + srv: s, + imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()), + uploadLockMap: map[string]bool{}, + collectionTypes: map[string]string{}, + topicTypes: map[string]string{}, } // To get another service: @@ -191,6 +208,10 @@ func NewChannels(services map[product.ServiceKey]any) (*Channels, error) { services[product.RouterKey] = ch.routerSvc // Setup routes. + pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() + pluginsRoute.HandleFunc("", ch.ServePluginRequest) + pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest) + pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest) services[product.PostKey] = &postServiceWrapper{ app: &App{ch: ch}, @@ -222,6 +243,39 @@ func NewChannels(services map[product.ServiceKey]any) (*Channels, error) { } func (ch *Channels) Start() error { + // Start plugins + ctx := request.EmptyContext(ch.srv.Log()) + ch.initPlugins(ctx, *ch.cfgSvc.Config().PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory) + + ch.AddConfigListener(func(prevCfg, cfg *model.Config) { + // We compute the difference between configs + // to ensure we don't re-init plugins unnecessarily. + diffs, err := config.Diff(prevCfg, cfg) + if err != nil { + ch.srv.Log().Warn("Error in comparing configs", mlog.Err(err)) + return + } + + hasDiff := false + // TODO: This could be a method on ConfigDiffs itself + for _, diff := range diffs { + if strings.HasPrefix(diff.Path, "PluginSettings.") { + hasDiff = true + break + } + } + + // Do only if some plugin related settings has changed. + if hasDiff { + if *cfg.PluginSettings.Enable { + ch.initPlugins(ctx, *cfg.PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory) + } else { + ch.ShutDownPlugins() + } + } + + }) + // TODO: This should be moved to the platform service. if err := ch.srv.platform.EnsureAsymmetricSigningKey(); err != nil { return errors.Wrapf(err, "unable to ensure asymmetric signing key") @@ -235,6 +289,8 @@ func (ch *Channels) Start() error { } func (ch *Channels) Stop() error { + ch.ShutDownPlugins() + ch.dndTaskMut.Lock() if ch.dndTask != nil { ch.dndTask.Cancel() @@ -276,18 +332,18 @@ func (s *hooksService) RegisterHooks(productID string, hooks any) error { return s.ch.srv.hooksManager.AddProduct(productID, hooks) } -func (s *Server) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) { - if env := s.pluginService.GetPluginsEnvironment(); env != nil { +func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) { + if env := ch.GetPluginsEnvironment(); env != nil { env.RunMultiPluginHook(hookRunnerFunc, hookId) } // run hook for the products - s.hooksManager.RunMultiHook(hookRunnerFunc, hookId) + ch.srv.hooksManager.RunMultiHook(hookRunnerFunc, hookId) } -func (s *Server) HooksForPluginOrProduct(id string) (plugin.Hooks, error) { +func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) { var hooks plugin.Hooks - if env := s.pluginService.GetPluginsEnvironment(); env != nil { + if env := ch.GetPluginsEnvironment(); env != nil { // we intentionally ignore the error here, because the id can be a product id // we are going to check if we have the hooks or not hooks, _ = env.HooksForPlugin(id) @@ -296,7 +352,7 @@ func (s *Server) HooksForPluginOrProduct(id string) (plugin.Hooks, error) { } } - hooks = s.hooksManager.HooksForProduct(id) + hooks = ch.srv.hooksManager.HooksForProduct(id) if hooks != nil { return hooks, nil } diff --git a/app/cluster_handlers.go b/app/cluster_handlers.go index 1cebe5596d..3fa90abf1e 100644 --- a/app/cluster_handlers.go +++ b/app/cluster_handlers.go @@ -16,7 +16,7 @@ func (s *Server) clusterInstallPluginHandler(msg *model.ClusterMessage) { if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil { mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr)) } - s.pluginService.installPluginFromData(data) + s.Channels().installPluginFromData(data) } func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) { @@ -24,7 +24,7 @@ func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) { if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil { mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr)) } - s.pluginService.removePluginFromData(data) + s.Channels().removePluginFromData(data) } func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) { @@ -44,7 +44,12 @@ func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) { return } - hooks, err := s.HooksForPluginOrProduct(pluginID) + channels, ok := s.products["channels"].(*Channels) + if !ok { + return + } + + hooks, err := channels.HooksForPluginOrProduct(pluginID) if err != nil { mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err)) return diff --git a/app/collection.go b/app/collection.go index ff489a18db..9b895e3bc0 100644 --- a/app/collection.go +++ b/app/collection.go @@ -10,26 +10,26 @@ import ( "github.com/mattermost/mattermost-server/v6/shared/mlog" ) -func (s *PluginService) registerCollectionAndTopic(pluginID, collectionType, topicType string) error { +func (a *App) registerCollectionAndTopic(pluginID, collectionType, topicType string) error { // we have a race condition due to multiple plugins calling this method - s.collectionAndTopicTypesMut.Lock() - defer s.collectionAndTopicTypesMut.Unlock() + a.ch.collectionAndTopicTypesMut.Lock() + defer a.ch.collectionAndTopicTypesMut.Unlock() // check if collectionType was already registered by other plugin - existingPluginID, ok := s.collectionTypes[collectionType] + existingPluginID, ok := a.ch.collectionTypes[collectionType] if ok && existingPluginID != pluginID { return model.NewAppError("registerCollectionAndTopic", "app.collection.add_collection.exists.app_error", nil, "", http.StatusBadRequest) } // check if topicType was already registered to other collection - existingCollectionType, ok := s.topicTypes[topicType] + existingCollectionType, ok := a.ch.topicTypes[topicType] if ok && existingCollectionType != collectionType { return model.NewAppError("registerCollectionAndTopic", "app.collection.add_topic.exists.app_error", nil, "", http.StatusBadRequest) } - s.collectionTypes[collectionType] = pluginID - s.topicTypes[topicType] = collectionType + a.ch.collectionTypes[collectionType] = pluginID + a.ch.topicTypes[topicType] = collectionType - s.platform.Log().Info("registered collection and topic type", mlog.String("plugin_id", pluginID), mlog.String("collection_type", collectionType), mlog.String("topic_type", topicType)) + a.ch.srv.Log().Info("registered collection and topic type", mlog.String("plugin_id", pluginID), mlog.String("collection_type", collectionType), mlog.String("topic_type", topicType)) return nil } diff --git a/app/download.go b/app/download.go index 56438507cc..449f787c46 100644 --- a/app/download.go +++ b/app/download.go @@ -22,10 +22,10 @@ const ( ) func (a *App) DownloadFromURL(downloadURL string) ([]byte, error) { - return a.Srv().pluginService.downloadFromURL(downloadURL) + return a.Srv().downloadFromURL(downloadURL) } -func (s *PluginService) downloadFromURL(downloadURL string) ([]byte, error) { +func (s *Server) downloadFromURL(downloadURL string) ([]byte, error) { if !model.IsValidHTTPURL(downloadURL) { return nil, errors.Errorf("invalid url %s", downloadURL) } @@ -38,7 +38,7 @@ func (s *PluginService) downloadFromURL(downloadURL string) ([]byte, error) { return nil, errors.Errorf("insecure url not allowed %s", downloadURL) } - client := s.httpService.MakeClient(true) + client := s.HTTPService().MakeClient(true) client.Timeout = HTTPRequestTimeout var resp *http.Response diff --git a/app/file.go b/app/file.go index e1acafa827..7a66d25010 100644 --- a/app/file.go +++ b/app/file.go @@ -926,7 +926,7 @@ func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTe var rejectionError *model.AppError pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { var newBytes bytes.Buffer replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes) if rejectionReason != "" { diff --git a/app/integration_action.go b/app/integration_action.go index 51bef2b86f..4ae7e97e07 100644 --- a/app/integration_action.go +++ b/app/integration_action.go @@ -375,10 +375,10 @@ func (w *LocalResponseWriter) WriteHeader(statusCode int) { } func (a *App) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) { - return a.ch.srv.pluginService.doPluginRequest(c, method, rawURL, values, body) + return a.ch.doPluginRequest(c, method, rawURL, values, body) } -func (s *PluginService) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) { +func (ch *Channels) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) { rawURL = strings.TrimPrefix(rawURL, "/") inURL, err := url.Parse(rawURL) if err != nil { @@ -427,7 +427,7 @@ func (s *PluginService) doPluginRequest(c *request.Context, method, rawURL strin params["plugin_id"] = pluginID r = mux.SetURLVars(r, params) - s.ServePluginRequest(w, r) + ch.ServePluginRequest(w, r) resp := &http.Response{ StatusCode: w.status, diff --git a/app/login.go b/app/login.go index af322bd13b..e0854bef37 100644 --- a/app/login.go +++ b/app/login.go @@ -159,7 +159,7 @@ func (a *App) GetUserForLogin(id, loginId string) (*model.User, *model.AppError) func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile, isOAuthUser, isSaml bool) *model.AppError { var rejectionReason string pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { rejectionReason = hooks.UserWillLogIn(pluginContext, user) return rejectionReason == "" }, plugin.UserWillLogInID) @@ -225,7 +225,7 @@ func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request } a.Srv().Go(func() { - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLoggedIn(pluginContext, user) return true }, plugin.UserHasLoggedInID) diff --git a/app/onboarding.go b/app/onboarding.go index fe40e9d997..d76525f017 100644 --- a/app/onboarding.go +++ b/app/onboarding.go @@ -28,6 +28,11 @@ func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError { } func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError { + pluginsEnvironment := a.Channels().GetPluginsEnvironment() + if pluginsEnvironment == nil { + return a.markAdminOnboardingComplete(c) + } + pluginContext := pluginContext(c) for _, pluginID := range request.InstallPlugins { @@ -36,7 +41,7 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo installRequest := &model.InstallMarketplacePluginRequest{ Id: id, } - _, appErr := a.Srv().pluginService.InstallMarketplacePlugin(installRequest) + _, appErr := a.Channels().InstallMarketplacePlugin(installRequest) if appErr != nil { mlog.Error("Failed to install plugin for onboarding", mlog.String("id", id), mlog.Err(appErr)) return @@ -48,7 +53,7 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo return } - hooks, err := a.Srv().HooksForPluginOrProduct(id) + hooks, err := a.ch.HooksForPluginOrProduct(id) if err != nil { mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", id), mlog.Err(err)) return diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 3d6c2b5ad5..367d98ec39 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -13125,23 +13125,6 @@ func (a *OpenTracingAppLayer) PluginCommandsForTeam(teamID string) []*model.Comm return resultVar0 } -func (a *OpenTracingAppLayer) PluginService() *app.PluginService { - origCtx := a.ctx - span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PluginService") - - 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.PluginService() - - return resultVar0 -} - func (a *OpenTracingAppLayer) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PopulateWebConnConfig") diff --git a/app/plugin.go b/app/plugin.go index d985455e5b..679576e723 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -20,37 +20,16 @@ import ( svg "github.com/h2non/go-is-svg" "github.com/pkg/errors" - "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/app/request" - "github.com/mattermost/mattermost-server/v6/config" "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/shared/filestore" "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/utils/fileutils" ) -type PluginService struct { - platform *platform.PlatformService - channels *Channels - fileStore filestore.FileBackend - httpService httpservice.HTTPService - - pluginCommandsLock sync.RWMutex - pluginCommands []*PluginCommand - pluginsLock sync.RWMutex - pluginsEnvironment *plugin.Environment - pluginConfigListenerID string - // collectionTypes maps from collection types to the registering plugin id - collectionTypes map[string]string - // topicTypes maps from topic types to collection types - topicTypes map[string]string - collectionAndTopicTypesMut sync.Mutex -} - const prepackagedPluginsDir = "prepackaged_plugins" type pluginSignaturePath struct { @@ -84,91 +63,20 @@ func (rs *routerService) getHandler(productID string) (http.Handler, bool) { return handler, ok } -func (a *App) PluginService() *PluginService { - return a.ch.srv.pluginService -} - -func (s *Server) InitializePluginService() error { - product, ok := s.products["channels"] - if !ok { - return errors.New("unable to find channels product") - } - channels, ok := product.(*Channels) - if !ok { - return errors.New("unable to cast product to channels product") - } - - ps := &PluginService{ - platform: s.platform, - channels: channels, - fileStore: s.platform.FileBackend(), - httpService: s.httpService, - collectionTypes: make(map[string]string), - topicTypes: make(map[string]string), - } - s.pluginService = ps - - pluginsRoute := s.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() - pluginsRoute.HandleFunc("", ps.ServePluginRequest) - pluginsRoute.HandleFunc("/public/{public_file:.*}", ps.ServePluginPublicRequest) - pluginsRoute.HandleFunc("/{anything:.*}", ps.ServePluginRequest) - - ps.initPlugins(request.EmptyContext(s.platform.Log()), *s.platform.Config().PluginSettings.Directory, *s.platform.Config().PluginSettings.ClientDirectory) - - // Start plugins - ctx := request.EmptyContext(s.platform.Log()) - - // Add the config listener to enable/disable plugins - s.platform.AddConfigListener(func(prevCfg, cfg *model.Config) { - // We compute the difference between configs - // to ensure we don't re-init plugins unnecessarily. - diffs, err := config.Diff(prevCfg, cfg) - if err != nil { - s.platform.Log().Warn("Error in comparing configs", mlog.Err(err)) - return - } - - hasDiff := false - // TODO: This could be a method on ConfigDiffs itself - for _, diff := range diffs { - if strings.HasPrefix(diff.Path, "PluginSettings.") { - hasDiff = true - break - } - } - - // Do only if some plugin related settings has changed. - if hasDiff { - if *cfg.PluginSettings.Enable { - s.pluginService.initPlugins(ctx, *cfg.PluginSettings.Directory, *s.Config().PluginSettings.ClientDirectory) - } else { - s.pluginService.ShutDownPlugins() - } - } - - }) - - return nil -} - -func (s *Server) GetPluginsEnvironment() *plugin.Environment { - return s.pluginService.GetPluginsEnvironment() -} - // GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and // initialized. // // To get the plugins environment when the plugins are disabled, manually acquire the plugins // lock instead. -func (s *PluginService) GetPluginsEnvironment() *plugin.Environment { - if !*s.platform.Config().PluginSettings.Enable { +func (ch *Channels) GetPluginsEnvironment() *plugin.Environment { + if !*ch.cfgSvc.Config().PluginSettings.Enable { return nil } - s.pluginsLock.RLock() - defer s.pluginsLock.RUnlock() + ch.pluginsLock.RLock() + defer ch.pluginsLock.RUnlock() - return s.pluginsEnvironment + return ch.pluginsEnvironment } // GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and @@ -177,39 +85,33 @@ func (s *PluginService) GetPluginsEnvironment() *plugin.Environment { // To get the plugins environment when the plugins are disabled, manually acquire the plugins // lock instead. func (a *App) GetPluginsEnvironment() *plugin.Environment { - // TODO: Telemetry service starts before products start, so we need to check if the plugin service is initialized. - // Move the telemetry service to start after products start. - if a.ch.srv.pluginService == nil { - return nil - } - - return a.ch.srv.pluginService.GetPluginsEnvironment() + return a.ch.GetPluginsEnvironment() } -func (s *PluginService) SetPluginsEnvironment(pluginsEnvironment *plugin.Environment) { - s.pluginsLock.Lock() - defer s.pluginsLock.Unlock() +func (ch *Channels) SetPluginsEnvironment(pluginsEnvironment *plugin.Environment) { + ch.pluginsLock.Lock() + defer ch.pluginsLock.Unlock() - s.pluginsEnvironment = pluginsEnvironment - s.platform.SetPluginsEnvironment(s.channels.srv) + ch.pluginsEnvironment = pluginsEnvironment + ch.srv.Platform().SetPluginsEnvironment(ch) } -func (s *PluginService) syncPluginsActiveState() { +func (ch *Channels) syncPluginsActiveState() { // Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment. - s.pluginsLock.RLock() - pluginsEnvironment := s.pluginsEnvironment - s.pluginsLock.RUnlock() + ch.pluginsLock.RLock() + pluginsEnvironment := ch.pluginsEnvironment + ch.pluginsLock.RUnlock() if pluginsEnvironment == nil { return } - config := s.platform.Config().PluginSettings + config := ch.cfgSvc.Config().PluginSettings if *config.Enable { availablePlugins, err := pluginsEnvironment.Available() if err != nil { - s.platform.Log().Error("Unable to get available plugins", mlog.Err(err)) + ch.srv.Log().Error("Unable to get available plugins", mlog.Err(err)) return } @@ -223,24 +125,24 @@ func (s *PluginService) syncPluginsActiveState() { pluginEnabled = state.Enable } - if hasOverride, value := s.getPluginStateOverride(pluginID); hasOverride { + if hasOverride, value := ch.getPluginStateOverride(pluginID); hasOverride { pluginEnabled = value } if pluginEnabled { // Disable focalboard in product mode. - if pluginID == model.PluginIdFocalboard && s.platform.Config().FeatureFlags.BoardsProduct { + if pluginID == model.PluginIdFocalboard && ch.cfgSvc.Config().FeatureFlags.BoardsProduct { msg := "Plugin cannot run in product mode. Disabling." mlog.Warn(msg, mlog.String("plugin_id", model.PluginIdFocalboard)) // This is a mini-version of ch.disablePlugin. // We don't call that directly, because that will recursively call // this method. - s.platform.UpdateConfig(func(cfg *model.Config) { + ch.cfgSvc.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[pluginID] = &model.PluginState{Enable: false} }) pluginsEnvironment.SetPluginError(pluginID, msg) - s.unregisterPluginCommands(pluginID) + ch.unregisterPluginCommands(pluginID) disabledPlugins = append(disabledPlugins, plugin) continue } @@ -264,7 +166,7 @@ func (s *PluginService) syncPluginsActiveState() { if deactivated && plugin.Manifest.HasClient() { message := model.NewWebSocketEvent(model.WebsocketEventPluginDisabled, "", "", "", nil, "") message.Add("manifest", plugin.Manifest.ClientManifest()) - s.platform.Publish(message) + ch.srv.platform.Publish(message) } }(plugin) } @@ -278,14 +180,14 @@ func (s *PluginService) syncPluginsActiveState() { pluginID := plugin.Manifest.Id updatedManifest, activated, err := pluginsEnvironment.Activate(pluginID) if err != nil { - plugin.WrapLogger(s.platform.Log().(*mlog.Logger)).Error("Unable to activate plugin", mlog.Err(err)) + plugin.WrapLogger(ch.srv.Log()).Error("Unable to activate plugin", mlog.Err(err)) return } if activated { // Notify all cluster clients if ready - if err := s.notifyPluginEnabled(updatedManifest); err != nil { - s.platform.Log().Error("Failed to notify cluster on plugin enable", mlog.Err(err)) + if err := ch.notifyPluginEnabled(updatedManifest); err != nil { + ch.srv.Log().Error("Failed to notify cluster on plugin enable", mlog.Err(err)) } } }(plugin) @@ -295,7 +197,7 @@ func (s *PluginService) syncPluginsActiveState() { pluginsEnvironment.Shutdown() } - if err := s.notifyPluginStatusesChanged(); err != nil { + if err := ch.notifyPluginStatusesChanged(); err != nil { mlog.Warn("failed to notify plugin status changed", mlog.Err(err)) } } @@ -305,29 +207,27 @@ func (a *App) NewPluginAPI(c *request.Context, manifest *model.Manifest) plugin. } func (a *App) InitPlugins(c *request.Context, pluginDir, webappPluginDir string) { - a.ch.srv.pluginService.initPlugins(c, pluginDir, webappPluginDir) + a.ch.initPlugins(c, pluginDir, webappPluginDir) } -func (s *PluginService) initPlugins(c *request.Context, pluginDir, webappPluginDir string) { +func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir string) { // Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment. defer func() { - // platform service requires plugins environment to be initialized - // so that it can use it in cluster service initialization - s.platform.SetPluginsEnvironment(s.channels.srv) + ch.srv.Platform().SetPluginsEnvironment(ch) }() - s.pluginsLock.RLock() - pluginsEnvironment := s.pluginsEnvironment - s.pluginsLock.RUnlock() - if pluginsEnvironment != nil || !*s.platform.Config().PluginSettings.Enable { - s.syncPluginsActiveState() + ch.pluginsLock.RLock() + pluginsEnvironment := ch.pluginsEnvironment + ch.pluginsLock.RUnlock() + if pluginsEnvironment != nil || !*ch.cfgSvc.Config().PluginSettings.Enable { + ch.syncPluginsActiveState() if pluginsEnvironment != nil { - pluginsEnvironment.TogglePluginHealthCheckJob(*s.platform.Config().PluginSettings.EnableHealthCheck) + pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck) } return } - s.platform.Log().Info("Starting up plugins") + ch.srv.Log().Info("Starting up plugins") if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) { mlog.Error("Failed to start up plugins", mlog.Err(err)) @@ -340,77 +240,77 @@ func (s *PluginService) initPlugins(c *request.Context, pluginDir, webappPluginD } newAPIFunc := func(manifest *model.Manifest) plugin.API { - return New(ServerConnector(s.channels)).NewPluginAPI(c, manifest) + return New(ServerConnector(ch)).NewPluginAPI(c, manifest) } env, err := plugin.NewEnvironment( newAPIFunc, - NewDriverImpl(s.platform), + NewDriverImpl(ch.srv), pluginDir, webappPluginDir, - *s.platform.Config().ExperimentalSettings.PatchPluginsReactDOM, - s.platform.Logger(), - s.platform.Metrics(), + *ch.cfgSvc.Config().ExperimentalSettings.PatchPluginsReactDOM, + ch.srv.Log(), + ch.srv.GetMetrics(), ) if err != nil { mlog.Error("Failed to start up plugins", mlog.Err(err)) return } - s.pluginsLock.Lock() - s.pluginsEnvironment = env - s.pluginsLock.Unlock() + ch.pluginsLock.Lock() + ch.pluginsEnvironment = env + ch.pluginsLock.Unlock() - s.pluginsEnvironment.TogglePluginHealthCheckJob(*s.platform.Config().PluginSettings.EnableHealthCheck) + ch.pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck) - if err := s.syncPlugins(); err != nil { + if err := ch.syncPlugins(); err != nil { mlog.Error("Failed to sync plugins from the file store", mlog.Err(err)) } - plugins := s.processPrepackagedPlugins(prepackagedPluginsDir) - pluginsEnvironment = s.GetPluginsEnvironment() + plugins := ch.processPrepackagedPlugins(prepackagedPluginsDir) + pluginsEnvironment = ch.GetPluginsEnvironment() if pluginsEnvironment == nil { mlog.Info("Plugins environment not found, server is likely shutting down") return } pluginsEnvironment.SetPrepackagedPlugins(plugins) - s.installFeatureFlagPlugins() + ch.installFeatureFlagPlugins() // Sync plugin active state when config changes. Also notify plugins. - s.pluginsLock.Lock() - s.platform.RemoveConfigListener(s.pluginConfigListenerID) - s.pluginConfigListenerID = s.platform.AddConfigListener(func(old, new *model.Config) { + ch.pluginsLock.Lock() + ch.RemoveConfigListener(ch.pluginConfigListenerID) + ch.pluginConfigListenerID = ch.AddConfigListener(func(old, new *model.Config) { // If plugin status remains unchanged, only then run this. // Because (*App).InitPlugins is already run as a config change hook. if *old.PluginSettings.Enable == *new.PluginSettings.Enable { - s.installFeatureFlagPlugins() - s.syncPluginsActiveState() + ch.installFeatureFlagPlugins() + ch.syncPluginsActiveState() } - s.pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + ch.RunMultiHook(func(hooks plugin.Hooks) bool { if err := hooks.OnConfigurationChange(); err != nil { - s.platform.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err)) + ch.srv.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err)) } return true }, plugin.OnConfigurationChangeID) }) - s.pluginsLock.Unlock() + ch.pluginsLock.Unlock() - s.syncPluginsActiveState() + ch.syncPluginsActiveState() } // SyncPlugins synchronizes the plugins installed locally // with the plugin bundles available in the file store. func (a *App) SyncPlugins() *model.AppError { - return a.ch.srv.pluginService.syncPlugins() + return a.ch.syncPlugins() } // SyncPlugins synchronizes the plugins installed locally // with the plugin bundles available in the file store. -func (s *PluginService) syncPlugins() *model.AppError { +func (ch *Channels) syncPlugins() *model.AppError { mlog.Info("Syncing plugins from the file store") - pluginsEnvironment := s.GetPluginsEnvironment() + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("SyncPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -426,14 +326,14 @@ func (s *PluginService) syncPlugins() *model.AppError { go func(pluginID string) { defer wg.Done() // Only handle managed plugins with .filestore flag file. - _, err := os.Stat(filepath.Join(*s.platform.Config().PluginSettings.Directory, pluginID, managedPluginFileName)) + _, err := os.Stat(filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, pluginID, managedPluginFileName)) if os.IsNotExist(err) { mlog.Warn("Skipping sync for unmanaged plugin", mlog.String("plugin_id", pluginID)) } else if err != nil { mlog.Error("Skipping sync for plugin after failure to check if managed", mlog.String("plugin_id", pluginID), mlog.Err(err)) } else { mlog.Debug("Removing local installation of managed plugin before sync", mlog.String("plugin_id", pluginID)) - if err := s.removePluginLocally(pluginID); err != nil { + if err := ch.removePluginLocally(pluginID); err != nil { mlog.Error("Failed to remove local installation of managed plugin before sync", mlog.String("plugin_id", pluginID), mlog.Err(err)) } } @@ -442,7 +342,7 @@ func (s *PluginService) syncPlugins() *model.AppError { wg.Wait() // Install plugins from the file store. - pluginSignaturePathMap, appErr := s.getPluginsFromFolder() + pluginSignaturePathMap, appErr := ch.getPluginsFromFolder() if appErr != nil { return appErr } @@ -451,7 +351,7 @@ func (s *PluginService) syncPlugins() *model.AppError { wg.Add(1) go func(plugin *pluginSignaturePath) { defer wg.Done() - reader, appErr := s.fileStore.Reader(plugin.path) + reader, appErr := ch.srv.fileReader(plugin.path) if appErr != nil { mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr)) return @@ -459,8 +359,8 @@ func (s *PluginService) syncPlugins() *model.AppError { defer reader.Close() var signature filestore.ReadCloseSeeker - if *s.platform.Config().PluginSettings.RequirePluginSignature { - signature, appErr = s.fileStore.Reader(plugin.signaturePath) + if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature { + signature, appErr = ch.srv.fileReader(plugin.signaturePath) if appErr != nil { mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr)) return @@ -469,7 +369,7 @@ func (s *PluginService) syncPlugins() *model.AppError { } mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path)) - if _, err := s.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil { + if _, err := ch.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil { mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err)) } }(plugin) @@ -479,11 +379,11 @@ func (s *PluginService) syncPlugins() *model.AppError { return nil } -func (s *PluginService) ShutDownPlugins() { +func (ch *Channels) ShutDownPlugins() { // Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment. - s.pluginsLock.RLock() - pluginsEnvironment := s.pluginsEnvironment - s.pluginsLock.RUnlock() + ch.pluginsLock.RLock() + pluginsEnvironment := ch.pluginsEnvironment + ch.pluginsLock.RUnlock() if pluginsEnvironment == nil { return } @@ -492,14 +392,14 @@ func (s *PluginService) ShutDownPlugins() { pluginsEnvironment.Shutdown() - s.platform.RemoveConfigListener(s.pluginConfigListenerID) - s.pluginConfigListenerID = "" + ch.RemoveConfigListener(ch.pluginConfigListenerID) + ch.pluginConfigListenerID = "" // Acquiring lock manually before cleaning up PluginsEnvironment. - s.pluginsLock.Lock() - defer s.pluginsLock.Unlock() - if s.pluginsEnvironment == pluginsEnvironment { - s.pluginsEnvironment = nil + ch.pluginsLock.Lock() + defer ch.pluginsLock.Unlock() + if ch.pluginsEnvironment == pluginsEnvironment { + ch.pluginsEnvironment = nil } else { mlog.Warn("Another PluginsEnvironment detected while shutting down plugins.") } @@ -525,11 +425,11 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) { // activation if inactive anywhere in the cluster. // Notifies cluster peers through config change. func (a *App) EnablePlugin(id string) *model.AppError { - return a.PluginService().enablePlugin(id) + return a.ch.enablePlugin(id) } -func (s *PluginService) enablePlugin(id string) *model.AppError { - pluginsEnvironment := s.GetPluginsEnvironment() +func (ch *Channels) enablePlugin(id string) *model.AppError { + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -553,16 +453,16 @@ func (s *PluginService) enablePlugin(id string) *model.AppError { return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound) } - if id == model.PluginIdFocalboard && s.platform.Config().FeatureFlags.BoardsProduct { + if id == model.PluginIdFocalboard && ch.cfgSvc.Config().FeatureFlags.BoardsProduct { return model.NewAppError("EnablePlugin", "app.plugin.product_mode.app_error", map[string]any{"Name": model.PluginIdFocalboard}, "", http.StatusBadRequest) } - s.platform.UpdateConfig(func(cfg *model.Config) { + ch.cfgSvc.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true} }) // This call will implicitly invoke SyncPluginsActiveState which will activate enabled plugins. - if _, _, err := s.platform.SaveConfig(s.platform.Config(), true); err != nil { + if _, _, err := ch.cfgSvc.SaveConfig(ch.cfgSvc.Config(), true); err != nil { if err.Id == "ent.cluster.save_config.error" { return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError) } @@ -575,7 +475,7 @@ func (s *PluginService) enablePlugin(id string) *model.AppError { // DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active. // Notifies cluster peers through config change. func (a *App) DisablePlugin(id string) *model.AppError { - appErr := a.ch.srv.pluginService.disablePlugin(id) + appErr := a.ch.disablePlugin(id) if appErr != nil { return appErr } @@ -583,22 +483,22 @@ func (a *App) DisablePlugin(id string) *model.AppError { return nil } -func (s *PluginService) disablePlugin(id string) *model.AppError { +func (ch *Channels) disablePlugin(id string) *model.AppError { // find all collectionTypes registered by plugin - for collectionTypeToRemove, existingPluginId := range s.collectionTypes { + for collectionTypeToRemove, existingPluginId := range ch.collectionTypes { if existingPluginId != id { continue } // find all topicTypes for existing collectionType - for topicTypeToRemove, existingCollectionType := range s.topicTypes { + for topicTypeToRemove, existingCollectionType := range ch.topicTypes { if existingCollectionType == collectionTypeToRemove { - delete(s.topicTypes, topicTypeToRemove) + delete(ch.topicTypes, topicTypeToRemove) } } - delete(s.collectionTypes, collectionTypeToRemove) + delete(ch.collectionTypes, collectionTypeToRemove) } - pluginsEnvironment := s.GetPluginsEnvironment() + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -622,13 +522,13 @@ func (s *PluginService) disablePlugin(id string) *model.AppError { return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound) } - s.platform.UpdateConfig(func(cfg *model.Config) { + ch.cfgSvc.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false} }) - s.unregisterPluginCommands(id) + ch.unregisterPluginCommands(id) // This call will implicitly invoke SyncPluginsActiveState which will deactivate disabled plugins. - if _, _, err := s.platform.SaveConfig(s.platform.Config(), true); err != nil { + if _, _, err := ch.cfgSvc.SaveConfig(ch.cfgSvc.Config(), true); err != nil { return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -715,8 +615,8 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m // getPrepackagedPlugin returns a pre-packaged plugin. // // If version is empty, the first matching plugin is returned. -func (s *PluginService) getPrepackagedPlugin(pluginID, version string) (*plugin.PrepackagedPlugin, *model.AppError) { - pluginsEnvironment := s.GetPluginsEnvironment() +func (ch *Channels) getPrepackagedPlugin(pluginID, version string) (*plugin.PrepackagedPlugin, *model.AppError) { + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.config.app_error", nil, "plugin environment is nil", http.StatusInternalServerError) } @@ -734,16 +634,16 @@ func (s *PluginService) getPrepackagedPlugin(pluginID, version string) (*plugin. // getRemoteMarketplacePlugin returns plugin from marketplace-server. // // If version is empty, the latest compatible version is used. -func (s *PluginService) getRemoteMarketplacePlugin(pluginID, version string) (*model.BaseMarketplacePlugin, *model.AppError) { +func (ch *Channels) getRemoteMarketplacePlugin(pluginID, version string) (*model.BaseMarketplacePlugin, *model.AppError) { marketplaceClient, err := marketplace.NewClient( - *s.platform.Config().PluginSettings.MarketplaceURL, - s.httpService, + *ch.cfgSvc.Config().PluginSettings.MarketplaceURL, + ch.srv.HTTPService(), ) if err != nil { return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - filter := s.getBaseMarketplaceFilter() + filter := ch.getBaseMarketplaceFilter() filter.PluginId = pluginID var plugin *model.BaseMarketplacePlugin @@ -898,15 +798,15 @@ func (a *App) mergeLocalPlugins(remoteMarketplacePlugins map[string]*model.Marke } func (a *App) getBaseMarketplaceFilter() *model.MarketplacePluginFilter { - return a.ch.srv.pluginService.getBaseMarketplaceFilter() + return a.ch.getBaseMarketplaceFilter() } -func (s *PluginService) getBaseMarketplaceFilter() *model.MarketplacePluginFilter { +func (ch *Channels) getBaseMarketplaceFilter() *model.MarketplacePluginFilter { filter := &model.MarketplacePluginFilter{ ServerVersion: model.CurrentVersion, } - license := s.platform.License() + license := ch.srv.License() if license != nil && license.HasEnterpriseMarketplacePlugins() { filter.EnterprisePlugins = true } @@ -953,8 +853,8 @@ func pluginMatchesFilter(manifest *model.Manifest, filter string) bool { // it will notify all connected websocket clients (across all peers) to trigger the (re-)installation. // There is a small chance that this never occurs, because the last server to finish installing dies before it can announce. // There is also a chance that multiple servers notify, but the webapp handles this idempotently. -func (s *PluginService) notifyPluginEnabled(manifest *model.Manifest) error { - pluginsEnvironment := s.GetPluginsEnvironment() +func (ch *Channels) notifyPluginEnabled(manifest *model.Manifest) error { + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return errors.New("pluginsEnvironment is nil") } @@ -964,15 +864,15 @@ func (s *PluginService) notifyPluginEnabled(manifest *model.Manifest) error { var statuses model.PluginStatuses - if s.platform.Cluster() != nil { + if ch.srv.platform.Cluster() != nil { var err *model.AppError - statuses, err = s.platform.Cluster().GetPluginStatuses() + statuses, err = ch.srv.platform.Cluster().GetPluginStatuses() if err != nil { return err } } - localStatus, err := s.GetPluginStatus(manifest.Id) + localStatus, err := ch.GetPluginStatus(manifest.Id) if err != nil { return err } @@ -992,26 +892,26 @@ func (s *PluginService) notifyPluginEnabled(manifest *model.Manifest) error { // Notify all cluster peer clients. message := model.NewWebSocketEvent(model.WebsocketEventPluginEnabled, "", "", "", nil, "") message.Add("manifest", manifest.ClientManifest()) - s.platform.Publish(message) + ch.srv.platform.Publish(message) return nil } -func (s *PluginService) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.AppError) { - fileStorePaths, appErr := s.fileStore.ListDirectory(fileStorePluginFolder) +func (ch *Channels) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.AppError) { + fileStorePaths, appErr := ch.srv.listDirectory(fileStorePluginFolder, false) if appErr != nil { return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) } - return s.getPluginsFromFilePaths(fileStorePaths), nil + return ch.getPluginsFromFilePaths(fileStorePaths), nil } -func (s *PluginService) getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath { +func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath { pluginSignaturePathMap := make(map[string]*pluginSignaturePath) fsPrefix := "" - if *s.platform.Config().FileSettings.DriverName == model.ImageDriverS3 { - ptr := s.platform.Config().FileSettings.AmazonS3PathPrefix + if *ch.cfgSvc.Config().FileSettings.DriverName == model.ImageDriverS3 { + ptr := ch.cfgSvc.Config().FileSettings.AmazonS3PathPrefix if ptr != nil && *ptr != "" { fsPrefix = *ptr + "/" } @@ -1044,7 +944,7 @@ func (s *PluginService) getPluginsFromFilePaths(fileStorePaths []string) map[str return pluginSignaturePathMap } -func (s *PluginService) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin { +func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin { prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir) if !found { return nil @@ -1060,7 +960,7 @@ func (s *PluginService) processPrepackagedPlugins(pluginsDir string) []*plugin.P return nil } - pluginSignaturePathMap := s.getPluginsFromFilePaths(fileStorePaths) + pluginSignaturePathMap := ch.getPluginsFromFilePaths(fileStorePaths) plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap)) prepackagedPlugins := make(chan *plugin.PrepackagedPlugin, len(pluginSignaturePathMap)) @@ -1069,7 +969,7 @@ func (s *PluginService) processPrepackagedPlugins(pluginsDir string) []*plugin.P wg.Add(1) go func(psPath *pluginSignaturePath) { defer wg.Done() - p, err := s.processPrepackagedPlugin(psPath) + p, err := ch.processPrepackagedPlugin(psPath) if err != nil { mlog.Error("Failed to install prepackaged plugin", mlog.String("path", psPath.path), mlog.Err(err)) return @@ -1090,7 +990,7 @@ func (s *PluginService) processPrepackagedPlugins(pluginsDir string) []*plugin.P // processPrepackagedPlugin will return the prepackaged plugin metadata and will also // install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true. -func (s *PluginService) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) { +func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) { mlog.Debug("Processing prepackaged plugin", mlog.String("path", pluginPath.path)) fileReader, err := os.Open(pluginPath.path) @@ -1111,18 +1011,18 @@ func (s *PluginService) processPrepackagedPlugin(pluginPath *pluginSignaturePath } // Skip installing the plugin at all if automatic prepackaged plugins is disabled - if !*s.platform.Config().PluginSettings.AutomaticPrepackagedPlugins { + if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins { return plugin, nil } // Skip installing if the plugin is has not been previously enabled. - pluginState := s.platform.Config().PluginSettings.PluginStates[plugin.Manifest.Id] + pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[plugin.Manifest.Id] if pluginState == nil || !pluginState.Enable { return plugin, nil } mlog.Debug("Installing prepackaged plugin", mlog.String("path", pluginPath.path)) - if _, err := s.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil { + if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil { return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.path) } @@ -1130,24 +1030,24 @@ func (s *PluginService) processPrepackagedPlugin(pluginPath *pluginSignaturePath } // installFeatureFlagPlugins handles the automatic installation/upgrade of plugins from feature flags -func (s *PluginService) installFeatureFlagPlugins() { - ffControledPlugins := s.platform.Config().FeatureFlags.Plugins() +func (ch *Channels) installFeatureFlagPlugins() { + ffControledPlugins := ch.cfgSvc.Config().FeatureFlags.Plugins() // Respect the automatic prepackaged disable setting - if !*s.platform.Config().PluginSettings.AutomaticPrepackagedPlugins { + if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins { return } for pluginID, version := range ffControledPlugins { // Skip installing if the plugin has been previously disabled. - pluginState := s.platform.Config().PluginSettings.PluginStates[pluginID] + pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[pluginID] if pluginState != nil && !pluginState.Enable { - s.platform.Log().Debug("Not auto installing/upgrade because plugin was disabled", mlog.String("plugin_id", pluginID), mlog.String("version", version)) + ch.srv.Log().Debug("Not auto installing/upgrade because plugin was disabled", mlog.String("plugin_id", pluginID), mlog.String("version", version)) continue } // Check if we already installed this version as InstallMarketplacePlugin can't handle re-installs well. - pluginStatus, err := s.GetPluginStatus(pluginID) + pluginStatus, err := ch.GetPluginStatus(pluginID) pluginExists := err == nil if pluginExists && pluginStatus.Version == version { continue @@ -1155,37 +1055,37 @@ func (s *PluginService) installFeatureFlagPlugins() { if version != "" && version != "control" { // If we are on-prem skip installation if this is a downgrade - license := s.platform.License() + license := ch.srv.License() inCloud := license != nil && *license.Features.Cloud if !inCloud && pluginExists { parsedVersion, err := semver.Parse(version) if err != nil { - s.platform.Log().Debug("Bad version from feature flag", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) + ch.srv.Log().Debug("Bad version from feature flag", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) return } parsedExistingVersion, err := semver.Parse(pluginStatus.Version) if err != nil { - s.platform.Log().Debug("Bad version from plugin manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version)) + ch.srv.Log().Debug("Bad version from plugin manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version)) return } if parsedVersion.LTE(parsedExistingVersion) { - s.platform.Log().Debug("Skip installation because given version was a downgrade and on-prem installations should not downgrade.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version)) + ch.srv.Log().Debug("Skip installation because given version was a downgrade and on-prem installations should not downgrade.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version)) return } } - _, err := s.InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{ + _, err := ch.InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{ Id: pluginID, Version: version, }) if err != nil { - s.platform.Log().Debug("Unable to install plugin from FF manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) + ch.srv.Log().Debug("Unable to install plugin from FF manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) } else { - if err := s.enablePlugin(pluginID); err != nil { - s.platform.Log().Debug("Unable to enable plugin installed from feature flag.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) + if err := ch.enablePlugin(pluginID); err != nil { + ch.srv.Log().Debug("Unable to enable plugin installed from feature flag.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version)) } else { - s.platform.Log().Debug("Installed and enabled plugin.", mlog.String("plugin_id", pluginID), mlog.String("version", version)) + ch.srv.Log().Debug("Installed and enabled plugin.", mlog.String("plugin_id", pluginID), mlog.String("version", version)) } } } @@ -1240,15 +1140,15 @@ func getIcon(iconPath string) (string, error) { return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil } -func (s *PluginService) getPluginStateOverride(pluginID string) (bool, bool) { +func (ch *Channels) getPluginStateOverride(pluginID string) (bool, bool) { switch pluginID { case model.PluginIdApps: // Tie Apps proxy disabled status to the feature flag. - if !s.platform.Config().FeatureFlags.AppsEnabled { + if !ch.cfgSvc.Config().FeatureFlags.AppsEnabled { return true, false } case model.PluginIdCalls: - if !s.platform.Config().FeatureFlags.CallsEnabled { + if !ch.cfgSvc.Config().FeatureFlags.CallsEnabled { return true, false } } diff --git a/app/plugin_api.go b/app/plugin_api.go index 57ee5b4192..5c8ee9fad1 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -886,7 +886,7 @@ func (api *PluginAPI) DisablePlugin(id string) *model.AppError { } func (api *PluginAPI) RemovePlugin(id string) *model.AppError { - return api.app.Srv().pluginService.RemovePlugin(id) + return api.app.Channels().RemovePlugin(id) } func (api *PluginAPI) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { @@ -1235,7 +1235,7 @@ func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) { // RegisterCollectionAndTopic informs the server that this plugin handles // the given collection and topic types. func (api *PluginAPI) RegisterCollectionAndTopic(collectionType, topicType string) error { - return api.app.Srv().pluginService.registerCollectionAndTopic(api.id, collectionType, topicType) + return api.app.registerCollectionAndTopic(api.id, collectionType, topicType) } func (api *PluginAPI) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) { diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index cec5736eb3..759613781b 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -92,7 +92,7 @@ func setupMultiPluginAPITest(t *testing.T, pluginCodes []string, pluginManifests return app.NewPluginAPI(c, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv().Platform()), pluginDir, webappPluginDir, false, app.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) require.Equal(t, len(pluginCodes), len(pluginIDs)) @@ -119,7 +119,7 @@ func setupMultiPluginAPITest(t *testing.T, pluginCodes []string, pluginManifests }) } - app.PluginService().SetPluginsEnvironment(env) + app.ch.SetPluginsEnvironment(env) return pluginDir } @@ -849,7 +849,7 @@ func TestPluginAPIGetPlugins(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), nil) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) pluginIDs := []string{"pluginid1", "pluginid2", "pluginid3"} @@ -866,7 +866,7 @@ func TestPluginAPIGetPlugins(t *testing.T) { require.True(t, activated) pluginManifests = append(pluginManifests, manifest) } - th.App.PluginService().SetPluginsEnvironment(env) + th.App.ch.SetPluginsEnvironment(env) // Deactivate the last one for testing success := env.Deactivate(pluginIDs[len(pluginIDs)-1]) @@ -937,10 +937,10 @@ func TestInstallPlugin(t *testing.T) { return app.NewPluginAPI(c, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv().Platform()), pluginDir, webappPluginDir, false, app.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) - app.PluginService().SetPluginsEnvironment(env) + app.ch.SetPluginsEnvironment(env) backend := filepath.Join(pluginDir, pluginID, "backend.exe") utils.CompileGo(t, pluginCode, backend) @@ -1632,10 +1632,10 @@ func TestAPIMetrics(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) require.NoError(t, err) - th.App.PluginService().SetPluginsEnvironment(env) + th.App.ch.SetPluginsEnvironment(env) pluginID := model.NewId() backend := filepath.Join(pluginDir, pluginID, "backend.exe") @@ -2079,10 +2079,10 @@ func TestRegisterCollectionAndTopic(t *testing.T) { return th.App.NewPluginAPI(th.Context, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) - th.App.PluginService().SetPluginsEnvironment(env) + th.App.ch.SetPluginsEnvironment(env) pluginID := "testplugin" pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}` @@ -2179,10 +2179,10 @@ func TestPluginUploadsAPI(t *testing.T) { newPluginAPI := func(manifest *model.Manifest) plugin.API { return th.App.NewPluginAPI(th.Context, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) - th.App.PluginService().SetPluginsEnvironment(env) + th.App.ch.SetPluginsEnvironment(env) pluginID := "testplugin" pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}` diff --git a/app/plugin_commands.go b/app/plugin_commands.go index 8d034931e9..e70c69a38a 100644 --- a/app/plugin_commands.go +++ b/app/plugin_commands.go @@ -22,10 +22,6 @@ type PluginCommand struct { } func (a *App) RegisterPluginCommand(pluginID string, command *model.Command) error { - return a.Srv().pluginService.registerPluginCommand(pluginID, command) -} - -func (s *PluginService) registerPluginCommand(pluginID string, command *model.Command) error { if command.Trigger == "" { return errors.New("invalid command") } @@ -59,10 +55,10 @@ func (s *PluginService) registerPluginCommand(pluginID string, command *model.Co AutocompleteIconData: command.AutocompleteIconData, } - s.pluginCommandsLock.Lock() - defer s.pluginCommandsLock.Unlock() + a.ch.pluginCommandsLock.Lock() + defer a.ch.pluginCommandsLock.Unlock() - for _, pc := range s.pluginCommands { + for _, pc := range a.ch.pluginCommands { if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId { if pc.PluginId == pluginID { pc.Command = command @@ -71,7 +67,7 @@ func (s *PluginService) registerPluginCommand(pluginID string, command *model.Co } } - s.pluginCommands = append(s.pluginCommands, &PluginCommand{ + a.ch.pluginCommands = append(a.ch.pluginCommands, &PluginCommand{ Command: command, PluginId: pluginID, }) @@ -79,47 +75,39 @@ func (s *PluginService) registerPluginCommand(pluginID string, command *model.Co } func (a *App) UnregisterPluginCommand(pluginID, teamID, trigger string) { - a.Srv().pluginService.unregisterPluginCommand(pluginID, teamID, trigger) -} - -func (s *PluginService) unregisterPluginCommand(pluginID, teamID, trigger string) { trigger = strings.ToLower(trigger) - s.pluginCommandsLock.Lock() - defer s.pluginCommandsLock.Unlock() + a.ch.pluginCommandsLock.Lock() + defer a.ch.pluginCommandsLock.Unlock() var remaining []*PluginCommand - for _, pc := range s.pluginCommands { + for _, pc := range a.ch.pluginCommands { if pc.Command.TeamId != teamID || pc.Command.Trigger != trigger { remaining = append(remaining, pc) } } - s.pluginCommands = remaining + a.ch.pluginCommands = remaining } -func (s *PluginService) unregisterPluginCommands(pluginID string) { - s.pluginCommandsLock.Lock() - defer s.pluginCommandsLock.Unlock() +func (ch *Channels) unregisterPluginCommands(pluginID string) { + ch.pluginCommandsLock.Lock() + defer ch.pluginCommandsLock.Unlock() var remaining []*PluginCommand - for _, pc := range s.pluginCommands { + for _, pc := range ch.pluginCommands { if pc.PluginId != pluginID { remaining = append(remaining, pc) } } - s.pluginCommands = remaining + ch.pluginCommands = remaining } func (a *App) PluginCommandsForTeam(teamID string) []*model.Command { - return a.Srv().pluginService.PluginCommandsForTeam(teamID) -} - -func (s *PluginService) PluginCommandsForTeam(teamID string) []*model.Command { - s.pluginCommandsLock.RLock() - defer s.pluginCommandsLock.RUnlock() + a.ch.pluginCommandsLock.RLock() + defer a.ch.pluginCommandsLock.RUnlock() var commands []*model.Command - for _, pc := range s.pluginCommands { + for _, pc := range a.ch.pluginCommands { if pc.Command.TeamId == "" || pc.Command.TeamId == teamID { commands = append(commands, pc.Command) } @@ -127,24 +115,6 @@ func (s *PluginService) PluginCommandsForTeam(teamID string) []*model.Command { return commands } -func (s *PluginService) getPluginCommandFromArgs(args *model.CommandArgs) *PluginCommand { - parts := strings.Split(args.Command, " ") - trigger := parts[0][1:] - trigger = strings.ToLower(trigger) - - var matched *PluginCommand - s.pluginCommandsLock.RLock() - for _, pc := range s.pluginCommands { - if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger { - matched = pc - break - } - } - s.pluginCommandsLock.RUnlock() - - return matched -} - // tryExecutePluginCommand attempts to run a command provided by a plugin based on the given arguments. If no such // command can be found, returns nil for all arguments. func (a *App) tryExecutePluginCommand(c request.CTX, args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) { @@ -152,7 +122,15 @@ func (a *App) tryExecutePluginCommand(c request.CTX, args *model.CommandArgs) (* trigger := parts[0][1:] trigger = strings.ToLower(trigger) - matched := a.Srv().pluginService.getPluginCommandFromArgs(args) + var matched *PluginCommand + a.ch.pluginCommandsLock.RLock() + for _, pc := range a.ch.pluginCommands { + if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger { + matched = pc + break + } + } + a.ch.pluginCommandsLock.RUnlock() if matched == nil { return nil, nil, nil } diff --git a/app/plugin_commands_test.go b/app/plugin_commands_test.go index 1cf9751d9a..e56cf74718 100644 --- a/app/plugin_commands_test.go +++ b/app/plugin_commands_test.go @@ -106,7 +106,7 @@ func TestPluginCommand(t *testing.T) { require.NotEqual(t, "plugin", commands.Trigger) } - th.App.PluginService().RemovePlugin(pluginIDs[0]) + th.App.ch.RemovePlugin(pluginIDs[0]) }) t.Run("re-entrant command registration on config change", func(t *testing.T) { @@ -207,7 +207,7 @@ func TestPluginCommand(t *testing.T) { killed = true } - th.App.PluginService().RemovePlugin(pluginIDs[0]) + th.App.ch.RemovePlugin(pluginIDs[0]) require.False(t, killed, "execute command appears to have deadlocked") }) @@ -285,7 +285,7 @@ func TestPluginCommand(t *testing.T) { require.Equal(t, model.CommandResponseTypeEphemeral, resp.ResponseType) require.Equal(t, "text", resp.Text) - th.App.PluginService().RemovePlugin(pluginIDs[0]) + th.App.ch.RemovePlugin(pluginIDs[0]) }) t.Run("plugin has crashed before execution of command", func(t *testing.T) { tearDown, pluginIDs, activationErrors := SetAppEnvironmentWithPlugins(t, []string{` @@ -329,7 +329,7 @@ func TestPluginCommand(t *testing.T) { require.Nil(t, resp) require.NotNil(t, err) require.Equal(t, err.Id, "model.plugin_command_error.error.app_error") - th.App.PluginService().RemovePlugin(pluginIDs[0]) + th.App.ch.RemovePlugin(pluginIDs[0]) }) t.Run("plugin has crashed due to the execution of the command", func(t *testing.T) { @@ -374,7 +374,7 @@ func TestPluginCommand(t *testing.T) { require.Nil(t, resp) require.NotNil(t, err) require.Equal(t, err.Id, "model.plugin_command_crash.error.app_error") - th.App.PluginService().RemovePlugin(pluginIDs[0]) + th.App.ch.RemovePlugin(pluginIDs[0]) }) t.Run("plugin returning status code 0", func(t *testing.T) { diff --git a/app/plugin_db_driver.go b/app/plugin_db_driver.go index 0b74577f36..a29fa7467f 100644 --- a/app/plugin_db_driver.go +++ b/app/plugin_db_driver.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" ) @@ -20,7 +19,7 @@ import ( // a new entry tracked centrally in a map. Further requests operate on the // object ID. type DriverImpl struct { - ps *platform.PlatformService + s *Server connMut sync.RWMutex connMap map[string]*sql.Conn txMut sync.Mutex @@ -31,9 +30,9 @@ type DriverImpl struct { rowsMap map[string]driver.Rows } -func NewDriverImpl(s *platform.PlatformService) *DriverImpl { +func NewDriverImpl(s *Server) *DriverImpl { return &DriverImpl{ - ps: s, + s: s, connMap: make(map[string]*sql.Conn), txMap: make(map[string]driver.Tx), stMap: make(map[string]driver.Stmt), @@ -42,11 +41,11 @@ func NewDriverImpl(s *platform.PlatformService) *DriverImpl { } func (d *DriverImpl) Conn(isMaster bool) (string, error) { - dbFunc := d.ps.Store.GetInternalMasterDB + dbFunc := d.s.Platform().Store.GetInternalMasterDB if !isMaster { - dbFunc = d.ps.Store.GetInternalReplicaDB + dbFunc = d.s.Platform().Store.GetInternalReplicaDB } - timeout := time.Duration(*d.ps.Config().SqlSettings.QueryTimeout) * time.Second + timeout := time.Duration(*d.s.Config().SqlSettings.QueryTimeout) * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() conn, err := dbFunc().Conn(ctx) diff --git a/app/plugin_db_driver_test.go b/app/plugin_db_driver_test.go index 797f677d59..a2c428fb6f 100644 --- a/app/plugin_db_driver_test.go +++ b/app/plugin_db_driver_test.go @@ -15,7 +15,7 @@ func TestConnCreateTimeout(t *testing.T) { *th.App.Config().SqlSettings.QueryTimeout = 0 - d := NewDriverImpl(th.Server.platform) + d := NewDriverImpl(th.Server) _, err := d.Conn(true) require.Error(t, err) } diff --git a/app/plugin_event.go b/app/plugin_event.go index 19d9f1dabc..c30e2d1af5 100644 --- a/app/plugin_event.go +++ b/app/plugin_event.go @@ -9,10 +9,10 @@ import ( "github.com/mattermost/mattermost-server/v6/model" ) -func (s *PluginService) notifyClusterPluginEvent(event model.ClusterEvent, data model.PluginEventData) { +func (ch *Channels) notifyClusterPluginEvent(event model.ClusterEvent, data model.PluginEventData) { buf, _ := json.Marshal(data) - if s.platform.Cluster() != nil { - s.platform.Cluster().SendClusterMessage(&model.ClusterMessage{ + if ch.srv.platform.Cluster() != nil { + ch.srv.platform.Cluster().SendClusterMessage(&model.ClusterMessage{ Event: event, SendType: model.ClusterSendReliable, WaitForAllToSend: true, diff --git a/app/plugin_hooks_test.go b/app/plugin_hooks_test.go index 105982a7b2..161994a1ab 100644 --- a/app/plugin_hooks_test.go +++ b/app/plugin_hooks_test.go @@ -33,10 +33,10 @@ func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, a webappPluginDir, err := os.MkdirTemp("", "") require.NoError(t, err) - env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv().Platform()), pluginDir, webappPluginDir, false, app.Log(), nil) + env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) - app.PluginService().SetPluginsEnvironment(env) + app.ch.SetPluginsEnvironment(env) pluginIDs := []string{} activationErrors := []error{} for _, code := range pluginCode { @@ -1030,10 +1030,10 @@ func TestHookMetrics(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) require.NoError(t, err) - th.App.PluginService().SetPluginsEnvironment(env) + th.App.ch.SetPluginsEnvironment(env) pluginID := model.NewId() backend := filepath.Join(pluginDir, pluginID, "backend.exe") @@ -1234,7 +1234,7 @@ func TestHookRunDataRetention(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { n, _ := hooks.RunDataRetention(0, 0) // Ensure return it correct assert.Equal(t, int64(100), n) @@ -1278,7 +1278,7 @@ func TestHookOnSendDailyTelemetry(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.OnSendDailyTelemetry() hookCalled = true @@ -1322,7 +1322,7 @@ func TestHookOnCloudLimitsUpdated(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.OnCloudLimitsUpdated(nil) hookCalled = true diff --git a/app/plugin_install.go b/app/plugin_install.go index f0c54a0da4..431c3abebe 100644 --- a/app/plugin_install.go +++ b/app/plugin_install.go @@ -58,10 +58,10 @@ const managedPluginFileName = ".filestore" // fileStorePluginFolder is the folder name in the file store of the plugin bundles installed. const fileStorePluginFolder = "plugins" -func (s *PluginService) installPluginFromData(data model.PluginEventData) { +func (ch *Channels) installPluginFromData(data model.PluginEventData) { mlog.Debug("Installing plugin as per cluster message", mlog.String("plugin_id", data.Id)) - pluginSignaturePathMap, appErr := s.getPluginsFromFolder() + pluginSignaturePathMap, appErr := ch.getPluginsFromFolder() if appErr != nil { mlog.Error("Failed to get plugin signatures from filestore. Can't install plugin from data.", mlog.Err(appErr)) return @@ -72,53 +72,53 @@ func (s *PluginService) installPluginFromData(data model.PluginEventData) { return } - reader, err := s.fileStore.Reader(plugin.path) - if err != nil { - mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(err)) + reader, appErr := ch.srv.fileReader(plugin.path) + if appErr != nil { + mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr)) return } defer reader.Close() var signature filestore.ReadCloseSeeker - if *s.platform.Config().PluginSettings.RequirePluginSignature { - signature, err = s.fileStore.Reader(plugin.signaturePath) - if err != nil { - mlog.Error("Failed to open plugin signature from file store.", mlog.Err(err)) + if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature { + signature, appErr = ch.srv.fileReader(plugin.signaturePath) + if appErr != nil { + mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr)) return } defer signature.Close() } - manifest, appErr := s.installPluginLocally(reader, signature, installPluginLocallyAlways) + manifest, appErr := ch.installPluginLocally(reader, signature, installPluginLocallyAlways) if appErr != nil { mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr)) return } - if err2 := s.notifyPluginEnabled(manifest); err2 != nil { - mlog.Error("Failed notify plugin enabled", mlog.Err(err2)) + if err := ch.notifyPluginEnabled(manifest); err != nil { + mlog.Error("Failed notify plugin enabled", mlog.Err(err)) } - if err2 := s.notifyPluginStatusesChanged(); err2 != nil { - mlog.Error("Failed to notify plugin status changed", mlog.Err(err2)) + if err := ch.notifyPluginStatusesChanged(); err != nil { + mlog.Error("Failed to notify plugin status changed", mlog.Err(err)) } } -func (s *PluginService) removePluginFromData(data model.PluginEventData) { +func (ch *Channels) removePluginFromData(data model.PluginEventData) { mlog.Debug("Removing plugin as per cluster message", mlog.String("plugin_id", data.Id)) - if err := s.removePluginLocally(data.Id); err != nil { + if err := ch.removePluginLocally(data.Id); err != nil { mlog.Warn("Failed to remove plugin locally", mlog.Err(err), mlog.String("id", data.Id)) } - if err := s.notifyPluginStatusesChanged(); err != nil { + if err := ch.notifyPluginStatusesChanged(); err != nil { mlog.Warn("failed to notify plugin status changed", mlog.Err(err)) } } // InstallPluginWithSignature verifies and installs plugin. -func (s *PluginService) installPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) { - return s.installPlugin(pluginFile, signature, installPluginLocallyAlways) +func (ch *Channels) installPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) { + return ch.installPlugin(pluginFile, signature, installPluginLocallyAlways) } // InstallPlugin unpacks and installs a plugin but does not enable or activate it. @@ -132,40 +132,40 @@ func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Mani } func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - return a.ch.srv.pluginService.installPlugin(pluginFile, signature, installationStrategy) + return a.ch.installPlugin(pluginFile, signature, installationStrategy) } -func (s *PluginService) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - manifest, appErr := s.installPluginLocally(pluginFile, signature, installationStrategy) +func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { + manifest, appErr := ch.installPluginLocally(pluginFile, signature, installationStrategy) if appErr != nil { return nil, appErr } if signature != nil { signature.Seek(0, 0) - if _, err := s.fileStore.WriteFile(signature, getSignatureStorePath(manifest.Id)); err != nil { - return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + if _, appErr = ch.srv.writeFile(signature, getSignatureStorePath(manifest.Id)); appErr != nil { + return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) } } // Store bundle in the file store to allow access from other servers. pluginFile.Seek(0, 0) - if _, appErr := s.fileStore.WriteFile(pluginFile, getBundleStorePath(manifest.Id)); appErr != nil { + if _, appErr := ch.srv.writeFile(pluginFile, getBundleStorePath(manifest.Id)); appErr != nil { return nil, model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) } - s.notifyClusterPluginEvent( + ch.notifyClusterPluginEvent( model.ClusterEventInstallPlugin, model.PluginEventData{ Id: manifest.Id, }, ) - if err := s.notifyPluginEnabled(manifest); err != nil { + if err := ch.notifyPluginEnabled(manifest); err != nil { mlog.Warn("Failed notify plugin enabled", mlog.Err(err)) } - if err := s.notifyPluginStatusesChanged(); err != nil { + if err := ch.notifyPluginStatusesChanged(); err != nil { mlog.Warn("Failed to notify plugin status changed", mlog.Err(err)) } @@ -174,10 +174,10 @@ func (s *PluginService) installPlugin(pluginFile, signature io.ReadSeeker, insta // InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle // from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true. -func (s *PluginService) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) { +func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) { var pluginFile, signatureFile io.ReadSeeker - prepackagedPlugin, appErr := s.getPrepackagedPlugin(request.Id, request.Version) + prepackagedPlugin, appErr := ch.getPrepackagedPlugin(request.Id, request.Version) if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" { return nil, appErr } @@ -192,9 +192,9 @@ func (s *PluginService) InstallMarketplacePlugin(request *model.InstallMarketpla signatureFile = bytes.NewReader(prepackagedPlugin.Signature) } - if *s.platform.Config().PluginSettings.EnableRemoteMarketplace { + if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace { var plugin *model.BaseMarketplacePlugin - plugin, appErr = s.getRemoteMarketplacePlugin(request.Id, request.Version) + plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version) if appErr != nil { return nil, appErr } @@ -214,7 +214,7 @@ func (s *PluginService) InstallMarketplacePlugin(request *model.InstallMarketpla } if prepackagedVersion.LT(marketplaceVersion) { // Always true if no prepackaged plugin was found - downloadedPluginBytes, err := s.downloadFromURL(plugin.DownloadURL) + downloadedPluginBytes, err := ch.srv.downloadFromURL(plugin.DownloadURL) if err != nil { return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -234,7 +234,7 @@ func (s *PluginService) InstallMarketplacePlugin(request *model.InstallMarketpla return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError) } - manifest, appErr := s.installPluginWithSignature(pluginFile, signatureFile) + manifest, appErr := ch.installPluginWithSignature(pluginFile, signatureFile) if appErr != nil { return nil, appErr } @@ -253,15 +253,15 @@ const ( installPluginLocallyAlways ) -func (s *PluginService) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - pluginsEnvironment := s.GetPluginsEnvironment() +func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } // verify signature if signature != nil { - if err := s.verifyPlugin(pluginFile, signature); err != nil { + if err := ch.verifyPlugin(pluginFile, signature); err != nil { return nil, err } } @@ -277,7 +277,7 @@ func (s *PluginService) installPluginLocally(pluginFile, signature io.ReadSeeker return nil, appErr } - manifest, appErr = s.installExtractedPlugin(manifest, pluginDir, installationStrategy) + manifest, appErr = ch.installExtractedPlugin(manifest, pluginDir, installationStrategy) if appErr != nil { return nil, appErr } @@ -312,8 +312,8 @@ func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest return manifest, extractDir, nil } -func (s *PluginService) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - pluginsEnvironment := s.GetPluginsEnvironment() +func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -360,12 +360,12 @@ func (s *PluginService) installExtractedPlugin(manifest *model.Manifest, fromPlu // Otherwise remove the existing installation prior to install below. mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version)) - if err := s.removePluginLocally(existingManifest.Id); err != nil { + if err := ch.removePluginLocally(existingManifest.Id); err != nil { return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest) } } - pluginPath := filepath.Join(*s.platform.Config().PluginSettings.Directory, manifest.Id) + pluginPath := filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, manifest.Id) err = utils.CopyDir(fromPluginDir, pluginPath) if err != nil { return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -387,9 +387,9 @@ func (s *PluginService) installExtractedPlugin(manifest *model.Manifest, fromPlu } // Activate the plugin if enabled. - pluginState := s.platform.Config().PluginSettings.PluginStates[manifest.Id] + pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[manifest.Id] if pluginState != nil && pluginState.Enable { - if hasOverride, enabled := s.getPluginStateOverride(manifest.Id); hasOverride && !enabled { + if hasOverride, enabled := ch.getPluginStateOverride(manifest.Id); hasOverride && !enabled { return manifest, nil } @@ -405,49 +405,49 @@ func (s *PluginService) installExtractedPlugin(manifest *model.Manifest, fromPlu return manifest, nil } -func (s *PluginService) RemovePlugin(id string) *model.AppError { +func (ch *Channels) RemovePlugin(id string) *model.AppError { // Disable plugin before removal to make sure this // plugin remains disabled on re-install. - if err := s.disablePlugin(id); err != nil { + if err := ch.disablePlugin(id); err != nil { return err } - if err := s.removePluginLocally(id); err != nil { + if err := ch.removePluginLocally(id); err != nil { return err } // Remove bundle from the file store. storePluginFileName := getBundleStorePath(id) - bundleExist, err := s.fileStore.FileExists(storePluginFileName) + bundleExist, err := ch.srv.fileExists(storePluginFileName) if err != nil { return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if !bundleExist { return nil } - if err = s.fileStore.RemoveFile(storePluginFileName); err != nil { + if err = ch.srv.removeFile(storePluginFileName); err != nil { return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - if err2 := s.removeSignature(id); err2 != nil { - mlog.Warn("Can't remove signature", mlog.Err(err2)) + if err = ch.removeSignature(id); err != nil { + mlog.Warn("Can't remove signature", mlog.Err(err)) } - s.notifyClusterPluginEvent( + ch.notifyClusterPluginEvent( model.ClusterEventRemovePlugin, model.PluginEventData{ Id: id, }, ) - if err := s.notifyPluginStatusesChanged(); err != nil { + if err := ch.notifyPluginStatusesChanged(); err != nil { mlog.Warn("Failed to notify plugin status changed", mlog.Err(err)) } return nil } -func (s *PluginService) removePluginLocally(id string) *model.AppError { - pluginsEnvironment := s.GetPluginsEnvironment() +func (ch *Channels) removePluginLocally(id string) *model.AppError { + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -473,7 +473,7 @@ func (s *PluginService) removePluginLocally(id string) *model.AppError { pluginsEnvironment.Deactivate(id) pluginsEnvironment.RemovePlugin(id) - s.unregisterPluginCommands(id) + ch.unregisterPluginCommands(id) if err := os.RemoveAll(pluginPath); err != nil { return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -482,9 +482,9 @@ func (s *PluginService) removePluginLocally(id string) *model.AppError { return nil } -func (s *PluginService) removeSignature(pluginID string) *model.AppError { +func (ch *Channels) removeSignature(pluginID string) *model.AppError { filePath := getSignatureStorePath(pluginID) - exists, err := s.fileStore.FileExists(filePath) + exists, err := ch.srv.fileExists(filePath) if err != nil { return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -492,7 +492,7 @@ func (s *PluginService) removeSignature(pluginID string) *model.AppError { mlog.Debug("no plugin signature to remove", mlog.String("plugin_id", pluginID)) return nil } - if err = s.fileStore.RemoveFile(filePath); err != nil { + if err = ch.srv.removeFile(filePath); err != nil { return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil diff --git a/app/plugin_install_test.go b/app/plugin_install_test.go index ca15747865..a3eb6cfb2d 100644 --- a/app/plugin_install_test.go +++ b/app/plugin_install_test.go @@ -73,7 +73,7 @@ func TestInstallPluginLocally(t *testing.T) { th := Setup(t) defer th.TearDown() - actualManifest, appErr := th.App.PluginService().installPluginLocally(&nilReadSeeker{}, nil, installPluginLocallyOnlyIfNew) + actualManifest, appErr := th.App.ch.installPluginLocally(&nilReadSeeker{}, nil, installPluginLocallyOnlyIfNew) require.NotNil(t, appErr) assert.Equal(t, "app.plugin.extract.app_error", appErr.Id, appErr.Error()) require.Nil(t, actualManifest) @@ -87,7 +87,7 @@ func TestInstallPluginLocally(t *testing.T) { {"test", "test file"}, }) - actualManifest, appErr := th.App.PluginService().installPluginLocally(reader, nil, installPluginLocallyOnlyIfNew) + actualManifest, appErr := th.App.ch.installPluginLocally(reader, nil, installPluginLocallyOnlyIfNew) require.NotNil(t, appErr) assert.Equal(t, "app.plugin.manifest.app_error", appErr.Id, appErr.Error()) require.Nil(t, actualManifest) @@ -106,7 +106,7 @@ func TestInstallPluginLocally(t *testing.T) { {"plugin.json", string(manifestJSON)}, }) - actualManifest, appError := th.App.PluginService().installPluginLocally(reader, nil, installationStrategy) + actualManifest, appError := th.App.ch.installPluginLocally(reader, nil, installationStrategy) if actualManifest != nil { require.Equal(t, manifest, actualManifest) } @@ -134,7 +134,7 @@ func TestInstallPluginLocally(t *testing.T) { require.NoError(t, err) for _, bundleInfo := range bundleInfos { - err := th.App.PluginService().removePluginLocally(bundleInfo.Manifest.Id) + err := th.App.ch.removePluginLocally(bundleInfo.Manifest.Id) require.Nilf(t, err, "failed to remove existing plugin %s", bundleInfo.Manifest.Id) } } diff --git a/app/plugin_requests.go b/app/plugin_requests.go index 208adb11f4..1ccc966822 100644 --- a/app/plugin_requests.go +++ b/app/plugin_requests.go @@ -20,16 +20,16 @@ import ( "github.com/mattermost/mattermost-server/v6/utils" ) -func (s *PluginService) ServePluginRequest(w http.ResponseWriter, r *http.Request) { +func (ch *Channels) ServePluginRequest(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - if handler, ok := s.channels.routerSvc.getHandler(params["plugin_id"]); ok { - s.servePluginRequest(w, r, func(*plugin.Context, http.ResponseWriter, *http.Request) { + if handler, ok := ch.routerSvc.getHandler(params["plugin_id"]); ok { + ch.servePluginRequest(w, r, func(*plugin.Context, http.ResponseWriter, *http.Request) { handler.ServeHTTP(w, r) }) return } - pluginsEnvironment := s.GetPluginsEnvironment() + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented) mlog.Error(err.Error()) @@ -49,11 +49,11 @@ func (s *PluginService) ServePluginRequest(w http.ResponseWriter, r *http.Reques return } - s.servePluginRequest(w, r, hooks.ServeHTTP) + ch.servePluginRequest(w, r, hooks.ServeHTTP) } func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) { - pluginsEnvironment := a.ch.srv.pluginService.GetPluginsEnvironment() + pluginsEnvironment := a.ch.GetPluginsEnvironment() if pluginsEnvironment == nil { err := model.NewAppError("ServeInterPluginRequest", "app.plugin.disabled.app_error", nil, "Plugin environment not found.", http.StatusNotImplemented) a.Log().Error(err.Error()) @@ -87,7 +87,7 @@ func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, so // ServePluginPublicRequest serves public plugin files // at the URL http(s)://$SITE_URL/plugins/$PLUGIN_ID/public/{anything} -func (s *PluginService) ServePluginPublicRequest(w http.ResponseWriter, r *http.Request) { +func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "/") { http.NotFound(w, r) return @@ -97,7 +97,7 @@ func (s *PluginService) ServePluginPublicRequest(w http.ResponseWriter, r *http. vars := mux.Vars(r) pluginID := vars["plugin_id"] - pluginsEnv := s.GetPluginsEnvironment() + pluginsEnv := ch.GetPluginsEnvironment() // Check if someone has nullified the pluginsEnv in the meantime if pluginsEnv == nil { @@ -121,11 +121,11 @@ func (s *PluginService) ServePluginPublicRequest(w http.ResponseWriter, r *http. http.ServeFile(w, r, publicFile) } -func (s *PluginService) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) { +func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) { token := "" context := &plugin.Context{ RequestId: model.NewId(), - IPAddress: utils.GetIPAddress(r, s.platform.Config().ServiceSettings.TrustedProxyIPHeader), + IPAddress: utils.GetIPAddress(r, ch.cfgSvc.Config().ServiceSettings.TrustedProxyIPHeader), AcceptLanguage: r.Header.Get("Accept-Language"), UserAgent: r.UserAgent(), } @@ -148,8 +148,8 @@ func (s *PluginService) servePluginRequest(w http.ResponseWriter, r *http.Reques r.Header.Del("Mattermost-User-Id") if token != "" { - session, err := New(ServerConnector(s.channels)).GetSession(token) - defer s.platform.ReturnSessionToPool(session) + session, err := New(ServerConnector(ch)).GetSession(token) + defer ch.srv.platform.ReturnSessionToPool(session) csrfCheckPassed := false @@ -190,7 +190,7 @@ func (s *PluginService) servePluginRequest(w http.ResponseWriter, r *http.Reques mlog.String("user_id", userID), } - if *s.platform.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement { + if *ch.cfgSvc.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement { mlog.Warn(csrfErrorMessage, fields...) } else { mlog.Debug(csrfErrorMessage, fields...) @@ -219,7 +219,7 @@ func (s *PluginService) servePluginRequest(w http.ResponseWriter, r *http.Reques params := mux.Vars(r) - subpath, _ := utils.GetSubpathFromConfig(s.platform.Config()) + subpath, _ := utils.GetSubpathFromConfig(ch.cfgSvc.Config()) newQuery := r.URL.Query() newQuery.Del("access_token") diff --git a/app/plugin_requests_test.go b/app/plugin_requests_test.go index e457d8e5f1..c41c70be6d 100644 --- a/app/plugin_requests_test.go +++ b/app/plugin_requests_test.go @@ -24,7 +24,7 @@ func TestServePluginPublicRequest(t *testing.T) { require.NoError(t, err) rr := httptest.NewRecorder() - handler := http.HandlerFunc(th.App.PluginService().ServePluginPublicRequest) + handler := http.HandlerFunc(th.App.ch.ServePluginPublicRequest) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusNotFound, rr.Code) diff --git a/app/plugin_shutdown_test.go b/app/plugin_shutdown_test.go index 1c77fc3814..293d882f1f 100644 --- a/app/plugin_shutdown_test.go +++ b/app/plugin_shutdown_test.go @@ -63,7 +63,7 @@ func TestPluginShutdownTest(t *testing.T) { done := make(chan bool) go func() { defer close(done) - th.App.PluginService().ShutDownPlugins() + th.App.ch.ShutDownPlugins() }() select { diff --git a/app/plugin_signature.go b/app/plugin_signature.go index 928a9687b8..0903aa08fc 100644 --- a/app/plugin_signature.go +++ b/app/plugin_signature.go @@ -73,16 +73,16 @@ func (a *App) DeletePublicKey(name string) *model.AppError { // VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate. func (a *App) VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError { - return a.ch.srv.pluginService.verifyPlugin(plugin, signature) + return a.ch.verifyPlugin(plugin, signature) } -func (s *PluginService) verifyPlugin(plugin, signature io.ReadSeeker) *model.AppError { +func (ch *Channels) verifyPlugin(plugin, signature io.ReadSeeker) *model.AppError { if err := verifySignature(bytes.NewReader(mattermostPluginPublicKey), plugin, signature); err == nil { return nil } - publicKeys := s.platform.Config().PluginSettings.SignaturePublicKeyFiles + publicKeys := ch.cfgSvc.Config().PluginSettings.SignaturePublicKeyFiles for _, pk := range publicKeys { - pkBytes, appErr := s.platform.GetConfigFile(pk) + pkBytes, appErr := ch.srv.getPublicKey(pk) if appErr != nil { mlog.Warn("Unable to get public key for ", mlog.String("filename", pk)) continue diff --git a/app/plugin_statuses.go b/app/plugin_statuses.go index 2b27d7520c..399d58e5b2 100644 --- a/app/plugin_statuses.go +++ b/app/plugin_statuses.go @@ -10,8 +10,8 @@ import ( ) // GetPluginStatus returns the status for a plugin installed on this server. -func (s *PluginService) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { - pluginsEnvironment := s.GetPluginsEnvironment() +func (ch *Channels) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("GetPluginStatus", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -24,8 +24,8 @@ func (s *PluginService) GetPluginStatus(id string) (*model.PluginStatus, *model. for _, status := range pluginStatuses { if status.PluginId == id { // Add our cluster ID - if s.platform.Cluster() != nil { - status.ClusterId = s.platform.Cluster().GetClusterId() + if ch.srv.platform.Cluster() != nil { + status.ClusterId = ch.srv.platform.Cluster().GetClusterId() } return status, nil @@ -37,12 +37,12 @@ func (s *PluginService) GetPluginStatus(id string) (*model.PluginStatus, *model. // GetPluginStatus returns the status for a plugin installed on this server. func (a *App) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) { - return a.ch.srv.pluginService.GetPluginStatus(id) + return a.ch.GetPluginStatus(id) } // GetPluginStatuses returns the status for plugins installed on this server. -func (s *PluginService) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { - pluginsEnvironment := s.GetPluginsEnvironment() +func (ch *Channels) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } @@ -54,8 +54,8 @@ func (s *PluginService) GetPluginStatuses() (model.PluginStatuses, *model.AppErr // Add our cluster ID for _, status := range pluginStatuses { - if s.platform.Cluster() != nil { - status.ClusterId = s.platform.Cluster().GetClusterId() + if ch.srv.platform.Cluster() != nil { + status.ClusterId = ch.srv.platform.Cluster().GetClusterId() } else { status.ClusterId = "" } @@ -66,22 +66,22 @@ func (s *PluginService) GetPluginStatuses() (model.PluginStatuses, *model.AppErr // GetPluginStatuses returns the status for plugins installed on this server. func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { - return a.ch.srv.pluginService.GetPluginStatuses() + return a.ch.GetPluginStatuses() } // GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster. func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) { - return a.ch.srv.pluginService.getClusterPluginStatuses() + return a.ch.getClusterPluginStatuses() } -func (s *PluginService) getClusterPluginStatuses() (model.PluginStatuses, *model.AppError) { - pluginStatuses, err := s.GetPluginStatuses() +func (ch *Channels) getClusterPluginStatuses() (model.PluginStatuses, *model.AppError) { + pluginStatuses, err := ch.GetPluginStatuses() if err != nil { return nil, err } - if s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable { - clusterPluginStatuses, err := s.platform.Cluster().GetPluginStatuses() + if ch.srv.platform.Cluster() != nil && *ch.cfgSvc.Config().ClusterSettings.Enable { + clusterPluginStatuses, err := ch.srv.platform.Cluster().GetPluginStatuses() if err != nil { return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -92,8 +92,8 @@ func (s *PluginService) getClusterPluginStatuses() (model.PluginStatuses, *model return pluginStatuses, nil } -func (s *PluginService) notifyPluginStatusesChanged() error { - pluginStatuses, err := s.getClusterPluginStatuses() +func (ch *Channels) notifyPluginStatusesChanged() error { + pluginStatuses, err := ch.getClusterPluginStatuses() if err != nil { return err } @@ -102,7 +102,7 @@ func (s *PluginService) notifyPluginStatusesChanged() error { message := model.NewWebSocketEvent(model.WebsocketEventPluginStatusesChanged, "", "", "", nil, "") message.Add("plugin_statuses", pluginStatuses) message.GetBroadcast().ContainsSensitiveData = true - s.platform.Publish(message) + ch.srv.platform.Publish(message) return nil } diff --git a/app/plugin_test.go b/app/plugin_test.go index 0d3ec65431..57802c67ac 100644 --- a/app/plugin_test.go +++ b/app/plugin_test.go @@ -346,7 +346,7 @@ func TestServePluginRequest(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/plugins/foo/bar", nil) - th.App.PluginService().ServePluginRequest(w, r) + th.App.ch.ServePluginRequest(w, r) assert.Equal(t, http.StatusNotImplemented, w.Result().StatusCode) } @@ -390,7 +390,7 @@ func TestPrivateServePluginRequest(t *testing.T) { request = mux.SetURLVars(request, map[string]string{"plugin_id": "id"}) - th.App.PluginService().servePluginRequest(recorder, request, handler) + th.App.ch.servePluginRequest(recorder, request, handler) }) } @@ -413,7 +413,7 @@ func TestHandlePluginRequest(t *testing.T) { var assertions func(*http.Request) router := mux.NewRouter() router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}/{anything:.*}", func(_ http.ResponseWriter, r *http.Request) { - th.App.PluginService().servePluginRequest(nil, r, func(_ *plugin.Context, _ http.ResponseWriter, r *http.Request) { + th.App.ch.servePluginRequest(nil, r, func(_ *plugin.Context, _ http.ResponseWriter, r *http.Request) { assertions(r) }) }) @@ -625,7 +625,7 @@ func TestPluginSync(t *testing.T) { appErr = th.App.DeletePublicKey("pub_key") checkNoError(t, appErr) - appErr = th.App.PluginService().RemovePlugin("testplugin") + appErr = th.App.ch.RemovePlugin("testplugin") checkNoError(t, appErr) }) }) @@ -642,7 +642,7 @@ func TestChannelsPluginsInit(t *testing.T) { path, _ := fileutils.FindDir("tests") require.NotPanics(t, func() { - th.Server.pluginService.initPlugins(ctx, path, path) + th.Server.Channels().initPlugins(ctx, path, path) }) } @@ -763,7 +763,7 @@ func TestPluginPanicLogs(t *testing.T) { th.TestLogger.Flush() // We shutdown plugins first so that the read on the log buffer is race-free. - th.App.PluginService().ShutDownPlugins() + th.App.ch.ShutDownPlugins() tearDown() testlib.AssertLog(t, th.LogBuffer, mlog.LvlDebug.Name, "panic: some text from panic") @@ -831,7 +831,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { require.NoError(t, err) require.NotNil(t, pluginBytes) - manifest, appErr := th.App.PluginService().installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) + manifest, appErr := th.App.ch.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) require.Nil(t, appErr) require.Equal(t, "testplugin", manifest.Id) @@ -848,7 +848,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { *cfg.PluginSettings.EnableRemoteMarketplace = false }) - plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 1) require.Equal(t, plugins[0].Manifest.Id, "testplugin") require.Empty(t, plugins[0].Signature, 0) @@ -858,7 +858,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { require.Len(t, pluginStatus, 1) require.Equal(t, pluginStatus[0].PluginId, "testplugin") - appErr = th.App.PluginService().RemovePlugin("testplugin") + appErr = th.App.ch.RemovePlugin("testplugin") checkNoError(t, appErr) pluginStatus, err = env.Statuses() @@ -875,7 +875,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { env := th.App.GetPluginsEnvironment() - plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 1) require.Equal(t, plugins[0].Manifest.Id, "testplugin") require.Empty(t, plugins[0].Signature, 0) @@ -908,7 +908,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) require.NoError(t, err) - plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 2) require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) require.NotEmpty(t, plugins[0].Signature) @@ -939,7 +939,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { require.NoError(t, err) require.NotNil(t, pluginBytes) - manifest, appErr := th.App.PluginService().installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) + manifest, appErr := th.App.ch.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) require.Nil(t, appErr) require.Equal(t, "testplugin", manifest.Id) @@ -957,7 +957,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) require.NoError(t, err) - plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 2) require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) require.NotEmpty(t, plugins[0].Signature) @@ -969,7 +969,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { require.Len(t, pluginStatus, 1) require.Equal(t, pluginStatus[0].PluginId, "testplugin") - appErr = th.App.PluginService().RemovePlugin("testplugin") + appErr = th.App.ch.RemovePlugin("testplugin") checkNoError(t, appErr) pluginStatus, err = env.Statuses() @@ -994,7 +994,7 @@ func TestProcessPrepackagedPlugins(t *testing.T) { err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) require.NoError(t, err) - plugins := th.App.PluginService().processPrepackagedPlugins(prepackagedPluginsDir) + plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) require.Len(t, plugins, 2) require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) require.NotEmpty(t, plugins[0].Signature) @@ -1071,14 +1071,14 @@ func TestGetPluginStateOverride(t *testing.T) { defer th.TearDown() t.Run("no override", func(t *testing.T) { - overrides, value := th.App.PluginService().getPluginStateOverride("focalboard") + overrides, value := th.App.ch.getPluginStateOverride("focalboard") require.False(t, overrides) require.False(t, value) }) t.Run("calls override", func(t *testing.T) { t.Run("on-prem", func(t *testing.T) { - overrides, value := th.App.PluginService().getPluginStateOverride("com.mattermost.calls") + overrides, value := th.App.ch.getPluginStateOverride("com.mattermost.calls") require.False(t, overrides) require.False(t, value) }) @@ -1086,7 +1086,7 @@ func TestGetPluginStateOverride(t *testing.T) { t.Run("Cloud, without enabled flag", func(t *testing.T) { os.Setenv("MM_CLOUD_INSTALLATION_ID", "test") defer os.Unsetenv("MM_CLOUD_INSTALLATION_ID") - overrides, value := th.App.PluginService().getPluginStateOverride("com.mattermost.calls") + overrides, value := th.App.ch.getPluginStateOverride("com.mattermost.calls") require.False(t, overrides) require.False(t, value) }) @@ -1100,7 +1100,7 @@ func TestGetPluginStateOverride(t *testing.T) { th2 := Setup(t) defer th2.TearDown() - overrides, value := th2.App.PluginService().getPluginStateOverride("com.mattermost.calls") + overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.calls") require.False(t, overrides) require.False(t, value) }) @@ -1114,7 +1114,7 @@ func TestGetPluginStateOverride(t *testing.T) { th2 := Setup(t) defer th2.TearDown() - overrides, value := th2.App.PluginService().getPluginStateOverride("com.mattermost.calls") + overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.calls") require.True(t, overrides) require.False(t, value) }) @@ -1126,7 +1126,7 @@ func TestGetPluginStateOverride(t *testing.T) { th2 := Setup(t) defer th2.TearDown() - overrides, value := th2.App.PluginService().getPluginStateOverride("com.mattermost.calls") + overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.calls") require.True(t, overrides) require.False(t, value) }) @@ -1134,7 +1134,7 @@ func TestGetPluginStateOverride(t *testing.T) { t.Run("apps override", func(t *testing.T) { t.Run("without enabled flag", func(t *testing.T) { - overrides, value := th.App.PluginService().getPluginStateOverride("com.mattermost.apps") + overrides, value := th.App.ch.getPluginStateOverride("com.mattermost.apps") require.False(t, overrides) require.False(t, value) }) @@ -1146,7 +1146,7 @@ func TestGetPluginStateOverride(t *testing.T) { th2 := Setup(t) defer th2.TearDown() - overrides, value := th2.App.PluginService().getPluginStateOverride("com.mattermost.apps") + overrides, value := th2.App.ch.getPluginStateOverride("com.mattermost.apps") require.True(t, overrides) require.False(t, value) }) diff --git a/app/post.go b/app/post.go index b98cb13759..095494d080 100644 --- a/app/post.go +++ b/app/post.go @@ -269,7 +269,7 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel } var rejectionError *model.AppError pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { replacementPost, rejectionReason := hooks.MessageWillBePosted(pluginContext, post.ForPlugin()) if rejectionReason != "" { id := "Post rejected by plugin. " + rejectionReason @@ -328,7 +328,7 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel // and to remove the non-GOB-encodable Metadata from it. pluginPost := rpost.ForPlugin() a.Srv().Go(func() { - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.MessageHasBeenPosted(pluginContext, pluginPost) return true }, plugin.MessageHasBeenPostedID) @@ -655,7 +655,7 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) var rejectionReason string pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost.ForPlugin(), oldPost.ForPlugin()) return post != nil }, plugin.MessageWillBeUpdatedID) @@ -680,7 +680,7 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) pluginOldPost := oldPost.ForPlugin() pluginNewPost := newPost.ForPlugin() a.Srv().Go(func() { - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.MessageHasBeenUpdated(pluginContext, pluginNewPost, pluginOldPost) return true }, plugin.MessageHasBeenUpdatedID) diff --git a/app/reaction.go b/app/reaction.go index c79036b443..fc6d54699f 100644 --- a/app/reaction.go +++ b/app/reaction.go @@ -45,7 +45,7 @@ func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction) pluginContext := pluginContext(c) a.Srv().Go(func() { - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ReactionHasBeenAdded(pluginContext, reaction) return true }, plugin.ReactionHasBeenAddedID) @@ -142,7 +142,7 @@ func (a *App) DeleteReactionForPost(c *request.Context, reaction *model.Reaction pluginContext := pluginContext(c) a.Srv().Go(func() { - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ReactionHasBeenRemoved(pluginContext, reaction) return true }, plugin.ReactionHasBeenRemovedID) diff --git a/app/server.go b/app/server.go index 22f9d24c1c..fbd433a6db 100644 --- a/app/server.go +++ b/app/server.go @@ -119,7 +119,6 @@ type Server struct { telemetryService *telemetry.TelemetryService userService *users.UserService teamService *teams.TeamService - pluginService *PluginService serviceMux sync.RWMutex remoteClusterService remotecluster.RemoteClusterServiceIFace @@ -719,10 +718,6 @@ func (s *Server) Shutdown() { } } - // Stop the plugin service, we need to stop plugin service before stopping the - // product as products are being consumed by this service. - s.pluginService.ShutDownPlugins() - // Stop products. // This needs to happen last because products are dependent // on parent services. @@ -829,18 +824,11 @@ func stripPort(hostport string) string { func (s *Server) Start() error { // Start products. // This needs to happen before because products are dependent on the HTTP server. + // make sure channels starts first if err := s.products["channels"].Start(); err != nil { return errors.Wrap(err, "Unable to start channels") } - - // This should actually be started after products, but we have a product hooks - // dependency for now, once that get sorted out, this should be moved to the appropriate - // order. - if err := s.InitializePluginService(); err != nil { - return errors.Wrap(err, "Unable to start plugin service") - } - for name, product := range s.products { if name == "channels" { continue diff --git a/app/team.go b/app/team.go index 5724f683c2..222d10e442 100644 --- a/app/team.go +++ b/app/team.go @@ -853,7 +853,7 @@ func (a *App) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, a.Srv().Go(func() { pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedTeam(pluginContext, teamMember, actor) return true }, plugin.UserHasJoinedTeamID) @@ -1225,7 +1225,7 @@ func (a *App) postProcessTeamMemberLeave(c request.CTX, teamMember *model.TeamMe a.Srv().Go(func() { pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLeftTeam(pluginContext, teamMember, actor) return true }, plugin.UserHasLeftTeamID) diff --git a/app/upload.go b/app/upload.go index b60f9be57b..318e3ede89 100644 --- a/app/upload.go +++ b/app/upload.go @@ -62,7 +62,7 @@ func (a *App) runPluginsHook(c request.CTX, info *model.FileInfo, file io.Reader var rejErr *model.AppError var once sync.Once pluginContext := pluginContext(c) - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { once.Do(func() { hookHasRunCh <- struct{}{} }) diff --git a/app/user.go b/app/user.go index 81faa5b57d..6e12bda34b 100644 --- a/app/user.go +++ b/app/user.go @@ -310,7 +310,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m pluginContext := pluginContext(c) a.Srv().Go(func() { - a.Srv().RunMultiHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasBeenCreated(pluginContext, ruser) return true }, plugin.UserHasBeenCreatedID) diff --git a/app/web_conn.go b/app/web_conn.go index 1db5866e0a..cdf59eb31e 100644 --- a/app/web_conn.go +++ b/app/web_conn.go @@ -16,5 +16,5 @@ func (a *App) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfi // NewWebConn returns a new WebConn instance. func (a *App) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn { - return a.Srv().Platform().NewWebConn(cfg, a, a.Srv()) + return a.Srv().Platform().NewWebConn(cfg, a, a.ch) } diff --git a/cmd/mattermost/commands/init.go b/cmd/mattermost/commands/init.go index a90b4b02b3..e93d9640fe 100644 --- a/cmd/mattermost/commands/init.go +++ b/cmd/mattermost/commands/init.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/mattermost/mattermost-server/v6/app" + "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/i18n" @@ -20,11 +21,7 @@ func initDBCommandContextCobra(command *cobra.Command, readOnlyConfigStore bool) panic(err) } - err = a.Srv().InitializePluginService() - if err != nil { - return nil, err - } - + a.InitPlugins(request.EmptyContext(a.Log()), *a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory) a.DoAppMigrations() return a, nil diff --git a/web/web_test.go b/web/web_test.go index b57e3888bd..4214649031 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -282,7 +282,7 @@ func TestPublicFilesRequest(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, app.NewDriverImpl(th.Server.Platform()), pluginDir, webappPluginDir, false, th.App.Log(), nil) + env, err := plugin.NewEnvironment(th.NewPluginAPI, app.NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) pluginID := "com.mattermost.sample" @@ -329,7 +329,7 @@ func TestPublicFilesRequest(t *testing.T) { require.NotNil(t, manifest) require.True(t, activated) - th.App.PluginService().SetPluginsEnvironment(env) + th.App.Channels().SetPluginsEnvironment(env) req, _ := http.NewRequest("GET", "/plugins/com.mattermost.sample/public/hello.html", nil) res := httptest.NewRecorder()