diff --git a/.circleci/config.yml b/.circleci/config.yml index e10fa93097..c53ed45a72 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -140,7 +140,7 @@ jobs: command: | cd mattermost-server make modules-tidy - if [[ -n $(git status --porcelain) ]]; then echo "Please tidy up the Go modules using make modules-tidy"; exit 1; fi + if [[ -n $(git status --porcelain) ]]; then echo "Please tidy up the Go modules using make modules-tidy"; git diff; exit 1; fi check-store-layers: docker: - image: cimg/go:1.18 diff --git a/Makefile b/Makefile index d93117a46f..b2b1b32803 100644 --- a/Makefile +++ b/Makefile @@ -149,13 +149,13 @@ TEMPLATES_DIR=templates PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2 PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.2.2 PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0 -PLUGIN_PACKAGES += mattermost-plugin-calls-v0.11.0 +PLUGIN_PACKAGES += mattermost-plugin-calls-v0.12.0 PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0 PLUGIN_PACKAGES += mattermost-plugin-confluence-v1.3.0 PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.1 PLUGIN_PACKAGES += mattermost-plugin-github-v2.1.4 PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.5.2 -PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.34.0 +PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.35.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 @@ -163,8 +163,8 @@ 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.4 -PLUGIN_PACKAGES += mattermost-plugin-apps-v1.1.0 +PLUGIN_PACKAGES += focalboard-v7.7.0 +PLUGIN_PACKAGES += mattermost-plugin-apps-v1.2.0 # Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target ifeq ($(BUILD_ENTERPRISE_READY),true) diff --git a/api4/channel_category_test.go b/api4/channel_category_test.go index e58a557ee6..af3e9b2319 100644 --- a/api4/channel_category_test.go +++ b/api4/channel_category_test.go @@ -103,7 +103,6 @@ func TestCreateCategoryForTeamForUser(t *testing.T) { }) t.Run("should publish expected WS payload", func(t *testing.T) { - t.Skip("MM-42652") userWSClient, err := th.CreateWebSocketClient() require.NoError(t, err) defer userWSClient.Close() diff --git a/api4/license_test.go b/api4/license_test.go index d11fe1b150..fe4d62848e 100644 --- a/api4/license_test.go +++ b/api4/license_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/mattermost/mattermost-server/v6/app" "github.com/mattermost/mattermost-server/v6/app/platform" "github.com/mattermost/mattermost-server/v6/einterfaces/mocks" "github.com/mattermost/mattermost-server/v6/model" @@ -242,7 +241,6 @@ func TestRequestTrialLicense(t *testing.T) { }) t.Run("trial license user count less than current users", func(t *testing.T) { - t.Skip("MM-48416") nUsers := 1 license := model.NewTestLicense() license.Features.Users = model.NewInt(nUsers) @@ -268,9 +266,9 @@ func TestRequestTrialLicense(t *testing.T) { th.App.Srv().Platform().SetLicenseManager(licenseManagerMock) defer func(requestTrialURL string) { - app.RequestTrialURL = requestTrialURL - }(app.RequestTrialURL) - app.RequestTrialURL = testServer.URL + platform.RequestTrialURL = requestTrialURL + }(platform.RequestTrialURL) + platform.RequestTrialURL = testServer.URL resp, err := th.SystemAdminClient.RequestTrialLicense(nUsers) CheckErrorID(t, err, "api.license.add_license.unique_users.app_error") diff --git a/app/app_iface.go b/app/app_iface.go index 4989828611..023e929c84 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -435,7 +435,7 @@ type AppIface interface { BuildPostReactions(ctx request.CTX, postID string) (*[]ReactionImportData, *model.AppError) BuildPushNotificationMessage(c request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, explicitMention bool, channelWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError) BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError) - BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts model.BulkExportOpts) *model.AppError + BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) CanNotifyAdmin(trial bool) bool diff --git a/app/emoji.go b/app/emoji.go index 48e7773b5a..9982428711 100644 --- a/app/emoji.go +++ b/app/emoji.go @@ -14,7 +14,6 @@ import ( "image/draw" "image/gif" _ "image/jpeg" - "image/png" "io" "mime/multipart" "net/http" @@ -31,7 +30,7 @@ import ( ) const ( - MaxEmojiFileSize = 1 << 20 // 1 MB + MaxEmojiFileSize = 1 << 19 // 512 KiB MaxEmojiWidth = 128 MaxEmojiHeight = 128 MaxEmojiOriginalWidth = 1028 @@ -155,8 +154,8 @@ func (a *App) UploadEmojiImage(c request.CTX, id string, imageData *multipart.Fi return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "", http.StatusBadRequest).Wrap(err) } - resized_image := resizeEmoji(img, config.Width, config.Height) - if err := png.Encode(newbuf, resized_image); err != nil { + resizedImg := resizeEmoji(img, config.Width, config.Height) + if err := a.ch.imgEncoder.EncodePNG(newbuf, resizedImg); err != nil { return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest).Wrap(err) } buf = newbuf diff --git a/app/export.go b/app/export.go index 8e52fa74e7..ee427b6936 100644 --- a/app/export.go +++ b/app/export.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "path/filepath" + "strconv" "strings" "time" @@ -65,7 +66,7 @@ var exportablePreferences = map[imports.ComparablePreference]string{{ }: "EmailInterval", } -func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts model.BulkExportOpts) *model.AppError { +func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError { var zipWr *zip.Writer if opts.CreateArchive { var err error @@ -78,46 +79,50 @@ func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts } } + if job != nil && job.Data == nil { + job.Data = make(model.StringMap) + } + ctx.Logger().Info("Bulk export: exporting version") if err := a.exportVersion(writer); err != nil { return err } ctx.Logger().Info("Bulk export: exporting teams") - teamNames, err := a.exportAllTeams(writer) + teamNames, err := a.exportAllTeams(ctx, job, writer) if err != nil { return err } ctx.Logger().Info("Bulk export: exporting channels") - if err = a.exportAllChannels(writer, teamNames); err != nil { + if err = a.exportAllChannels(ctx, job, writer, teamNames); err != nil { return err } ctx.Logger().Info("Bulk export: exporting users") - if err = a.exportAllUsers(writer); err != nil { + if err = a.exportAllUsers(ctx, job, writer); err != nil { return err } ctx.Logger().Info("Bulk export: exporting posts") - attachments, err := a.exportAllPosts(ctx, writer, opts.IncludeAttachments) + attachments, err := a.exportAllPosts(ctx, job, writer, opts.IncludeAttachments) if err != nil { return err } ctx.Logger().Info("Bulk export: exporting emoji") - emojiPaths, err := a.exportCustomEmoji(ctx, writer, outPath, "exported_emoji", !opts.CreateArchive) + emojiPaths, err := a.exportCustomEmoji(ctx, job, writer, outPath, "exported_emoji", !opts.CreateArchive) if err != nil { return err } ctx.Logger().Info("Bulk export: exporting direct channels") - if err = a.exportAllDirectChannels(writer); err != nil { + if err = a.exportAllDirectChannels(ctx, job, writer); err != nil { return err } ctx.Logger().Info("Bulk export: exporting direct posts") - directAttachments, err := a.exportAllDirectPosts(ctx, writer, opts.IncludeAttachments) + directAttachments, err := a.exportAllDirectPosts(ctx, job, writer, opts.IncludeAttachments) if err != nil { return err } @@ -139,6 +144,8 @@ func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts return err } } + + updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "attachments_exported", len(attachments)+len(directAttachments)+len(emojiPaths)) } return nil @@ -175,9 +182,10 @@ func (a *App) exportVersion(writer io.Writer) *model.AppError { return a.exportWriteLine(writer, versionLine) } -func (a *App) exportAllTeams(writer io.Writer) (map[string]bool, *model.AppError) { +func (a *App) exportAllTeams(ctx request.CTX, job *model.Job, writer io.Writer) (map[string]bool, *model.AppError) { afterId := strings.Repeat("0", 26) teamNames := make(map[string]bool) + cnt := 0 for { teams, err := a.Srv().Store().Team().GetAllForExportAfter(1000, afterId) if err != nil { @@ -187,6 +195,8 @@ func (a *App) exportAllTeams(writer io.Writer) (map[string]bool, *model.AppError if len(teams) == 0 { break } + cnt += len(teams) + updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "teams_exported", cnt) for _, team := range teams { afterId = team.Id @@ -207,8 +217,9 @@ func (a *App) exportAllTeams(writer io.Writer) (map[string]bool, *model.AppError return teamNames, nil } -func (a *App) exportAllChannels(writer io.Writer, teamNames map[string]bool) *model.AppError { +func (a *App) exportAllChannels(ctx request.CTX, job *model.Job, writer io.Writer, teamNames map[string]bool) *model.AppError { afterId := strings.Repeat("0", 26) + cnt := 0 for { channels, err := a.Srv().Store().Channel().GetAllChannelsForExportAfter(1000, afterId) @@ -219,6 +230,8 @@ func (a *App) exportAllChannels(writer io.Writer, teamNames map[string]bool) *mo if len(channels) == 0 { break } + cnt += len(channels) + updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "channels_exported", cnt) for _, channel := range channels { afterId = channel.Id @@ -242,8 +255,9 @@ func (a *App) exportAllChannels(writer io.Writer, teamNames map[string]bool) *mo return nil } -func (a *App) exportAllUsers(writer io.Writer) *model.AppError { +func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError { afterId := strings.Repeat("0", 26) + cnt := 0 for { users, err := a.Srv().Store().User().GetAllAfter(1000, afterId) @@ -254,6 +268,8 @@ func (a *App) exportAllUsers(writer io.Writer) *model.AppError { if len(users) == 0 { break } + cnt += len(users) + updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "users_exported", cnt) for _, user := range users { afterId = user.Id @@ -395,12 +411,13 @@ func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *imports.UserNot } } -func (a *App) exportAllPosts(ctx request.CTX, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { +func (a *App) exportAllPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { var attachments []imports.AttachmentImportData afterId := strings.Repeat("0", 26) var postProcessCount uint64 logCheckpoint := time.Now() + cnt := 0 for { if time.Since(logCheckpoint) > 5*time.Minute { ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d posts", postProcessCount)) @@ -415,6 +432,8 @@ func (a *App) exportAllPosts(ctx request.CTX, writer io.Writer, withAttachments if len(posts) == 0 { return attachments, nil } + cnt += len(posts) + updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "posts_exported", cnt) for _, post := range posts { afterId = post.Id @@ -538,9 +557,10 @@ func (a *App) buildPostAttachments(postID string) ([]imports.AttachmentImportDat return attachments, nil } -func (a *App) exportCustomEmoji(c request.CTX, writer io.Writer, outPath, exportDir string, exportFiles bool) ([]string, *model.AppError) { +func (a *App) exportCustomEmoji(c request.CTX, job *model.Job, writer io.Writer, outPath, exportDir string, exportFiles bool) ([]string, *model.AppError) { var emojiPaths []string pageNumber := 0 + cnt := 0 for { customEmojiList, err := a.GetEmojiList(c, pageNumber, 100, model.EmojiSortByName) @@ -551,6 +571,8 @@ func (a *App) exportCustomEmoji(c request.CTX, writer io.Writer, outPath, export if len(customEmojiList) == 0 { break } + cnt += len(customEmojiList) + updateJobProgress(c.Logger(), a.Srv().Store(), job, "emojis_exported", cnt) pageNumber++ @@ -619,8 +641,9 @@ func (a *App) copyEmojiImages(emojiId string, emojiImagePath string, pathToDir s return nil } -func (a *App) exportAllDirectChannels(writer io.Writer) *model.AppError { +func (a *App) exportAllDirectChannels(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError { afterId := strings.Repeat("0", 26) + cnt := 0 for { channels, err := a.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, afterId) if err != nil { @@ -630,6 +653,8 @@ func (a *App) exportAllDirectChannels(writer io.Writer) *model.AppError { if len(channels) == 0 { break } + cnt += len(channels) + updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_channels_exported", cnt) for _, channel := range channels { afterId = channel.Id @@ -682,12 +707,13 @@ func (a *App) buildFavoritedByList(channelID string) ([]string, *model.AppError) return userIDs, nil } -func (a *App) exportAllDirectPosts(ctx request.CTX, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { +func (a *App) exportAllDirectPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { var attachments []imports.AttachmentImportData afterId := strings.Repeat("0", 26) var postProcessCount uint64 logCheckpoint := time.Now() + cnt := 0 for { if time.Since(logCheckpoint) > 5*time.Minute { ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d direct posts", postProcessCount)) @@ -702,6 +728,8 @@ func (a *App) exportAllDirectPosts(ctx request.CTX, writer io.Writer, withAttach if len(posts) == 0 { break } + cnt += len(posts) + updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_posts_exported", cnt) for _, post := range posts { afterId = post.Id @@ -815,3 +843,12 @@ func (a *App) DeleteExport(name string) *model.AppError { return a.RemoveFile(filePath) } + +func updateJobProgress(logger mlog.LoggerIFace, store store.Store, job *model.Job, key string, value int) { + if job != nil { + job.Data[key] = strconv.Itoa(value) + if _, err2 := store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err2 != nil { + logger.Warn("Failed to update job status", mlog.Err(err2)) + } + } +} diff --git a/app/export_test.go b/app/export_test.go index 7dbaa984b7..e839e1dc13 100644 --- a/app/export_test.go +++ b/app/export_test.go @@ -164,7 +164,7 @@ func TestExportCustomEmoji(t *testing.T) { outPath, err := filepath.Abs(filePath) require.NoError(t, err) - _, appErr := th.App.exportCustomEmoji(th.Context, fileWriter, outPath, dirNameToExportEmoji, false) + _, appErr := th.App.exportCustomEmoji(th.Context, nil, fileWriter, outPath, dirNameToExportEmoji, false) require.Nil(t, appErr, "should not have failed") } @@ -178,7 +178,7 @@ func TestExportAllUsers(t *testing.T) { require.Nil(t, err) var b bytes.Buffer - err = th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + err = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) th2 := Setup(t) @@ -235,7 +235,7 @@ func TestExportDMChannel(t *testing.T) { }) var b bytes.Buffer - err := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") @@ -282,7 +282,7 @@ func TestExportDMChannel(t *testing.T) { th1.App.PermanentDeleteUser(th1.Context, th1.BasicUser) var b bytes.Buffer - err := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) th2 := Setup(t).InitBasic() @@ -306,7 +306,7 @@ func TestExportDMChannelToSelf(t *testing.T) { th1.CreateDmChannel(th1.BasicUser) var b bytes.Buffer - err := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") @@ -344,7 +344,7 @@ func TestExportGMChannel(t *testing.T) { th1.CreateGroupChannel(th1.Context, user1, user2) var b bytes.Buffer - err := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") @@ -376,7 +376,7 @@ func TestExportGMandDMChannels(t *testing.T) { th1.CreateGroupChannel(th1.Context, user1, user2) var b bytes.Buffer - err := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") @@ -459,7 +459,7 @@ func TestExportDMandGMPost(t *testing.T) { assert.Equal(t, 4, len(posts)) var b bytes.Buffer - appErr := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + appErr := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, appErr) th1.TearDown() @@ -534,7 +534,7 @@ func TestExportPostWithProps(t *testing.T) { require.NotEmpty(t, posts[1].Props) var b bytes.Buffer - appErr := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + appErr := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, appErr) th1.TearDown() @@ -572,7 +572,7 @@ func TestExportDMPostWithSelf(t *testing.T) { th1.CreatePost(dmChannel) var b bytes.Buffer - err := th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) posts, nErr := th1.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000") @@ -640,7 +640,7 @@ func TestBulkExport(t *testing.T) { IncludeAttachments: true, CreateArchive: true, } - appErr = th.App.BulkExport(th.Context, exportFile, dir, opts) + appErr = th.App.BulkExport(th.Context, exportFile, dir, nil, opts) require.Nil(t, appErr) th.TearDown() @@ -731,7 +731,7 @@ func TestExportDeletedTeams(t *testing.T) { require.Nil(t, err) var b bytes.Buffer - err = th1.App.BulkExport(th1.Context, &b, "somePath", model.BulkExportOpts{}) + err = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) th2 := Setup(t) diff --git a/app/file.go b/app/file.go index 7a66d25010..2d3de4d2f3 100644 --- a/app/file.go +++ b/app/file.go @@ -178,22 +178,8 @@ func (s *Server) writeFile(fr io.Reader, path string) (int64, *model.AppError) { } 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) - } + written, err := filestore.TryWriteFileContext(s.FileBackend(), ctx, fr, path) if err != nil { return written, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } diff --git a/app/imaging/encode.go b/app/imaging/encode.go index 1afb3159d9..d10f73c91a 100644 --- a/app/imaging/encode.go +++ b/app/imaging/encode.go @@ -45,7 +45,9 @@ func NewEncoder(opts EncoderOptions) (*Encoder, error) { e.sem = make(chan struct{}, opts.ConcurrencyLevel) } e.opts = opts - e.pngEncoder = &png.Encoder{} + e.pngEncoder = &png.Encoder{ + CompressionLevel: png.BestCompression, + } return &e, nil } diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index f0282ec2e9..8bee3b289d 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -974,7 +974,7 @@ func (a *OpenTracingAppLayer) BuildSamlMetadataObject(idpMetadata []byte) (*mode return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts model.BulkExportOpts) *model.AppError { +func (a *OpenTracingAppLayer) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkExport") @@ -986,7 +986,7 @@ func (a *OpenTracingAppLayer) BulkExport(ctx request.CTX, writer io.Writer, outP }() defer span.Finish() - resultVar0 := a.app.BulkExport(ctx, writer, outPath, opts) + resultVar0 := a.app.BulkExport(ctx, writer, outPath, job, opts) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) diff --git a/app/platform/service_test.go b/app/platform/service_test.go index 8654b18685..3b97b68eed 100644 --- a/app/platform/service_test.go +++ b/app/platform/service_test.go @@ -141,6 +141,7 @@ func TestMetrics(t *testing.T) { mockMetricsImpl := &mocks.MetricsInterface{} mockMetricsImpl.On("Register").Return() mockMetricsImpl.On("ObserveStoreMethodDuration", mock.Anything, mock.Anything, mock.Anything).Return() + mockMetricsImpl.On("RegisterDBCollector", mock.AnythingOfType("*sql.DB"), "master") th := Setup(t, StartMetrics(), func(ps *PlatformService) error { ps.metricsIFace = mockMetricsImpl diff --git a/app/plugin_requests.go b/app/plugin_requests.go index 1ccc966822..2670cbae12 100644 --- a/app/plugin_requests.go +++ b/app/plugin_requests.go @@ -5,7 +5,6 @@ package app import ( "bytes" - "fmt" "io" "net/http" "path" @@ -93,7 +92,7 @@ func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Requ return } - // Should be in the form of /$PLUGIN_ID/public/{anything} by the time we get here + // Should be in the form of /(subpath/)?/plugins/{plugin_id}/public/* by the time we get here vars := mux.Vars(r) pluginID := vars["plugin_id"] @@ -111,8 +110,13 @@ func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Requ return } + subpath, err := utils.GetSubpathFromConfig(ch.cfgSvc.Config()) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + publicFilePath := path.Clean(r.URL.Path) - prefix := fmt.Sprintf("/plugins/%s/public/", pluginID) + prefix := path.Join(subpath, "plugins", pluginID, "public") if !strings.HasPrefix(publicFilePath, prefix) { http.NotFound(w, r) return diff --git a/app/plugin_requests_test.go b/app/plugin_requests_test.go index c41c70be6d..d5796a975f 100644 --- a/app/plugin_requests_test.go +++ b/app/plugin_requests_test.go @@ -4,23 +4,65 @@ package app import ( + "fmt" + "io" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/utils/fileutils" ) func TestServePluginPublicRequest(t *testing.T) { + installPlugin := func(t *testing.T, th *TestHelper, pluginID string) { + t.Helper() + + path, _ := fileutils.FindDir("tests") + fileReader, err := os.Open(filepath.Join(path, fmt.Sprintf("%s.tar.gz", pluginID))) + require.NoError(t, err) + defer fileReader.Close() + + _, appErr := th.App.WriteFile(fileReader, getBundleStorePath(pluginID)) + checkNoError(t, appErr) + + appErr = th.App.SyncPlugins() + checkNoError(t, appErr) + + env := th.App.GetPluginsEnvironment() + require.NotNil(t, env) + + // Check if installed + pluginStatus, err := env.Statuses() + require.NoError(t, err) + found := false + for _, pluginStatus := range pluginStatus { + if pluginStatus.PluginId == pluginID { + found = true + } + } + require.True(t, found, "failed to find plugin %s in plugin statuses", pluginID) + + appErr = th.App.EnablePlugin(pluginID) + checkNoError(t, appErr) + + t.Cleanup(func() { + appErr = th.App.ch.RemovePlugin(pluginID) + checkNoError(t, appErr) + }) + } + t.Run("returns not found when plugins environment is nil", func(t *testing.T) { th := Setup(t) - defer th.TearDown() + t.Cleanup(th.TearDown) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) - req, err := http.NewRequest("GET", "/plugins", nil) + req, err := http.NewRequest("GET", "/plugins/plugin_id/public/file.txt", nil) require.NoError(t, err) rr := httptest.NewRecorder() @@ -29,4 +71,83 @@ func TestServePluginPublicRequest(t *testing.T) { assert.Equal(t, http.StatusNotFound, rr.Code) }) + + t.Run("resolves path for valid plugin", func(t *testing.T) { + th := Setup(t) + t.Cleanup(th.TearDown) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) + + path, _ := fileutils.FindDir("tests") + fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) + defer fileReader.Close() + + installPlugin(t, th, "testplugin") + + req, err := http.NewRequest("GET", "/plugins/testplugin/public/file.txt", nil) + require.NoError(t, err) + + rr := httptest.NewRecorder() + th.App.ch.srv.Router.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + body, err := io.ReadAll(rr.Body) + require.NoError(t, err) + require.Equal(t, "Hello World!", string(body)) + }) + + t.Run("resolves path for valid plugin when subpath configured", func(t *testing.T) { + os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://localhost:8065/subpath") + defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") + + th := Setup(t) + t.Cleanup(th.TearDown) + + installPlugin(t, th, "testplugin") + + req, err := http.NewRequest("GET", "/subpath/plugins/testplugin/public/file.txt", nil) + require.NoError(t, err) + + rr := httptest.NewRecorder() + th.App.ch.srv.RootRouter.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + body, err := io.ReadAll(rr.Body) + require.NoError(t, err) + assert.Equal(t, "Hello World!", string(body)) + }) + + t.Run("fails for invalid plugin", func(t *testing.T) { + th := Setup(t) + t.Cleanup(th.TearDown) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) + + req, err := http.NewRequest("GET", "/plugins/invalidplugin/public/file.txt", nil) + require.NoError(t, err) + + rr := httptest.NewRecorder() + th.App.ch.srv.Router.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusNotFound, rr.Code) + }) + + t.Run("fails attempting to break out of path", func(t *testing.T) { + os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://localhost:8065/subpath") + defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") + + th := Setup(t) + t.Cleanup(th.TearDown) + + installPlugin(t, th, "testplugin") + installPlugin(t, th, "testplugin2") + + req, err := http.NewRequest("GET", "/subpath/plugins/testplugin/public/../../testplugin2/file.txt", nil) + require.NoError(t, err) + + rr := httptest.NewRecorder() + th.App.ch.srv.RootRouter.ServeHTTP(rr, req) + + require.Equal(t, http.StatusMovedPermanently, rr.Code) + assert.Equal(t, "/subpath/plugins/testplugin2/file.txt", rr.Header()["Location"][0]) + }) } diff --git a/app/post_test.go b/app/post_test.go index 4630eb75af..3e6b090419 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -2409,10 +2409,17 @@ func TestFollowThreadSkipsParticipants(t *testing.T) { require.True(t, p.Id == sysadmin.Id || p.Id == user.Id) } + oldID := threadMembership.PostId threadMembership.PostId = "notfound" _, err = th.App.GetThreadForUser(threadMembership, false) require.NotNil(t, err) assert.Equal(t, http.StatusNotFound, err.StatusCode) + + threadMembership.Following = false + threadMembership.PostId = oldID + _, err = th.App.GetThreadForUser(threadMembership, false) + require.NotNil(t, err) + assert.Equal(t, http.StatusNotFound, err.StatusCode) } func TestAutofollowBasedOnRootPost(t *testing.T) { diff --git a/app/preference.go b/app/preference.go index f995eb9391..6bf536249f 100644 --- a/app/preference.go +++ b/app/preference.go @@ -33,7 +33,8 @@ func (w *preferencesServiceWrapper) DeletePreferencesForUser(userID string, pref } func (a *App) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) { - preferences, err := a.Srv().Store().Preference().GetAll(userID) + limit := *a.Config().ServiceSettings.ExperimentalMaxUserPreferences + preferences, err := a.Srv().Store().Preference().GetAll(userID, limit) if err != nil { return nil, model.NewAppError("GetPreferencesForUser", "app.preference.get_all.app_error", nil, "", http.StatusBadRequest).Wrap(err) } diff --git a/app/user_test.go b/app/user_test.go index 086e71e25f..b4a57dd32a 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -121,7 +121,7 @@ func TestAdjustProfileImage(t *testing.T) { assert.True(t, adjusted.Len() > 0) assert.NotEqual(t, testjpg, adjusted) - // default image should require adjustment + // default image should not require adjustment user := th.BasicUser image, err := th.App.GetDefaultProfileImage(user) require.Nil(t, err) diff --git a/app/users/profile_picture.go b/app/users/profile_picture.go index 1858140e52..4a9c73f36f 100644 --- a/app/users/profile_picture.go +++ b/app/users/profile_picture.go @@ -160,7 +160,10 @@ func createProfileImage(username string, userID string, initialFont string) ([]b buf := new(bytes.Buffer) - if imgErr := png.Encode(buf, dstImg); imgErr != nil { + enc := png.Encoder{ + CompressionLevel: png.BestCompression, + } + if imgErr := enc.Encode(buf, dstImg); imgErr != nil { return nil, ImageEncodingError } diff --git a/cmd/mattermost/commands/export.go b/cmd/mattermost/commands/export.go index d5573ee975..21ff04162e 100644 --- a/cmd/mattermost/commands/export.go +++ b/cmd/mattermost/commands/export.go @@ -240,7 +240,7 @@ func bulkExportCmdF(command *cobra.Command, args []string) error { var opts model.BulkExportOpts opts.IncludeAttachments = attachments opts.CreateArchive = archive - if err := a.BulkExport(request.EmptyContext(a.Log()), fileWriter, filepath.Dir(outPath), opts); err != nil { + if err := a.BulkExport(request.EmptyContext(a.Log()), fileWriter, filepath.Dir(outPath), nil /* nil job since it's spawned from CLI */, opts); err != nil { CommandPrintErrorln(err.Error()) return err } diff --git a/config/client.go b/config/client.go index 6ce5433dc1..047d9eef1a 100644 --- a/config/client.go +++ b/config/client.go @@ -223,6 +223,8 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m props["BuildHash"] = model.BuildHash props["BuildHashEnterprise"] = model.BuildHashEnterprise props["BuildEnterpriseReady"] = model.BuildEnterpriseReady + props["BuildHashBoards"] = model.BuildHashBoards + props["BuildBoards"] = model.BuildBoards props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation) props["EnableFile"] = strconv.FormatBool(*c.LogSettings.EnableFile) diff --git a/config/diff_test.go b/config/diff_test.go index 15ec6f41f7..427045dae7 100644 --- a/config/diff_test.go +++ b/config/diff_test.go @@ -808,7 +808,7 @@ func TestDiff(t *testing.T) { Enable: !defaultConfigGen().PluginSettings.PluginStates["com.mattermost.nps"].Enable, }, "focalboard": { - Enable: true, + Enable: false, }, "playbooks": { Enable: true, @@ -846,7 +846,7 @@ func TestDiff(t *testing.T) { Enable: true, }, "focalboard": { - Enable: true, + Enable: false, }, "playbooks": { Enable: true, @@ -876,7 +876,7 @@ func TestDiff(t *testing.T) { BaseVal: defaultConfigGen().PluginSettings.PluginStates, ActualVal: map[string]*model.PluginState{ "focalboard": { - Enable: true, + Enable: false, }, "playbooks": { Enable: true, diff --git a/einterfaces/metrics.go b/einterfaces/metrics.go index 56e6cb5aa0..fa7b5d714a 100644 --- a/einterfaces/metrics.go +++ b/einterfaces/metrics.go @@ -4,12 +4,15 @@ package einterfaces import ( + "database/sql" + "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/mlog" ) type MetricsInterface interface { Register() + RegisterDBCollector(db *sql.DB, name string) IncrementPostCreate() IncrementWebhookPost() diff --git a/einterfaces/mocks/MetricsInterface.go b/einterfaces/mocks/MetricsInterface.go index 18ae7ad658..9eb7d06b13 100644 --- a/einterfaces/mocks/MetricsInterface.go +++ b/einterfaces/mocks/MetricsInterface.go @@ -9,6 +9,8 @@ import ( mock "github.com/stretchr/testify/mock" model "github.com/mattermost/mattermost-server/v6/model" + + sql "database/sql" ) // MetricsInterface is an autogenerated mock type for the MetricsInterface type @@ -302,6 +304,11 @@ func (_m *MetricsInterface) Register() { _m.Called() } +// RegisterDBCollector provides a mock function with given fields: db, name +func (_m *MetricsInterface) RegisterDBCollector(db *sql.DB, name string) { + _m.Called(db, name) +} + // SetReplicaLagAbsolute provides a mock function with given fields: node, value func (_m *MetricsInterface) SetReplicaLagAbsolute(node string, value float64) { _m.Called(node, value) diff --git a/i18n/de.json b/i18n/de.json index 82a8cccb82..3015f856b6 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -648,15 +648,15 @@ }, { "id": "api.command_invite.hint", - "translation": "@[Benutzername] ~[Kanal]" + "translation": "@[username]... ~[channel]..." }, { "id": "api.command_invite.missing_message.app_error", - "translation": "Benutzername und Kanal fehlen." + "translation": "Benutzername und/oder Kanal fehlen." }, { "id": "api.command_invite.missing_user.app_error", - "translation": "Der Benutzer konnte nicht gefunden werden. Er/sie wurde eventuell vom Systemadministrator deaktiviert." + "translation": "Benutzer {{.User}} konnte nicht gefunden werden. Er/sie wurde eventuell vom Systemadministrator deaktiviert." }, { "id": "api.command_invite.name", @@ -668,7 +668,7 @@ }, { "id": "api.command_invite.private_channel.app_error", - "translation": "Konnte den Kanal {{.Channel}} nicht finden. Bitte verwende den Kanal-Handle, um Kanäle zu identifizieren." + "translation": "Konnte den Kanal {{.Channel}} nicht finden. Bitte den [Kanal Handle](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) verwenden, um Kanäle zu identifizieren." }, { "id": "api.command_invite.success", @@ -999,7 +999,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Das Emoji konnte nicht erstellt werden. Das Bild muss kleiner als 1 MB sein." + "translation": "Das Emoji konnte nicht erstellt werden. Das Bild muss kleiner als 512 KiB sein." }, { "id": "api.emoji.disabled.app_error", @@ -7237,7 +7237,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "Dieser Test-Lizenzschlüssel für Mattermost Enterprise Edition ist abgelaufen und nicht mehr gültig. Wenn du deine Testphase verlängern willst, kontaktiere bitte [unser Vertriebsteam](https://mattermost.com/contact-us/)." + "translation": "Neue Testlizenz konnte nicht angewendet werden. Du hast bereits eine Testlizenz auf diese Mattermost-Instanz angewendet... Wenn du deine Testphase verlängern willst, kontaktiere bitte [unser Vertriebsteam](https://mattermost.com/contact-us/)." }, { "id": "api.license.request-trial.can-start-trial.error", @@ -9725,5 +9725,25 @@ { "id": "api.server.hosted_signup_unavailable.error", "translation": "Das Portal ist für selbst gehostete Anmeldungen nicht verfügbar." + }, + { + "id": "ent.elasticsearch.create_client.client_key_missing", + "translation": "Die Client-Schlüsseldatei für Elasticsearch konnte nicht geöffnet werden" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_missing", + "translation": "Die Client-Zertifikatsdatei für Elasticsearch konnte nicht geöffnet werden" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_malformed", + "translation": "Dekodierung des Client-Zertifikats für Elasticsearch fehlgeschlagen" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_missing", + "translation": "Die CA-Datei für Elasticsearch konnte nicht geöffnet werden" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_malformed", + "translation": "Dekodierung der CA für Elasticsearch fehlgeschlagen" } ] diff --git a/i18n/en.json b/i18n/en.json index 0fac9e2df3..6bf836abe8 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1719,7 +1719,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Unable to create emoji. Image must be less than 1 MB in size." + "translation": "Unable to create emoji. Image must be less than 512 KiB in size." }, { "id": "api.emoji.disabled.app_error", @@ -2055,7 +2055,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "This trial license key for Mattermost Enterprise Edition has expired and is no longer valid. If you would like to extend your trial period please [contact our sales team](https://mattermost.com/contact-us/)." + "translation": "Failed to apply new trial license. You have previously applied a trial license to this Mattermost instance.. If you would like to extend your trial period please [contact our sales team](https://mattermost.com/contact-us/)." }, { "id": "api.license.request_renewal_link.app_error", @@ -7351,6 +7351,26 @@ "id": "ent.elasticsearch.aggregator_worker.index_job_failed.error", "translation": "Elasticsearch aggregator worker failed due to the indexing job failing" }, + { + "id": "ent.elasticsearch.create_client.ca_cert_malformed", + "translation": "Decoding of the CA for Elasticsearch failed" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_missing", + "translation": "Could not open the CA file for Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_malformed", + "translation": "Decoding of the client certificate for Elasticsearch failed" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_missing", + "translation": "Could not open the client certificate file for Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.client_key_missing", + "translation": "Could not open the client key file for Elasticsearch" + }, { "id": "ent.elasticsearch.create_client.connect_failed", "translation": "Setting up Elasticsearch Client Failed" diff --git a/i18n/en_AU.json b/i18n/en_AU.json index e03b05824f..094e51ed6e 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -6461,7 +6461,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Unable to create emoji. Image must be less than 1 MB in size." + "translation": "Unable to create emoji. Image must be less than 512 KiB in size." }, { "id": "api.emoji.create.parse.app_error", @@ -7055,7 +7055,7 @@ }, { "id": "api.command_invite.private_channel.app_error", - "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + "translation": "Could not find the channel {{.Channel}}. Please use the [channel handle](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) to identify channels." }, { "id": "api.command_invite.permission.app_error", @@ -7067,15 +7067,15 @@ }, { "id": "api.command_invite.missing_user.app_error", - "translation": "Couldn't find a matching user. They may have been deactivated by the System Administrator." + "translation": "Couldn't find a matching user {{.User}}. They may have been deactivated by the System Administrator." }, { "id": "api.command_invite.missing_message.app_error", - "translation": "Missing Username and Channel." + "translation": "Missing Username and/or Channel." }, { "id": "api.command_invite.hint", - "translation": "@[username] ~[channel]" + "translation": "@[username]... ~[channel]..." }, { "id": "api.command_invite.group_constrained_user_denied", @@ -8772,7 +8772,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "This trial licence key for Mattermost Enterprise Edition has expired and is no longer valid. If you would like to extend your trial period please [contact our sales team](https://mattermost.com/contact-us/)." + "translation": "Failed to apply new trial licence. You have previously applied a trial license to this Mattermost instance. If you would like to extend your trial period please [contact the sales team](https://mattermost.com/contact-us/)." }, { "id": "api.license.request-trial.can-start-trial.error", diff --git a/i18n/nl.json b/i18n/nl.json index 45c0f6813e..a10623f90d 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -648,15 +648,15 @@ }, { "id": "api.command_invite.hint", - "translation": "@[gebruikersnaam] ~[kanaal]" + "translation": "@[gebruikersnaam]... ~[kanaal]..." }, { "id": "api.command_invite.missing_message.app_error", - "translation": "Ontbrekende gebruikersnaam en kanaal." + "translation": "Ontbrekende gebruikersnaam en/of kanaal." }, { "id": "api.command_invite.missing_user.app_error", - "translation": "We kunnen de gebruiker niet vinden. Deze kan gedeactiveerd zijn door de systeembeheerder." + "translation": "We kunnen de gebruiker {{.User}} niet vinden. Deze kan gedeactiveerd zijn door de systeembeheerder." }, { "id": "api.command_invite.name", @@ -668,7 +668,7 @@ }, { "id": "api.command_invite.private_channel.app_error", - "translation": "Kan het kanaal{{.Channel}} niet vinden. Gebruik de kanaalaanduiding om kanalen te identificeren." + "translation": "Kan het kanaal{{.Channel}} niet vinden. Gebruik de [kanaalaanduiding](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) om kanalen aan te duiden." }, { "id": "api.command_invite.success", @@ -999,7 +999,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Kon emoji niet aanmaken. Afbeelding moet minstens 1MB groot zijn." + "translation": "Kon emoji niet aanmaken. Afbeelding moet minder dan 512KiB groot zijn." }, { "id": "api.emoji.disabled.app_error", @@ -8772,7 +8772,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "Deze proeflicentiesleutel voor Mattermost Enterprise Edition is verlopen en is niet langer geldig. Als je jouw proefperiode wil verlengen, neem dan [contact op met ons verkoopteam](https://mattermost.com/contact-us/)." + "translation": "Fout bij het toevoegen van de nieuwe proeflicentie. Je hebt eerder al een proeflicentie toegepast op deze Mattermost instance.Als je jouw proefperiode wil verlengen, neem dan [contact op met ons verkoopteam](https://mattermost.com/contact-us/)." }, { "id": "api.license.request-trial.can-start-trial.error", @@ -9725,5 +9725,25 @@ { "id": "api.draft.create_draft.can_not_draft_to_deleted.error", "translation": "Kan concept niet bewaren in een verwijderd kanaal" + }, + { + "id": "ent.elasticsearch.create_client.client_key_missing", + "translation": "Kon het client-keybestand voor Elasticsearch niet openen" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_missing", + "translation": "Kon het client-certificaatbestand voor Elasticsearch niet openen" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_malformed", + "translation": "Het decoderen van het client-certificaat voor Elasticsearch is mislukt" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_missing", + "translation": "Kon het CA-bestand voor Elasticsearch niet openen" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_malformed", + "translation": "Decodering van de CA voor Elasticsearch is mislukt" } ] diff --git a/i18n/pl.json b/i18n/pl.json index ae492d4db1..8b6fbce3eb 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -649,15 +649,15 @@ }, { "id": "api.command_invite.hint", - "translation": "@[użytkownik] ~[kanał]" + "translation": "@[nazwa użytkownika]... ~[kanał]..." }, { "id": "api.command_invite.missing_message.app_error", - "translation": "Brak nazwy użytkownika oraz kanału." + "translation": "Brak nazwy użytkownika i/lub kanału." }, { "id": "api.command_invite.missing_user.app_error", - "translation": "Nie możemy odnaleźć użytkownika. Mógł on zostać dezaktywowany przez administratora systemu." + "translation": "Nie mogliśmy znaleźć użytkownika {{.User}}. Być może został on dezaktywowany przez Administratora systemu." }, { "id": "api.command_invite.name", @@ -669,7 +669,7 @@ }, { "id": "api.command_invite.private_channel.app_error", - "translation": "Nie można znaleźć kanału {{.Channel}}. Użyj uchwytu kanału, aby zidentyfikować kanały." + "translation": "Nie można znaleźć kanału {{.Channel}}. Użyj [uchwytu kanału](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel), aby zidentyfikować kanały." }, { "id": "api.command_invite.success", @@ -1001,7 +1001,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Nie można utworzyć emoji. Obraz musi być mniejszy niż 1 MB." + "translation": "Nie można utworzyć emoji. Obraz musi być mniejszy niż 512 KiB." }, { "id": "api.emoji.disabled.app_error", @@ -7325,7 +7325,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "Ten klucz licencji próbnej dla Mattermost Enterprise Edition wygasł i nie jest już ważny. Jeśli chcesz przedłużyć okres próbny, prosimy o [kontakt z naszym zespołem sprzedaży](https://mattermost.com/contact-us/)." + "translation": "Nie udało się zastosować nowej licencji próbnej. Zastosowano już wcześniej licencję próbną do tej instancji Mattermost... Jeśli chcesz przedłużyć okres próbny, prosimy o [kontakt z naszym zespołem sprzedaży](https://mattermost.com/contact-us/)." }, { "id": "api.license.request-trial.can-start-trial.error", @@ -9726,5 +9726,25 @@ { "id": "api.server.hosted_signup_unavailable.error", "translation": "Portal niedostępny dla samodzielnej rejestracji." + }, + { + "id": "ent.elasticsearch.create_client.client_key_missing", + "translation": "Nie można otworzyć pliku klucza klienta dla Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_missing", + "translation": "Nie można otworzyć pliku z certyfikatem klienta dla Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_malformed", + "translation": "Dekodowanie certyfikatu klienta dla Elasticsearch nie powiodło się" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_missing", + "translation": "Nie można otworzyć pliku CA dla Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_malformed", + "translation": "Dekodowanie CA dla Elasticsearch nie powiodło się" } ] diff --git a/i18n/ru.json b/i18n/ru.json index 62022682fd..f8e6b9cb60 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -649,15 +649,15 @@ }, { "id": "api.command_invite.hint", - "translation": "@[имя пользователя] ~[канал]" + "translation": "@[имя пользователя]... ~[канал]..." }, { "id": "api.command_invite.missing_message.app_error", - "translation": "Отсутствует имя пользователя и канал." + "translation": "Отсутствует имя пользователя и/или канал." }, { "id": "api.command_invite.missing_user.app_error", - "translation": "Мы не смогли найти пользователя. Вероятно он заблокирован Системным администратором." + "translation": "Мы не смогли найти пользователя {{.User}}. Вероятно он заблокирован Системным администратором." }, { "id": "api.command_invite.name", @@ -669,7 +669,7 @@ }, { "id": "api.command_invite.private_channel.app_error", - "translation": "Невозможно найти канал {{.Channel}}. Пожалуйста, используйте управление каналом для идентификации." + "translation": "Невозможно найти канал {{.Channel}}. Пожалуйста, используйте [управление каналом](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) для идентификации." }, { "id": "api.command_invite.success", @@ -1001,7 +1001,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Невозможно создать смайлик. Изображение должно быть не более 1 MB." + "translation": "Невозможно создать смайлик. Изображение должно быть не более 512 КБ." }, { "id": "api.emoji.disabled.app_error", @@ -8713,7 +8713,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "Срок действия этого пробного лицензионного ключа для Mattermost Enterprise Edition истек и больше не действует. Если вы хотите продлить пробный период, пожалуйста, [свяжитесь с нашим отделом продаж] (https://mattermost.com/contact-us/)." + "translation": "Не удалось применить новую пробную лицензию. Вы уже применяли пробную лицензию к этому экземпляру Mattermost. Если вы хотите продлить пробный период, пожалуйста, [свяжитесь с нашим отделом продаж] (https://mattermost.com/contact-us/)." }, { "id": "api.license.request-trial.can-start-trial.error", @@ -9726,5 +9726,25 @@ { "id": "api.server.hosted_signup_unavailable.error", "translation": "Портал недоступен для самостоятельной регистрации." + }, + { + "id": "ent.elasticsearch.create_client.client_key_missing", + "translation": "Не удалось открыть файл клиентского ключа для Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_missing", + "translation": "Не удалось открыть файл сертификата клиента для Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.client_cert_malformed", + "translation": "Не удалось декодировать сертификат клиента для Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_missing", + "translation": "Не удалось открыть файл CA для Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.ca_cert_malformed", + "translation": "Расшифровка CA для Elasticsearch не удалась" } ] diff --git a/i18n/sv.json b/i18n/sv.json index f5b180b542..b9b189c4c7 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -6329,7 +6329,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Det gick inte att skapa emoji. Bildfilen måste vara mindre än 1 MB i storlek." + "translation": "Det gick inte att skapa emoji. Bildfilen måste vara mindre än 512 KiB i storlek." }, { "id": "api.emoji.create.parse.app_error", @@ -6716,7 +6716,7 @@ }, { "id": "api.command_invite.private_channel.app_error", - "translation": "Kunde inte hitta kanalen {{.Channel}}. Använd Channel handle för att identifiera kanaler." + "translation": "Kunde inte hitta kanalen {{.Channel}}. Använd [channel handle](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) för att identifiera kanaler." }, { "id": "api.command_invite.permission.app_error", @@ -6728,15 +6728,15 @@ }, { "id": "api.command_invite.missing_user.app_error", - "translation": "Kunde inte hitta användaren. Kan ha blivit avstängd av Systemadministratören." + "translation": "Kunde inte hitta användaren {{.User}}. Kan ha blivit avstängd av Systemadministratören." }, { "id": "api.command_invite.missing_message.app_error", - "translation": "Saknar Användarnamn och Kanal." + "translation": "Saknar Användarnamn och/eller Kanal." }, { "id": "api.command_invite.hint", - "translation": "@[användarnamn] ~[kanal]" + "translation": "@[username]... ~[channel]..." }, { "id": "api.command_invite.group_constrained_user_denied", @@ -8772,7 +8772,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "Licensnyckeln för testperioden av Mattermost Enterprise Edition är inte längre giltig. [Kontakta Sales team] (https://mattermost.com/contact-us/) om du vill utöka testperionden." + "translation": "Misslyckades med att installera ny testlicens. Du har tidigare använt en testlicens på denna Mattermost-instans. [Kontakta försäljningsteamet] (https://mattermost.com/contact-us/) om du vill utöka testperioden." }, { "id": "api.license.request-trial.can-start-trial.error", diff --git a/i18n/tr.json b/i18n/tr.json index 2eadae012a..df4ab52fed 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -648,15 +648,15 @@ }, { "id": "api.command_invite.hint", - "translation": "@[kullanıcıadı] ~[kanal]" + "translation": "@[username]... ~[channel]..." }, { "id": "api.command_invite.missing_message.app_error", - "translation": "Kullanıcı adı ya da kanal eksik." + "translation": "Kullanıcı adı ve/veya kanal eksik." }, { "id": "api.command_invite.missing_user.app_error", - "translation": "Kullanıcı bulunamadı. Sistem yöneticisi tarafından devre dışı bırakılmış olabilir." + "translation": "{{.User}} kullanıcısı bulunamadı. Sistem yöneticisi tarafından devre dışı bırakılmış olabilir." }, { "id": "api.command_invite.name", @@ -668,7 +668,7 @@ }, { "id": "api.command_invite.private_channel.app_error", - "translation": "{{.Channel}} kanalı bulunamadı. Lütfen kanalları belirtmek için kanal kısaltması kullanın." + "translation": "{{.Channel}} kanalı bulunamadı. Lütfen kanalları belirtmek için [kanal kısaltmasını](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) kullanın." }, { "id": "api.command_invite.success", @@ -999,7 +999,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Emoji eklenemedi. Görsel boyutu 1 MB değerinden küçük olmalıdır." + "translation": "Emoji eklenemedi. Görsel 512 KiB boyutundan küçük olmalıdır." }, { "id": "api.emoji.disabled.app_error", @@ -8772,7 +8772,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "Mattermost Enterprise sürümünün deneme süresi sona ermiş olduğundan artık kullanılamaz. Deneme sürenizi uzatmak istiyorsanız [satış ekibimizle](https://mattermost.com/contact-us/) görüşebilirsiniz." + "translation": "Yeni deneme lisansı kullanılamadı. Bu Mattermost kopyasında daha önce deneme lisansı kullanmışsınız. Deneme sürenizi uzatmak istiyorsanız [satış ekibimizle](https://mattermost.com/contact-us/) görüşebilirsiniz." }, { "id": "api.license.request-trial.can-start-trial.error", @@ -9721,5 +9721,9 @@ { "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", "translation": "{{.WorkspaceName}} çalışma alanınız güncellendi. Faturanız {{.Date}} tarihinden başlayarak hesaplanacak" + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portal kişisel barındırma hesabı açmak için kullanılamaz." } ] diff --git a/jobs/base_workers.go b/jobs/base_workers.go index 58bdc98ba7..71c360690f 100644 --- a/jobs/base_workers.go +++ b/jobs/base_workers.go @@ -78,6 +78,14 @@ func (worker *SimpleWorker) DoJob(job *model.Job) { return } + var appErr *model.AppError + // We get the job again because ClaimJob changes the job status. + job, appErr = worker.jobServer.GetJob(job.Id) + if appErr != nil { + mlog.Error("SimpleWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(appErr)) + worker.setJobError(job, appErr) + } + err := worker.execute(job) if err != nil { mlog.Error("SimpleWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) @@ -90,8 +98,13 @@ func (worker *SimpleWorker) DoJob(job *model.Job) { } func (worker *SimpleWorker) setJobSuccess(job *model.Job) { + if err := worker.jobServer.SetJobProgress(job, 100); err != nil { + mlog.Error("Worker: Failed to update progress for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + worker.setJobError(job, err) + } + if err := worker.jobServer.SetJobSuccess(job); err != nil { - mlog.Error("SimpleWorker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + mlog.Error("SimpleWorker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) worker.setJobError(job, err) } } diff --git a/jobs/export_process/worker.go b/jobs/export_process/worker.go index 7f80ac1950..48605b8fb6 100644 --- a/jobs/export_process/worker.go +++ b/jobs/export_process/worker.go @@ -21,7 +21,7 @@ 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 + BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError Log() *mlog.Logger } @@ -56,7 +56,7 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { }() logger := app.Log().With(mlog.String("job_id", job.Id)) - appErr := app.BulkExport(request.EmptyContext(logger), wr, outPath, opts) + appErr := app.BulkExport(request.EmptyContext(logger), wr, outPath, job, opts) wr.Close() // Close never returns an error if appErr != nil { diff --git a/model/config.go b/model/config.go index 7f9c807562..2a8fb848c9 100644 --- a/model/config.go +++ b/model/config.go @@ -385,6 +385,7 @@ type ServiceSettings struct { EnableCustomGroups *bool `access:"site_users_and_teams"` SelfHostedPurchase *bool `access:"write_restrictable,cloud_restrictable"` AllowSyncedDrafts *bool `access:"site_posts"` + ExperimentalMaxUserPreferences *int } func (s *ServiceSettings) SetDefaults(isUpdate bool) { @@ -857,6 +858,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { if s.SelfHostedPurchase == nil { s.SelfHostedPurchase = NewBool(true) } + + if s.ExperimentalMaxUserPreferences == nil { + s.ExperimentalMaxUserPreferences = NewInt(1000) + } } type ClusterSettings struct { @@ -2563,6 +2568,9 @@ type ElasticsearchSettings struct { BatchSize *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` RequestTimeoutSeconds *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` SkipTLSVerification *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + CA *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + ClientCert *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + ClientKey *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` Trace *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` } @@ -2579,6 +2587,18 @@ func (s *ElasticsearchSettings) SetDefaults() { s.Password = NewString(ElasticsearchSettingsDefaultPassword) } + if s.CA == nil { + s.CA = NewString("") + } + + if s.ClientCert == nil { + s.ClientCert = NewString("") + } + + if s.ClientKey == nil { + s.ClientKey = NewString("") + } + if s.EnableIndexing == nil { s.EnableIndexing = NewBool(false) } @@ -2852,8 +2872,8 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) { } if s.PluginStates[PluginIdFocalboard] == nil { - // Enable the focalboard plugin by default - s.PluginStates[PluginIdFocalboard] = &PluginState{Enable: true} + // Disable the focalboard plugin by default + s.PluginStates[PluginIdFocalboard] = &PluginState{Enable: false} } if s.PluginStates[PluginIdApps] == nil { diff --git a/model/config_test.go b/model/config_test.go index cedffbcb08..752ace1547 100644 --- a/model/config_test.go +++ b/model/config_test.go @@ -387,11 +387,11 @@ func TestConfigDefaultChannelExportPluginState(t *testing.T) { } func TestConfigDefaultFocalboardPluginState(t *testing.T) { - t.Run("should enable Focalboard plugin by default", func(t *testing.T) { + t.Run("should not enable Focalboard plugin by default", func(t *testing.T) { c1 := Config{} c1.SetDefaults() - assert.True(t, c1.PluginSettings.PluginStates["focalboard"].Enable) + assert.False(t, c1.PluginSettings.PluginStates["focalboard"].Enable) }) t.Run("should not re-enable focalboard plugin after it has been disabled", func(t *testing.T) { diff --git a/model/feature_flags.go b/model/feature_flags.go index 47dd10378d..449a04b5bc 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -63,6 +63,9 @@ type FeatureFlags struct { PostPriority bool + // Enable WYSIWYG text editor + WysiwygEditor bool + PeopleProduct bool AnnualSubscription bool @@ -92,7 +95,7 @@ func (f *FeatureFlags) SetDefaults() { f.InsightsEnabled = true f.CommandPalette = false f.CallsEnabled = true - f.BoardsProduct = false + f.BoardsProduct = true f.SendWelcomePost = true f.PostPriority = true f.PeopleProduct = false @@ -101,6 +104,7 @@ func (f *FeatureFlags) SetDefaults() { f.ReduceOnBoardingTaskList = false f.ThreadsEverywhere = false f.GlobalDrafts = true + f.WysiwygEditor = false } func (f *FeatureFlags) Plugins() map[string]string { diff --git a/scripts/test.sh b/scripts/test.sh index 63f89fc785..f13d9244c5 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -14,6 +14,7 @@ COVERMODE=$8 PACKAGES_COMMA=$(echo $PACKAGES | tr ' ' ',') export MM_SERVER_PATH=$PWD +export MM_FEATUREFLAGS_BoardsProduct=false echo "Packages to test: $PACKAGES" echo "GOFLAGS: $GOFLAGS" diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index ec88b868c0..fb30db7979 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -460,6 +460,7 @@ func (ts *TelemetryService) trackConfig() { "post_priority": *cfg.ServiceSettings.PostPriority, "self_hosted_purchase": *cfg.ServiceSettings.SelfHostedPurchase, "allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts, + "experimental_max_user_preferences": *cfg.ServiceSettings.ExperimentalMaxUserPreferences, }) ts.SendTelemetry(TrackConfigTeam, map[string]any{ @@ -777,6 +778,9 @@ func (ts *TelemetryService) trackConfig() { "bulk_indexing_batch_size": *cfg.ElasticsearchSettings.BatchSize, "request_timeout_seconds": *cfg.ElasticsearchSettings.RequestTimeoutSeconds, "skip_tls_verification": *cfg.ElasticsearchSettings.SkipTLSVerification, + "isdefault_ca": isDefault(*cfg.ElasticsearchSettings.CA, ""), + "isdefault_client_cert": isDefault(*cfg.ElasticsearchSettings.ClientCert, ""), + "isdefault_client_key": isDefault(*cfg.ElasticsearchSettings.ClientKey, ""), "trace": *cfg.ElasticsearchSettings.Trace, }) diff --git a/shared/filestore/filesstore.go b/shared/filestore/filesstore.go index 59b0d28947..fd1017591c 100644 --- a/shared/filestore/filesstore.go +++ b/shared/filestore/filesstore.go @@ -4,6 +4,7 @@ package filestore import ( + "context" "io" "time" @@ -84,3 +85,18 @@ func NewFileBackend(settings FileBackendSettings) (FileBackend, error) { } return nil, errors.New("no valid filestorage driver found") } + +// TryWriteFileContext checks if the file backend supports context writes and passes the context in that case. +// Should the file backend not support contexts, it just calls WriteFile instead. This can be used to disable +// the timeouts for long writes (like exports). +func TryWriteFileContext(fb FileBackend, ctx context.Context, fr io.Reader, path string) (int64, error) { + type ContextWriter interface { + WriteFileContext(context.Context, io.Reader, string) (int64, error) + } + + if cw, ok := fb.(ContextWriter); ok { + return cw.WriteFileContext(ctx, fr, path) + } + + return fb.WriteFile(fr, path) +} diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 129ef5254b..e7e9aa5264 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -6919,7 +6919,7 @@ func (s *OpenTracingLayerPreferenceStore) Get(userID string, category string, na return result, err } -func (s *OpenTracingLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) { +func (s *OpenTracingLayerPreferenceStore) GetAll(userID string, limit int) (model.Preferences, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.GetAll") s.Root.Store.SetContext(newCtx) @@ -6928,7 +6928,7 @@ func (s *OpenTracingLayerPreferenceStore) GetAll(userID string) (model.Preferenc }() defer span.Finish() - result, err := s.PreferenceStore.GetAll(userID) + result, err := s.PreferenceStore.GetAll(userID, limit) 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 065a1400a9..cfe39a65d4 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -7845,11 +7845,11 @@ func (s *RetryLayerPreferenceStore) Get(userID string, category string, name str } -func (s *RetryLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) { +func (s *RetryLayerPreferenceStore) GetAll(userID string, limit int) (model.Preferences, error) { tries := 0 for { - result, err := s.PreferenceStore.GetAll(userID) + result, err := s.PreferenceStore.GetAll(userID, limit) if err == nil { return result, nil } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index ca7575924a..5d6b9e83f3 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -577,7 +577,7 @@ func (s SqlChannelStore) upsertPublicChannelT(transaction *sqlxTxWrapper, channe return nil } -// Save writes the (non-direct) channel channel to the database. +// Save writes the (non-direct) channel to the database. func (s SqlChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (_ *model.Channel, err error) { if channel.DeleteAt != 0 { return nil, store.NewErrInvalidInput("Channel", "DeleteAt", channel.DeleteAt) diff --git a/store/sqlstore/preference_store.go b/store/sqlstore/preference_store.go index 6815e950e8..ed223e7233 100644 --- a/store/sqlstore/preference_store.go +++ b/store/sqlstore/preference_store.go @@ -175,17 +175,22 @@ func (s SqlPreferenceStore) GetCategory(userId string, category string) (model.P } -func (s SqlPreferenceStore) GetAll(userId string) (model.Preferences, error) { - var preferences model.Preferences - query, args, err := s.getQueryBuilder(). +func (s SqlPreferenceStore) GetAll(userId string, limit int) (model.Preferences, error) { + query := s.getQueryBuilder(). Select("*"). From("Preferences"). - Where(sq.Eq{"UserId": userId}). - ToSql() + Where(sq.Eq{"UserId": userId}) + if limit > 0 { + query = query.Limit(uint64(limit)) + } + + queryString, args, err := query.ToSql() if err != nil { return nil, errors.Wrap(err, "could not build sql query to get preference") } - if err = s.GetReplicaX().Select(&preferences, query, args...); err != nil { + + var preferences model.Preferences + if err = s.GetReplicaX().Select(&preferences, queryString, args...); err != nil { return nil, errors.Wrapf(err, "failed to find Preference with userId=%s", userId) } return preferences, nil diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 28a86886cf..4e5641c219 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -301,6 +301,9 @@ func (ss *SqlStore) initConnection() { if ss.DriverName() == model.DatabaseDriverMysql { ss.masterX.MapperFunc(noOpMapper) } + if ss.metrics != nil { + ss.metrics.RegisterDBCollector(ss.masterX.DB.DB, "master") + } if len(ss.settings.DataSourceReplicas) > 0 { ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas)) @@ -312,6 +315,9 @@ func (ss *SqlStore) initConnection() { if ss.DriverName() == model.DatabaseDriverMysql { ss.ReplicaXs[i].MapperFunc(noOpMapper) } + if ss.metrics != nil { + ss.metrics.RegisterDBCollector(ss.ReplicaXs[i].DB.DB, "replica-"+strconv.Itoa(i)) + } } } @@ -325,6 +331,9 @@ func (ss *SqlStore) initConnection() { if ss.DriverName() == model.DatabaseDriverMysql { ss.searchReplicaXs[i].MapperFunc(noOpMapper) } + if ss.metrics != nil { + ss.metrics.RegisterDBCollector(ss.searchReplicaXs[i].DB.DB, "searchreplica-"+strconv.Itoa(i)) + } } } diff --git a/store/sqlstore/store_test.go b/store/sqlstore/store_test.go index 180780661c..bfd10d14c1 100644 --- a/store/sqlstore/store_test.go +++ b/store/sqlstore/store_test.go @@ -24,6 +24,7 @@ import ( "github.com/mattermost/mattermost-server/v6/db" "github.com/mattermost/mattermost-server/v6/einterfaces/mocks" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock" "github.com/mattermost/mattermost-server/v6/store" "github.com/mattermost/mattermost-server/v6/store/searchtest" "github.com/mattermost/mattermost-server/v6/store/storetest" @@ -731,6 +732,7 @@ func TestReplicaLagQuery(t *testing.T) { defer mockMetrics.AssertExpectations(t) mockMetrics.On("SetReplicaLagAbsolute", tableName, float64(1)) mockMetrics.On("SetReplicaLagTime", tableName, float64(1)) + mockMetrics.On("RegisterDBCollector", mock.AnythingOfType("*sql.DB"), "master") store := &SqlStore{ rrCounter: 0, diff --git a/store/sqlstore/thread_store.go b/store/sqlstore/thread_store.go index 0de9c5db45..f4c81ed450 100644 --- a/store/sqlstore/thread_store.go +++ b/store/sqlstore/thread_store.go @@ -503,7 +503,7 @@ func (s *SqlThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive boo func (s *SqlThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended, postPriorityEnabled bool) (*model.ThreadResponse, error) { if !threadMembership.Following { - return nil, nil // in case the thread is not followed anymore - return nil error to be interpreted as 404 + return nil, store.NewErrNotFound("ThreadMembership", "") } unreadRepliesQuery := sq. diff --git a/store/store.go b/store/store.go index 8a1c69d91a..94156513fd 100644 --- a/store/store.go +++ b/store/store.go @@ -641,7 +641,7 @@ type PreferenceStore interface { GetCategory(userID string, category string) (model.Preferences, error) GetCategoryAndName(category string, nane string) (model.Preferences, error) Get(userID string, category string, name string) (*model.Preference, error) - GetAll(userID string) (model.Preferences, error) + GetAll(userID string, limit int) (model.Preferences, error) Delete(userID, category, name string) error DeleteCategory(userID string, category string) error DeleteCategoryAndName(category string, name string) error diff --git a/store/storetest/mocks/PreferenceStore.go b/store/storetest/mocks/PreferenceStore.go index c651e905bc..9c7cbd84c4 100644 --- a/store/storetest/mocks/PreferenceStore.go +++ b/store/storetest/mocks/PreferenceStore.go @@ -121,13 +121,13 @@ func (_m *PreferenceStore) Get(userID string, category string, name string) (*mo return r0, r1 } -// GetAll provides a mock function with given fields: userID -func (_m *PreferenceStore) GetAll(userID string) (model.Preferences, error) { - ret := _m.Called(userID) +// GetAll provides a mock function with given fields: userID, limit +func (_m *PreferenceStore) GetAll(userID string, limit int) (model.Preferences, error) { + ret := _m.Called(userID, limit) var r0 model.Preferences - if rf, ok := ret.Get(0).(func(string) model.Preferences); ok { - r0 = rf(userID) + if rf, ok := ret.Get(0).(func(string, int) model.Preferences); ok { + r0 = rf(userID, limit) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(model.Preferences) @@ -135,8 +135,8 @@ func (_m *PreferenceStore) GetAll(userID string) (model.Preferences, error) { } var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(userID) + if rf, ok := ret.Get(1).(func(string, int) error); ok { + r1 = rf(userID, limit) } else { r1 = ret.Error(1) } diff --git a/store/storetest/preference_store.go b/store/storetest/preference_store.go index 5d645f345a..42a5ae9ed1 100644 --- a/store/storetest/preference_store.go +++ b/store/storetest/preference_store.go @@ -184,7 +184,7 @@ func testPreferenceGetAll(t *testing.T, ss store.Store) { err := ss.Preference().Save(preferences) require.NoError(t, err) - result, err := ss.Preference().GetAll(userId) + result, err := ss.Preference().GetAll(userId, 0) require.NoError(t, err) require.Equal(t, 3, len(result), "got the wrong number of preferences") @@ -243,13 +243,13 @@ func testPreferenceDelete(t *testing.T, ss store.Store) { err := ss.Preference().Save(model.Preferences{preference}) require.NoError(t, err) - preferences, err := ss.Preference().GetAll(preference.UserId) + preferences, err := ss.Preference().GetAll(preference.UserId, 0) require.NoError(t, err) assert.Len(t, preferences, 1, "should've returned 1 preference") err = ss.Preference().Delete(preference.UserId, preference.Category, preference.Name) require.NoError(t, err) - preferences, err = ss.Preference().GetAll(preference.UserId) + preferences, err = ss.Preference().GetAll(preference.UserId, 0) require.NoError(t, err) assert.Empty(t, preferences, "should've returned no preferences") } @@ -275,14 +275,14 @@ func testPreferenceDeleteCategory(t *testing.T, ss store.Store) { err := ss.Preference().Save(model.Preferences{preference1, preference2}) require.NoError(t, err) - preferences, err := ss.Preference().GetAll(userId) + preferences, err := ss.Preference().GetAll(userId, 0) require.NoError(t, err) assert.Len(t, preferences, 2, "should've returned 2 preferences") err = ss.Preference().DeleteCategory(userId, category) require.NoError(t, err) - preferences, err = ss.Preference().GetAll(userId) + preferences, err = ss.Preference().GetAll(userId, 0) require.NoError(t, err) assert.Empty(t, preferences, "should've returned no preferences") } @@ -310,22 +310,22 @@ func testPreferenceDeleteCategoryAndName(t *testing.T, ss store.Store) { err := ss.Preference().Save(model.Preferences{preference1, preference2}) require.NoError(t, err) - preferences, err := ss.Preference().GetAll(userId) + preferences, err := ss.Preference().GetAll(userId, 0) require.NoError(t, err) assert.Len(t, preferences, 1, "should've returned 1 preference") - preferences, err = ss.Preference().GetAll(userId2) + preferences, err = ss.Preference().GetAll(userId2, 0) require.NoError(t, err) assert.Len(t, preferences, 1, "should've returned 1 preference") err = ss.Preference().DeleteCategoryAndName(category, name) require.NoError(t, err) - preferences, err = ss.Preference().GetAll(userId) + preferences, err = ss.Preference().GetAll(userId, 0) require.NoError(t, err) assert.Empty(t, preferences, "should've returned no preference") - preferences, err = ss.Preference().GetAll(userId2) + preferences, err = ss.Preference().GetAll(userId2, 0) require.NoError(t, err) assert.Empty(t, preferences, "should've returned no preference") } diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 19ccaf656d..2a2ed7eefe 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -6256,10 +6256,10 @@ func (s *TimerLayerPreferenceStore) Get(userID string, category string, name str return result, err } -func (s *TimerLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) { +func (s *TimerLayerPreferenceStore) GetAll(userID string, limit int) (model.Preferences, error) { start := time.Now() - result, err := s.PreferenceStore.GetAll(userID) + result, err := s.PreferenceStore.GetAll(userID, limit) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/tests/10000x1_expected_preview.png b/tests/10000x1_expected_preview.png index bf2ca9cc21..6c48a52421 100644 Binary files a/tests/10000x1_expected_preview.png and b/tests/10000x1_expected_preview.png differ diff --git a/tests/10000x1_expected_thumb.png b/tests/10000x1_expected_thumb.png index a354c41047..93a8cb3667 100644 Binary files a/tests/10000x1_expected_thumb.png and b/tests/10000x1_expected_thumb.png differ diff --git a/tests/1x10000_expected_preview.png b/tests/1x10000_expected_preview.png index b4317244e9..ba938e6aef 100644 Binary files a/tests/1x10000_expected_preview.png and b/tests/1x10000_expected_preview.png differ diff --git a/tests/1x10000_expected_thumb.png b/tests/1x10000_expected_thumb.png index 71dd5eba8f..4078828348 100644 Binary files a/tests/1x10000_expected_thumb.png and b/tests/1x10000_expected_thumb.png differ diff --git a/tests/README.md b/tests/README.md index 3a464bf033..d259132da5 100644 --- a/tests/README.md +++ b/tests/README.md @@ -22,18 +22,19 @@ It is possible to manually test specific sections of any test, instead of using There are two test plugins: `testplugin.tar.gz` and `testplugin2.tar.gz`. These are use in some integration tests in the `api4` package. Any changes to the plugin bundles require updating the corresponding signatures. First, import the public and private development key: -``` -$ gpg --import ./development-public-key.gpg -$ gpg --import ./development-private-key.asc +```sh +gpg --import ./development-public-key.gpg +gpg --import ./development-private-key.asc ``` This has to be done only once. Then update the signatures: +```sh +gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin.tar.gz +gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin.tar.gz +gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin2.tar.gz +gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin2.tar.gz ``` -$ gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin.tar.gz -$ gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin.tar.gz -$ gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin2.tar.gz -$ gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin2.tar.gz Finally, include the updates bundles and signatures in your commit. diff --git a/tests/fill_test_16bit_rgb_out.png b/tests/fill_test_16bit_rgb_out.png index bb9741d53f..72f7b9ca99 100644 Binary files a/tests/fill_test_16bit_rgb_out.png and b/tests/fill_test_16bit_rgb_out.png differ diff --git a/tests/fill_test_16bit_rgba_out.png b/tests/fill_test_16bit_rgba_out.png index 491bee1ce5..7e521ccbe6 100644 Binary files a/tests/fill_test_16bit_rgba_out.png and b/tests/fill_test_16bit_rgba_out.png differ diff --git a/tests/fill_test_8bit_palette_out.png b/tests/fill_test_8bit_palette_out.png index d865b78def..5feebf44cd 100644 Binary files a/tests/fill_test_8bit_palette_out.png and b/tests/fill_test_8bit_palette_out.png differ diff --git a/tests/fill_test_8bit_rgb_out.png b/tests/fill_test_8bit_rgb_out.png index 499026d665..3214f84c0f 100644 Binary files a/tests/fill_test_8bit_rgb_out.png and b/tests/fill_test_8bit_rgb_out.png differ diff --git a/tests/fill_test_8bit_rgba_out.png b/tests/fill_test_8bit_rgba_out.png index bd5c157f2a..3167887d9d 100644 Binary files a/tests/fill_test_8bit_rgba_out.png and b/tests/fill_test_8bit_rgba_out.png differ diff --git a/tests/testplugin.tar.gz b/tests/testplugin.tar.gz index da716bbee3..456276f917 100644 Binary files a/tests/testplugin.tar.gz and b/tests/testplugin.tar.gz differ diff --git a/tests/testplugin.tar.gz.asc b/tests/testplugin.tar.gz.asc index 2ea3c9f3b1..7a7885f779 100644 --- a/tests/testplugin.tar.gz.asc +++ b/tests/testplugin.tar.gz.asc @@ -1,14 +1,14 @@ -----BEGIN PGP SIGNATURE----- -iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAl4sogwACgkQTHxlYsGS -zB+CiwwAqNhwq6PQeKCQyJ4F1kZBpSkHrlbaT+V89tcj5BomhxFin30XukW2tiov -+U4cfeKI+NAu9uPUxN6f4r6khQOGQK0bvun3YDemhbVozaPneNoxs+ugkBLMrwvp -v3Vbi241bTWsi6NxlwJDSM+LEYWkFXZKCjQFjX2UWEM86uocKZnjHHqqke4ZkXWm -Sal1mOvfZtx/R0+8aKt7FEbdUy4s15gRcVfnp017PD9VDwfiXSMVrdaYr3HqD2Q/ -WMMmZ9lW4Y0I6qtv+1Ud9YZAXPr8OzsgU13FXU1GcUG+L/W8jSb9XY/4EIFpb4O6 -tQGRBBtjq0EofVq8S9V6/LMPH3/CPgHufK7TWl12mnyGUOac4YmFlGtStkovIHZJ -+nxcMsV5xU3UhdM+/uBJnC5EH8sH2hQpkJugZIFruswfHNSiNKUpiHjupepfsV7v -jzKCEgh7Rv99QBSSBtZSuBitnzEWAE3X9UsEYx5qCQJvBBVLiugFHFv6MtkePRNd -ElLLqMat -=KHZP +iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmOaRXoACgkQTHxlYsGS +zB8H1Av+MuNxBuQFxNvORGcudExCAAgQZb3ykYNVxPT1CzwVdd16B+VvRyt3+PKz +nSsyYyrdvd2xpdaEXFHBA8RxS5ZWCz/hkrdxUhBUWV8O5OUMOHDYvetWc6/9GeuR +3dd4VElpLsEs6hIpwnejR1EouNr5OhxstpnB2AOz5N7LWlG5lTKhaHs1zN1uLc4f +GmdJZ+5+PYm1UUipFj4kolkI+44Ytl6mj+tTyC4VJAj0mwnXQtp/JdFcDmmeRrTI +AwmanJKQlK3yw331FYSd/CXuqCGOh157X7Z5P2Mtr3ZOaNj7qLY0mjweqxjj4fnN +YTUu22KhRLGzggEbTg+5huYhtvqa1b87EcH6ukxWoBYQpFK+TyyhuX3ZeT5x6lFi +8SP9o/9KcQxB5oD6X5FGMR4v5VDosNnNuqW8G7g4fkcjQKY65tnX75G5Ih156BAP +dfZ6+nKCQvXgv1XRymF3UrJeddXtOMQzh4aSvYwwy46qNMPYMeDq6PoMXGVNeylP +bTrjLEtE +=OvPw -----END PGP SIGNATURE----- diff --git a/tests/testplugin.tar.gz.sig b/tests/testplugin.tar.gz.sig index 39efc817b7..280f77dc12 100644 Binary files a/tests/testplugin.tar.gz.sig and b/tests/testplugin.tar.gz.sig differ