diff options
-rw-r--r-- | api4/system.go | 4 | ||||
-rw-r--r-- | api4/system_test.go | 57 | ||||
-rw-r--r-- | app/notification.go | 15 | ||||
-rw-r--r-- | app/plugin.go | 33 | ||||
-rw-r--r-- | app/plugin_test.go | 25 | ||||
-rw-r--r-- | cmd/commands/user.go | 4 | ||||
-rw-r--r-- | i18n/de.json | 34 | ||||
-rw-r--r-- | i18n/en.json | 4 | ||||
-rw-r--r-- | i18n/it.json | 2 | ||||
-rw-r--r-- | i18n/ko.json | 56 | ||||
-rw-r--r-- | i18n/nl.json | 2 | ||||
-rw-r--r-- | i18n/pt-BR.json | 22 | ||||
-rw-r--r-- | i18n/zh-CN.json | 10 | ||||
-rw-r--r-- | i18n/zh-TW.json | 26 | ||||
-rw-r--r-- | mkdocs.yml | 11 | ||||
-rw-r--r-- | model/client4.go | 4 | ||||
-rw-r--r-- | plugin/rpcplugin/sandbox/sandbox_linux.go | 49 | ||||
-rw-r--r-- | store/sqlstore/post_store.go | 85 | ||||
-rw-r--r-- | store/storetest/post_store.go | 248 | ||||
-rw-r--r-- | utils/api.go | 1 | ||||
-rw-r--r-- | utils/config.go | 3 | ||||
-rw-r--r-- | utils/file_backend_s3.go | 7 | ||||
-rw-r--r-- | utils/file_backend_s3_test.go | 10 | ||||
-rw-r--r-- | utils/mail.go | 123 | ||||
-rw-r--r-- | utils/mail_test.go | 76 |
25 files changed, 570 insertions, 341 deletions
diff --git a/api4/system.go b/api4/system.go index c1541f0b5..7b63afc0b 100644 --- a/api4/system.go +++ b/api4/system.go @@ -395,6 +395,10 @@ func testS3(c *Context, w http.ResponseWriter, r *http.Request) { return } + if cfg.FileSettings.AmazonS3SecretAccessKey == model.FAKE_SETTING { + cfg.FileSettings.AmazonS3SecretAccessKey = c.App.Config().FileSettings.AmazonS3SecretAccessKey + } + license := c.App.License() backend, appErr := utils.NewFileBackend(&cfg.FileSettings, license != nil && *license.Features.Compliance) if appErr == nil { diff --git a/api4/system_test.go b/api4/system_test.go index e39486b77..6ef02cbfe 100644 --- a/api4/system_test.go +++ b/api4/system_test.go @@ -262,28 +262,34 @@ func TestEmailTest(t *testing.T) { defer th.TearDown() Client := th.Client - SendEmailNotifications := th.App.Config().EmailSettings.SendEmailNotifications - SMTPServer := th.App.Config().EmailSettings.SMTPServer - SMTPPort := th.App.Config().EmailSettings.SMTPPort - FeedbackEmail := th.App.Config().EmailSettings.FeedbackEmail - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SendEmailNotifications = SendEmailNotifications }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SMTPServer = SMTPServer }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SMTPPort = SMTPPort }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.FeedbackEmail = FeedbackEmail }) - }() - - th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SendEmailNotifications = false }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SMTPServer = "" }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.SMTPPort = "" }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.EmailSettings.FeedbackEmail = "" }) + config := model.Config{ + EmailSettings: model.EmailSettings{ + SMTPServer: "", + SMTPPort: "", + }, + } - _, resp := Client.TestEmail() + _, resp := Client.TestEmail(&config) CheckForbiddenStatus(t, resp) - _, resp = th.SystemAdminClient.TestEmail() + _, resp = th.SystemAdminClient.TestEmail(&config) CheckErrorMessage(t, resp, "api.admin.test_email.missing_server") CheckBadRequestStatus(t, resp) + + inbucket_host := os.Getenv("CI_HOST") + if inbucket_host == "" { + inbucket_host = "dockerhost" + } + + inbucket_port := os.Getenv("CI_INBUCKET_PORT") + if inbucket_port == "" { + inbucket_port = "9000" + } + + config.EmailSettings.SMTPServer = inbucket_host + config.EmailSettings.SMTPPort = inbucket_port + _, resp = th.SystemAdminClient.TestEmail(&config) + CheckOKStatus(t, resp) } func TestDatabaseRecycle(t *testing.T) { @@ -491,7 +497,7 @@ func TestS3TestConnection(t *testing.T) { AmazonS3AccessKeyId: model.MINIO_ACCESS_KEY, AmazonS3SecretAccessKey: model.MINIO_SECRET_KEY, AmazonS3Bucket: "", - AmazonS3Endpoint: "", + AmazonS3Endpoint: s3Endpoint, AmazonS3SSL: model.NewBool(false), }, } @@ -506,20 +512,11 @@ func TestS3TestConnection(t *testing.T) { } config.FileSettings.AmazonS3Bucket = model.MINIO_BUCKET + config.FileSettings.AmazonS3Region = "us-east-1" _, resp = th.SystemAdminClient.TestS3Connection(&config) - CheckBadRequestStatus(t, resp) - if resp.Error.Message != "S3 Endpoint is required" { - t.Fatal("should return error - missing s3 endpoint") - } - - config.FileSettings.AmazonS3Endpoint = s3Endpoint - _, resp = th.SystemAdminClient.TestS3Connection(&config) - CheckBadRequestStatus(t, resp) - if resp.Error.Message != "S3 Region is required" { - t.Fatal("should return error - missing s3 region") - } + CheckOKStatus(t, resp) - config.FileSettings.AmazonS3Region = "us-east-1" + config.FileSettings.AmazonS3Region = "" _, resp = th.SystemAdminClient.TestS3Connection(&config) CheckOKStatus(t, resp) diff --git a/app/notification.go b/app/notification.go index 8cb63fbaf..bb0c8703f 100644 --- a/app/notification.go +++ b/app/notification.go @@ -55,10 +55,15 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod if channel.Type == model.CHANNEL_DIRECT { var otherUserId string - if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { - otherUserId = userIds[1] - } else { - otherUserId = userIds[0] + + userIds := strings.Split(channel.Name, "__") + + if userIds[0] != userIds[1] { + if userIds[0] == post.UserId { + otherUserId = userIds[1] + } else { + otherUserId = userIds[0] + } } if _, ok := profileMap[otherUserId]; ok { @@ -89,7 +94,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod delete(mentionedUserIds, post.UserId) } - if len(m.OtherPotentialMentions) > 0 { + if len(m.OtherPotentialMentions) > 0 && !post.IsSystemMessage() { if result := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, team.Id); result.Err == nil { outOfChannelMentions := result.Data.([]*model.User) if channel.Type != model.CHANNEL_GROUP { diff --git a/app/plugin.go b/app/plugin.go index fe671d26a..6702e9227 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -91,18 +91,11 @@ func (a *App) ActivatePlugins() { active := a.PluginEnv.IsPluginActive(id) if pluginState.Enable && !active { - if err := a.PluginEnv.ActivatePlugin(id); err != nil { - l4g.Error(err.Error()) + if err := a.activatePlugin(plugin.Manifest); err != nil { + l4g.Error("%v plugin enabled in config.json but failing to activate err=%v", plugin.Manifest.Id, err.DetailedError) continue } - if plugin.Manifest.HasClient() { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil) - message.Add("manifest", plugin.Manifest.ClientManifest()) - a.Publish(message) - } - - l4g.Info("Activated %v plugin", id) } else if !pluginState.Enable && active { if err := a.deactivatePlugin(plugin.Manifest); err != nil { l4g.Error(err.Error()) @@ -111,6 +104,21 @@ func (a *App) ActivatePlugins() { } } +func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError { + if err := a.PluginEnv.ActivatePlugin(manifest.Id); err != nil { + return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if manifest.HasClient() { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil) + message.Add("manifest", manifest.ClientManifest()) + a.Publish(message) + } + + l4g.Info("Activated %v plugin", manifest.Id) + return nil +} + func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError { if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil { return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) @@ -301,11 +309,18 @@ func (a *App) EnablePlugin(id string) *model.AppError { return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) } + if err := a.activatePlugin(manifest); err != nil { + return err + } + a.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true} }) if err := a.SaveConfig(a.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) + } return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) } diff --git a/app/plugin_test.go b/app/plugin_test.go index 4794d2704..9ad5dc1fa 100644 --- a/app/plugin_test.go +++ b/app/plugin_test.go @@ -4,8 +4,10 @@ package app import ( + "errors" "net/http" "net/http/httptest" + "strings" "testing" "github.com/gorilla/mux" @@ -195,3 +197,26 @@ func TestPluginCommands(t *testing.T) { require.NotNil(t, err) assert.Equal(t, http.StatusNotFound, err.StatusCode) } + +type pluginBadActivation struct { + testPlugin +} + +func (p *pluginBadActivation) OnActivate(api plugin.API) error { + return errors.New("won't activate for some reason") +} + +func TestPluginBadActivation(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.InstallPlugin(&model.Manifest{ + Id: "foo", + }, &pluginBadActivation{}) + + t.Run("EnablePlugin bad activation", func(t *testing.T) { + err := th.App.EnablePlugin("foo") + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.DetailedError, "won't activate for some reason")) + }) +} diff --git a/cmd/commands/user.go b/cmd/commands/user.go index a8b7341b2..fe4d34a48 100644 --- a/cmd/commands/user.go +++ b/cmd/commands/user.go @@ -553,7 +553,7 @@ func migrateAuthToLdapCmdF(command *cobra.Command, args []string) error { } fromAuth := args[0] - matchField := args[1] + matchField := args[2] if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "saml") { return errors.New("Invalid from_auth argument") @@ -594,7 +594,7 @@ func migrateAuthToSamlCmdF(command *cobra.Command, args []string) error { matchesFile := "" matches := map[string]string{} if !autoFlag { - matchesFile = args[1] + matchesFile = args[2] file, e := ioutil.ReadFile(matchesFile) if e != nil { diff --git a/i18n/de.json b/i18n/de.json index 35355919c..2a7952315 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1370,7 +1370,7 @@ }, { "id": "api.file.upload_file.incorrect_number_of_files.app_error", - "translation": "Unable to upload files. Incorrect number of files specified." + "translation": "Konnte Dateien nicht hochladen. Falsche Anzahl von Dateien spezifiziert." }, { "id": "api.file.upload_file.large_image.app_error", @@ -1618,7 +1618,7 @@ }, { "id": "api.oauth.singup_with_oauth.expired_link.app_error", - "translation": "Der Registrierungs-Link ist abgelaufen" + "translation": "Der Registrierungslink ist abgelaufen" }, { "id": "api.oauth.singup_with_oauth.invalid_link.app_error", @@ -1812,15 +1812,15 @@ }, { "id": "api.post.send_notifications_and_forget.push_image_only", - "translation": " Eine oder mehrere Dateien hochgeladen in " + "translation": " hat eine oder mehrere Dateien hochgeladen in " }, { "id": "api.post.send_notifications_and_forget.push_image_only_dm", - "translation": " Eine oder mehrere Dateien in einer Direktnachricht hochgeladen" + "translation": " hat eine oder mehrere Dateien in einer Direktnachricht hochgeladen" }, { "id": "api.post.send_notifications_and_forget.push_image_only_no_channel", - "translation": " Eine oder mehrere Dateien hochgeladen in " + "translation": " hat eine oder mehrere Dateien hochgeladen" }, { "id": "api.post.send_notifications_and_forget.push_in", @@ -2308,11 +2308,11 @@ }, { "id": "api.team.move_channel.post.error", - "translation": "Fehler beim Senden des Kanalzwecks" + "translation": "Fehler beim Senden der Nachricht zur Verschiebung des Kanals." }, { "id": "api.team.move_channel.success", - "translation": "This channel has been moved to this team from %v." + "translation": "Dieser Kanal wurde von %v in dieses Team verschoben." }, { "id": "api.team.permanent_delete_team.attempting.warn", @@ -2720,7 +2720,7 @@ }, { "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error", - "translation": "Der Einladungslink scheint nicht gültig zu sein" + "translation": "Der Registrierungslink scheint nicht gültig zu sein" }, { "id": "api.user.create_user.team_name.app_error", @@ -3080,7 +3080,7 @@ }, { "id": "api.webhook.incoming.error", - "translation": "Could not decode the multipart payload of incoming webhook." + "translation": "Konnte die Multipart-Daten des eingehenden Webhooks nicht entschlüsseln." }, { "id": "api.webhook.init.debug", @@ -3656,7 +3656,7 @@ }, { "id": "app.plugin.disabled.app_error", - "translation": "Plugins have been disabled. Please check your logs for details." + "translation": "Plugins wurden deaktiviert. Bitte prüfen Sie Ihre Logs für Details." }, { "id": "app.plugin.extract.app_error", @@ -4192,15 +4192,15 @@ }, { "id": "ent.migration.migratetosaml.email_already_used_by_other_user", - "translation": "Email already used by another SAML user." + "translation": "E-Mail-Adresse wird bereits von einem anderen SAML-Benutzer verwendet." }, { "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", - "translation": "User not found in the users file." + "translation": "Benutzer nicht in der Benutzerdatei gefunden." }, { "id": "ent.migration.migratetosaml.username_already_used_by_other_user", - "translation": "Username already used by another Mattermost user." + "translation": "Benutzername wird bereits von einem anderen Mattermost-Benutzer verwendet." }, { "id": "ent.saml.attribute.app_error", @@ -4904,7 +4904,7 @@ }, { "id": "model.config.is_valid.message_export.export_type.app_error", - "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + "translation": "'ExportFormat' des Nachrichten-Export-Jobs muss 'actiance' oder 'globalrelay' sein." }, { "id": "model.config.is_valid.message_export.file_location.app_error", @@ -4916,7 +4916,7 @@ }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", - "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" + "translation": "Nachrichten-Export-Job GlobalRelayEmailAddress muss eine gültige E-Mail-Adresse sein." }, { "id": "model.config.is_valid.password_length.app_error", @@ -5048,7 +5048,7 @@ }, { "id": "model.config.is_valid.websocket_url.app_error", - "translation": "Die WebRTC-Gateway-Websocket-URL muss gesetzt und eine gültige URL sein sowie mit ws:// oder wss:// beginnen." + "translation": "Websocket-URL muss eine gültige URL sein sowie mit ws:// oder wss:// beginnen." }, { "id": "model.config.is_valid.write_timeout.app_error", @@ -7136,7 +7136,7 @@ }, { "id": "utils.mail.sendMail.attachments.write_error", - "translation": "Failed to write attachment to email" + "translation": "Fehler beim Hinzufügen des Mailanhanges" }, { "id": "utils.mail.send_mail.close.app_error", diff --git a/i18n/en.json b/i18n/en.json index 85a09a139..c3206caff 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3719,6 +3719,10 @@ "translation": "Error saving plugin state in config" }, { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, + { "id": "app.plugin.deactivate.app_error", "translation": "Unable to deactivate plugin" }, diff --git a/i18n/it.json b/i18n/it.json index 0918ececa..5cb1c8a85 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -3636,7 +3636,7 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "[{{.SubjectText}}] Nuovo messaggio diretto da {{.SenderDisplayName}} il {{.Day}}/{{.Month}}/{{.Year}}" + "translation": "[{{.SiteName}}] Nuovo messaggio diretto da {{.SenderDisplayName}} il {{.Day}}/{{.Month}}/{{.Year}}" }, { "id": "app.notification.subject.notification.full", diff --git a/i18n/ko.json b/i18n/ko.json index 79edba95c..13ba19b79 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -101,7 +101,7 @@ }, { "id": "api.admin.test_email.reenter_password", - "translation": "SMTP 서버, 포트, 사용자 정보가 변경되었습니다. SMTP 비밀번호를 다시 입력해주세요." + "translation": "SMTP 서버, 포트 또는 사용자이름이 변경되었습니다. SMTP 비밀번호를 다시 입력해주세요." }, { "id": "api.admin.test_email.subject", @@ -141,7 +141,7 @@ }, { "id": "api.auth.unable_to_get_user.app_error", - "translation": "권한을 확인하기 위한 유저 정보를 가져올 수 없습니다." + "translation": "권한을 확인하기 위한 사용자 정보를 가져올 수 없습니다." }, { "id": "api.brand.init.debug", @@ -197,11 +197,11 @@ }, { "id": "api.channel.change_channel_privacy.private_to_public", - "translation": "This channel has been converted to a Public Channel and can be joined by any team member." + "translation": "이 채널은 공개 채널로 전환되어 모든 팀원이 들어올 수 있습니다." }, { "id": "api.channel.change_channel_privacy.public_to_private", - "translation": "This channel has been converted to a Private Channel." + "translation": "이 채널은 비공개 채널로 변경되었습니다." }, { "id": "api.channel.create_channel.direct_channel.app_error", @@ -433,7 +433,7 @@ }, { "id": "api.command.execute_command.not_found.app_error", - "translation": "Command with a trigger of '{{.Trigger}}' not found. To send a message beginning with \"/\", try adding an empty space at the beginning of the message." + "translation": "'{{.Trigger}}' 트리거에 실행되는 커맨드를 찾지 못했습니다. \"/\"로 시작하는 메시지를 보내려면 메시지 시작 부분에 빈 공간을 추가하십시오." }, { "id": "api.command.execute_command.save.app_error", @@ -521,7 +521,7 @@ }, { "id": "api.command_channel_header.name", - "translation": "header" + "translation": "머리글" }, { "id": "api.command_channel_header.permission.app_error", @@ -541,11 +541,11 @@ }, { "id": "api.command_channel_purpose.desc", - "translation": "Edit the channel purpose" + "translation": "체널 설명 수정하기" }, { "id": "api.command_channel_purpose.direct_group.app_error", - "translation": "Cannot set purpose for direct message channels. Use /header to set the header instead." + "translation": "개인 메시지의 설명은 설정할 수 없습니다. 머리글을 설정하려면 /header 를 사용하세요." }, { "id": "api.command_channel_purpose.hint", @@ -557,11 +557,11 @@ }, { "id": "api.command_channel_purpose.name", - "translation": "purpose" + "translation": "설명" }, { "id": "api.command_channel_purpose.permission.app_error", - "translation": "당신은 채널 머릿말을 수정할 권한을 가지고 있지 않습니다." + "translation": "당신은 채널 설명을 수정할 권한을 가지고 있지 않습니다." }, { "id": "api.command_channel_purpose.update_channel.app_error", @@ -573,11 +573,11 @@ }, { "id": "api.command_channel_rename.desc", - "translation": "Rename the channel" + "translation": "채널 이름 바꾸기" }, { "id": "api.command_channel_rename.direct_group.app_error", - "translation": "개인 메시지 채널은 나갈 수 없습니다" + "translation": "개인 메시지 채널의 이름은 변경 할 수 없습니다." }, { "id": "api.command_channel_rename.hint", @@ -585,23 +585,23 @@ }, { "id": "api.command_channel_rename.message.app_error", - "translation": "메시지는 /echo 명령어와 함께 제공되어야 합니다." + "translation": "메시지는 /rename 명령어와 함께 제공되어야 합니다." }, { "id": "api.command_channel_rename.name", - "translation": "rename" + "translation": "이름변경" }, { "id": "api.command_channel_rename.permission.app_error", - "translation": "당신은 채널 머릿말을 수정할 권한을 가지고 있지 않습니다." + "translation": "당신은 채널 이름을 수정할 권한을 가지고 있지 않습니다." }, { "id": "api.command_channel_rename.too_long.app_error", - "translation": "Channel name must be {{.Length}} or fewer characters" + "translation": "채널 이름은 {{.Length}} 자 이하여야 합니다." }, { "id": "api.command_channel_rename.too_short.app_error", - "translation": "Channel name must be {{.Length}} or more characters" + "translation": "채널 이름은 {{.Length}} 자 이상이여야 합니다." }, { "id": "api.command_channel_rename.update_channel.app_error", @@ -609,11 +609,11 @@ }, { "id": "api.command_channel_rename.update_channel.success", - "translation": "채널 머릿말이 성공적으로 업데이트되었습니다." + "translation": "채널이름이 성공적으로 업데이트되었습니다." }, { "id": "api.command_code.desc", - "translation": "Display text as a code block" + "translation": "텍스트를 코드 블록으로 표시합니다." }, { "id": "api.command_code.hint", @@ -621,11 +621,11 @@ }, { "id": "api.command_code.message.app_error", - "translation": "메시지는 /echo 명령어와 함께 제공되어야 합니다." + "translation": "메시지는 /code 명령어와 함께 제공되어야 합니다." }, { "id": "api.command_code.name", - "translation": "code" + "translation": "코드" }, { "id": "api.command_collapse.desc", @@ -645,7 +645,7 @@ }, { "id": "api.command_dnd.disabled", - "translation": "Do Not Disturb is disabled." + "translation": "방해 금지 모드가 해제되었습니다." }, { "id": "api.command_dnd.error", @@ -705,7 +705,7 @@ }, { "id": "api.command_groupmsg.desc", - "translation": "Sends a Group Message to the specified users" + "translation": "지정된 사용자에게 그룹 메시지를 보냅니다." }, { "id": "api.command_groupmsg.fail.app_error", @@ -760,7 +760,7 @@ }, { "id": "api.command_help.name", - "translation": "help" + "translation": "도움말" }, { "id": "api.command_join.desc", @@ -816,7 +816,7 @@ }, { "id": "api.command_leave.success", - "translation": "%v 가 채널을 떠났습니다." + "translation": "채널을 떠났습니다." }, { "id": "api.command_logout.desc", @@ -1772,7 +1772,7 @@ }, { "id": "api.post.make_direct_channel_visible.get_2_members.error", - "translation": "다이렉트 채널에 2명의 구성원을 가져올 수 없습니다. channel_id={{.ChannelId}}" + "translation": "개인 메시지 채널에 2명의 사용자를 가져오는데 실패했습니다. 채널 아이디={{.ChannelId}}" }, { "id": "api.post.make_direct_channel_visible.get_members.error", @@ -3636,7 +3636,7 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "{{.SubjectText}} on {{.TeamDisplayName}} at {{.Month}} {{.Day}}, {{.Year}}" + "translation": "[{{.SiteName}}] {{.SenderDisplayName}} (으)로부터 {{.Month}} {{.Day}}, {{.Year}} 에 새로운 개인 메시지가 왔습니다." }, { "id": "app.notification.subject.notification.full", @@ -3712,7 +3712,7 @@ }, { "id": "app.user_access_token.disabled", - "translation": "Personal access tokens are disabled on this server. Please contact your system administrator for details." + "translation": "개인 액세스 토큰이 현재 서버에서 활성화 되어있지 않습니다. 시스템 관리자에게 연락하여 자세한 사항을 확인하시길 바랍니다." }, { "id": "app.user_access_token.invalid_or_missing", diff --git a/i18n/nl.json b/i18n/nl.json index dda0ebab9..1777b59d1 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -3636,7 +3636,7 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "{{.SubjectText}} in {{.TeamDisplayName}} van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}" + "translation": "[{{.SiteName}}] Nieuw direct bericht van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}" }, { "id": "app.notification.subject.notification.full", diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 44400b8c0..000d87a0c 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -1370,7 +1370,7 @@ }, { "id": "api.file.upload_file.incorrect_number_of_files.app_error", - "translation": "Unable to upload files. Incorrect number of files specified." + "translation": "Não é possível enviar arquivos. Número incorreto de arquivos especificado." }, { "id": "api.file.upload_file.large_image.app_error", @@ -1820,7 +1820,7 @@ }, { "id": "api.post.send_notifications_and_forget.push_image_only_no_channel", - "translation": " enviado um ou mais arquivos em " + "translation": " enviado um ou mais arquivos" }, { "id": "api.post.send_notifications_and_forget.push_in", @@ -3080,7 +3080,7 @@ }, { "id": "api.webhook.incoming.error", - "translation": "Could not decode the multipart payload of incoming webhook." + "translation": "Não foi possível decodificar a carga multiparte do webhook de entrada." }, { "id": "api.webhook.init.debug", @@ -3656,7 +3656,7 @@ }, { "id": "app.plugin.disabled.app_error", - "translation": "Plugins have been disabled. Please check your logs for details." + "translation": "Os plugins foram desativados. Verifique os seus logs para obter detalhes." }, { "id": "app.plugin.extract.app_error", @@ -4192,15 +4192,15 @@ }, { "id": "ent.migration.migratetosaml.email_already_used_by_other_user", - "translation": "Email already used by another SAML user." + "translation": "Email já usado por outro usuário SAML." }, { "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", - "translation": "User not found in the users file." + "translation": "Usuário não encontrado no arquivo de usuários." }, { "id": "ent.migration.migratetosaml.username_already_used_by_other_user", - "translation": "Username already used by another Mattermost user." + "translation": "Nome de usuário já usado por outro usuário Mattermost." }, { "id": "ent.saml.attribute.app_error", @@ -4904,7 +4904,7 @@ }, { "id": "model.config.is_valid.message_export.export_type.app_error", - "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + "translation": "Na tarefa de exportação de mensagens o ExportFormat deve ser 'actiance' ou 'globalrelay'" }, { "id": "model.config.is_valid.message_export.file_location.app_error", @@ -4916,7 +4916,7 @@ }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", - "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" + "translation": "Na tarefa de exportação de mensagens o GlobalRelayEmailAddress deve ser um endereço de email válido" }, { "id": "model.config.is_valid.password_length.app_error", @@ -5048,7 +5048,7 @@ }, { "id": "model.config.is_valid.websocket_url.app_error", - "translation": "A URL Websocket do WebRTC Gateway deve ser uma URL válida e começar com ws:// ou wss://." + "translation": "A URL Websocket deve ser uma URL válida e começar com ws:// ou wss://." }, { "id": "model.config.is_valid.write_timeout.app_error", @@ -7136,7 +7136,7 @@ }, { "id": "utils.mail.sendMail.attachments.write_error", - "translation": "Failed to write attachment to email" + "translation": "Falha ao escrever o anexo para o e-mail" }, { "id": "utils.mail.send_mail.close.app_error", diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index f476b48ed..43d3fde87 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -2308,11 +2308,11 @@ }, { "id": "api.team.move_channel.post.error", - "translation": "发送频道作用消息失败" + "translation": "发送频道移动消息失败。" }, { "id": "api.team.move_channel.success", - "translation": "This channel has been moved to this team from %v." + "translation": "此频道已从 %v 移至此团队。" }, { "id": "api.team.permanent_delete_team.attempting.warn", @@ -3080,7 +3080,7 @@ }, { "id": "api.webhook.incoming.error", - "translation": "Could not decode the multipart payload of incoming webhook." + "translation": "无法解码传入的 webhook 混合数据。" }, { "id": "api.webhook.init.debug", @@ -4904,7 +4904,7 @@ }, { "id": "model.config.is_valid.message_export.export_type.app_error", - "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + "translation": "消息导出任务 ExportFormat 必须为 'actiance' 或 'globalrelay'" }, { "id": "model.config.is_valid.message_export.file_location.app_error", @@ -4916,7 +4916,7 @@ }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", - "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" + "translation": "消息导出任务 GlobalRelayEmailAddress 必须为有效的电子邮箱地址" }, { "id": "model.config.is_valid.password_length.app_error", diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index b9edc5edf..0700a15a5 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -1370,7 +1370,7 @@ }, { "id": "api.file.upload_file.incorrect_number_of_files.app_error", - "translation": "Unable to upload files. Incorrect number of files specified." + "translation": "無法上傳檔案。檔案數量不對。" }, { "id": "api.file.upload_file.large_image.app_error", @@ -1812,7 +1812,7 @@ }, { "id": "api.post.send_notifications_and_forget.push_image_only", - "translation": "已上傳一個或更多檔案" + "translation": "已上傳一個或更多檔案至" }, { "id": "api.post.send_notifications_and_forget.push_image_only_dm", @@ -2308,11 +2308,11 @@ }, { "id": "api.team.move_channel.post.error", - "translation": "發送頻道用途訊息失敗" + "translation": "發送頻道移動訊息失敗" }, { "id": "api.team.move_channel.success", - "translation": "This channel has been moved to this team from %v." + "translation": "此頻道已從 %v 移動至此團隊。" }, { "id": "api.team.permanent_delete_team.attempting.warn", @@ -3080,7 +3080,7 @@ }, { "id": "api.webhook.incoming.error", - "translation": "Could not decode the multipart payload of incoming webhook." + "translation": "無法解碼 Incoming Webhook 的 multipart 內容。" }, { "id": "api.webhook.init.debug", @@ -3656,7 +3656,7 @@ }, { "id": "app.plugin.disabled.app_error", - "translation": "Plugins have been disabled. Please check your logs for details." + "translation": "模組已被停用。詳情請看系統紀錄。" }, { "id": "app.plugin.extract.app_error", @@ -4192,15 +4192,15 @@ }, { "id": "ent.migration.migratetosaml.email_already_used_by_other_user", - "translation": "Email already used by another SAML user." + "translation": "電子郵件已被其他 SAML 使用者使用。" }, { "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", - "translation": "User not found in the users file." + "translation": "在使用者檔案中找不到使用者。" }, { "id": "ent.migration.migratetosaml.username_already_used_by_other_user", - "translation": "Username already used by another Mattermost user." + "translation": "使用者名稱已被其他 Mattermost 使用者使用。 " }, { "id": "ent.saml.attribute.app_error", @@ -4904,7 +4904,7 @@ }, { "id": "model.config.is_valid.message_export.export_type.app_error", - "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + "translation": "訊息匯出工作的 ExportFormat 必須為 'actiance' 或 'globalrelay'" }, { "id": "model.config.is_valid.message_export.file_location.app_error", @@ -4916,7 +4916,7 @@ }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", - "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" + "translation": "訊息匯出工作的 GlobalRelayEmailAddress 必須為 'actiance' 或 'globalrelay'" }, { "id": "model.config.is_valid.password_length.app_error", @@ -5048,7 +5048,7 @@ }, { "id": "model.config.is_valid.websocket_url.app_error", - "translation": "WebRTC 閘道 Websocket 網址必須是以 ws:// 或 wss:// 起始的有效網址。" + "translation": "Websocket 網址必須是以 ws:// 或 wss:// 起始的有效網址。" }, { "id": "model.config.is_valid.write_timeout.app_error", @@ -7136,7 +7136,7 @@ }, { "id": "utils.mail.sendMail.attachments.write_error", - "translation": "Failed to write attachment to email" + "translation": "無法寫入附加檔案到電子郵件" }, { "id": "utils.mail.send_mail.close.app_error", diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 412914738..000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,11 +0,0 @@ -site_name: Mattermost Documentation -site_url: http://docs.mattermost.org -repo_url: https://github.com/mattermost/platform -repo_name: GitHub -site_favicon: favicon.ico -copyright: "Copyright (c) 2015-2017 Mattermost, Inc. All Rights Reserved." -strict: true -docs_dir: doc -site_dir: documentation-html -use_directory_urls: false -theme: mkdocs diff --git a/model/client4.go b/model/client4.go index f12e4712b..693008734 100644 --- a/model/client4.go +++ b/model/client4.go @@ -2102,8 +2102,8 @@ func (c *Client4) GetPing() (string, *Response) { } // TestEmail will attempt to connect to the configured SMTP server. -func (c *Client4) TestEmail() (bool, *Response) { - if r, err := c.DoApiPost(c.GetTestEmailRoute(), ""); err != nil { +func (c *Client4) TestEmail(config *Config) (bool, *Response) { + if r, err := c.DoApiPost(c.GetTestEmailRoute(), config.ToJson()); err != nil { return false, BuildErrorResponse(r, err) } else { defer closeBody(r) diff --git a/plugin/rpcplugin/sandbox/sandbox_linux.go b/plugin/rpcplugin/sandbox/sandbox_linux.go index 4ade00cf2..beb00995d 100644 --- a/plugin/rpcplugin/sandbox/sandbox_linux.go +++ b/plugin/rpcplugin/sandbox/sandbox_linux.go @@ -23,7 +23,7 @@ import ( ) func init() { - if len(os.Args) < 3 || os.Args[0] != "sandbox.runProcess" { + if len(os.Args) < 4 || os.Args[0] != "sandbox.runProcess" { return } @@ -32,7 +32,7 @@ func init() { fmt.Println(err.Error()) os.Exit(1) } - if err := runProcess(&config, os.Args[2]); err != nil { + if err := runProcess(&config, os.Args[2], os.Args[3]); err != nil { if eerr, ok := err.(*exec.ExitError); ok { if status, ok := eerr.Sys().(syscall.WaitStatus); ok { os.Exit(status.ExitStatus()) @@ -98,13 +98,7 @@ func systemMountPoints() (points []*MountPoint) { return } -func runProcess(config *Configuration, path string) error { - root, err := ioutil.TempDir("", "") - if err != nil { - return err - } - defer os.RemoveAll(root) - +func runProcess(config *Configuration, path, root string) error { if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil { return errors.Wrapf(err, "unable to make root private") } @@ -330,9 +324,10 @@ func runExecutable(path string) error { type process struct { command *exec.Cmd + root string } -func newProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) { +func newProcess(ctx context.Context, config *Configuration, path string) (pOut rpcplugin.Process, rwcOut io.ReadWriteCloser, errOut error) { configJSON, err := json.Marshal(config) if err != nil { return nil, nil, err @@ -345,8 +340,18 @@ func newProcess(ctx context.Context, config *Configuration, path string) (rpcplu defer childFiles[0].Close() defer childFiles[1].Close() + root, err := ioutil.TempDir("", "") + if err != nil { + return nil, nil, err + } + defer func() { + if errOut != nil { + os.RemoveAll(root) + } + }() + cmd := exec.CommandContext(ctx, "/proc/self/exe") - cmd.Args = []string{"sandbox.runProcess", string(configJSON), path} + cmd.Args = []string{"sandbox.runProcess", string(configJSON), path, root} cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.ExtraFiles = childFiles @@ -378,19 +383,21 @@ func newProcess(ctx context.Context, config *Configuration, path string) (rpcplu return &process{ command: cmd, + root: root, }, ipc, nil } func (p *process) Wait() error { + defer os.RemoveAll(p.root) return p.command.Wait() } func init() { - if len(os.Args) < 1 || os.Args[0] != "sandbox.checkSupportInNamespace" { + if len(os.Args) < 2 || os.Args[0] != "sandbox.checkSupportInNamespace" { return } - if err := checkSupportInNamespace(); err != nil { + if err := checkSupportInNamespace(os.Args[1]); err != nil { fmt.Fprintf(os.Stderr, "%v", err.Error()) os.Exit(1) } @@ -398,13 +405,7 @@ func init() { os.Exit(0) } -func checkSupportInNamespace() error { - root, err := ioutil.TempDir("", "") - if err != nil { - return err - } - defer os.RemoveAll(root) - +func checkSupportInNamespace(root string) error { if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil { return errors.Wrapf(err, "unable to make root private") } @@ -444,8 +445,14 @@ func checkSupport() error { stderr := &bytes.Buffer{} + root, err := ioutil.TempDir("", "") + if err != nil { + return err + } + defer os.RemoveAll(root) + cmd := exec.Command("/proc/self/exe") - cmd.Args = []string{"sandbox.checkSupportInNamespace"} + cmd.Args = []string{"sandbox.checkSupportInNamespace", root} cmd.Stderr = stderr cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER, diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index 92ee28ffa..3ff9a3e1b 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -687,31 +687,70 @@ func (s SqlPostStore) getRootPosts(channelId string, offset int, limit int) stor func (s SqlPostStore) getParentsPosts(channelId string, offset int, limit int) store.StoreChannel { return store.Do(func(result *store.StoreResult) { var posts []*model.Post - _, err := s.GetReplica().Select(&posts, - `SELECT - q2.* + _, err := s.GetReplica().Select(&posts, ` + SELECT + * FROM - Posts q2 - INNER JOIN - (SELECT DISTINCT - q3.RootId - FROM - (SELECT - RootId - FROM - Posts - WHERE - ChannelId = :ChannelId1 - AND DeleteAt = 0 - ORDER BY CreateAt DESC - LIMIT :Limit OFFSET :Offset) q3 - WHERE q3.RootId != '') q1 - ON q1.RootId = q2.Id OR q1.RootId = q2.RootId + Posts WHERE - ChannelId = :ChannelId2 - AND DeleteAt = 0 - ORDER BY CreateAt`, - map[string]interface{}{"ChannelId1": channelId, "Offset": offset, "Limit": limit, "ChannelId2": channelId}) + Id IN (SELECT * FROM ( + -- The root post of any replies in the window + (SELECT * FROM ( + SELECT + CASE RootId + WHEN '' THEN NULL + ELSE RootId + END + FROM + Posts + WHERE + ChannelId = :ChannelId1 + AND DeleteAt = 0 + ORDER BY + CreateAt DESC + LIMIT :Limit1 OFFSET :Offset1 + ) x ) + + UNION + + -- The reply posts to all threads intersecting with the window, including replies + -- to root posts in the window itself. + ( + SELECT + Id + FROM + Posts + WHERE RootId IN (SELECT * FROM ( + SELECT + CASE RootId + -- If there is no RootId, return the post id itself to be considered + -- as a root post. + WHEN '' THEN Id + -- If there is a RootId, this post isn't a root post and return its + -- root to be considered as a root post. + ELSE RootId + END + FROM + Posts + WHERE + ChannelId = :ChannelId2 + AND DeleteAt = 0 + ORDER BY + CreateAt DESC + LIMIT :Limit2 OFFSET :Offset2 + ) x ) + ) + ) x ) + AND + DeleteAt = 0 + `, map[string]interface{}{ + "ChannelId1": channelId, + "ChannelId2": channelId, + "Offset1": offset, + "Offset2": offset, + "Limit1": limit, + "Limit2": limit, + }) if err != nil { result.Err = model.NewAppError("SqlPostStore.GetLinearPosts", "store.sql_post.get_parents_posts.app_error", nil, "channelId="+channelId+" err="+err.Error(), http.StatusInternalServerError) } else { diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index e663d5a41..91fc40213 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -5,6 +5,7 @@ package storetest import ( "fmt" + "sort" "strings" "testing" "time" @@ -491,125 +492,182 @@ func testPostStoreGetWithChildren(t *testing.T, ss store.Store) { } func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) { - o1 := &model.Post{} - o1.ChannelId = model.NewId() - o1.UserId = model.NewId() - o1.Message = "zz" + model.NewId() + "b" - o1 = (<-ss.Post().Save(o1)).Data.(*model.Post) - time.Sleep(2 * time.Millisecond) + assertPosts := func(expected []*model.Post, actual map[string]*model.Post) { + expectedIds := make([]string, 0, len(expected)) + expectedMessages := make([]string, 0, len(expected)) + for _, post := range expected { + expectedIds = append(expectedIds, post.Id) + expectedMessages = append(expectedMessages, post.Message) + } + sort.Strings(expectedIds) + sort.Strings(expectedMessages) + + actualIds := make([]string, 0, len(actual)) + actualMessages := make([]string, 0, len(actual)) + for _, post := range actual { + actualIds = append(actualIds, post.Id) + actualMessages = append(actualMessages, post.Message) + } + sort.Strings(actualIds) + sort.Strings(actualMessages) - o2 := &model.Post{} - o2.ChannelId = o1.ChannelId - o2.UserId = model.NewId() - o2.Message = "zz" + model.NewId() + "b" - o2.ParentId = o1.Id - o2.RootId = o1.Id - o2 = (<-ss.Post().Save(o2)).Data.(*model.Post) - time.Sleep(2 * time.Millisecond) + if assert.Equal(t, expectedIds, actualIds) { + assert.Equal(t, expectedMessages, actualMessages) + } + } - o2a := &model.Post{} - o2a.ChannelId = o1.ChannelId - o2a.UserId = model.NewId() - o2a.Message = "zz" + model.NewId() + "b" - o2a.ParentId = o1.Id - o2a.RootId = o1.Id - o2a = (<-ss.Post().Save(o2a)).Data.(*model.Post) + root1 := &model.Post{} + root1.ChannelId = model.NewId() + root1.UserId = model.NewId() + root1.Message = "zz" + model.NewId() + "b" + root1 = (<-ss.Post().Save(root1)).Data.(*model.Post) time.Sleep(2 * time.Millisecond) - o3 := &model.Post{} - o3.ChannelId = o1.ChannelId - o3.UserId = model.NewId() - o3.Message = "zz" + model.NewId() + "b" - o3.ParentId = o1.Id - o3.RootId = o1.Id - o3 = (<-ss.Post().Save(o3)).Data.(*model.Post) + root1Reply1 := &model.Post{} + root1Reply1.ChannelId = root1.ChannelId + root1Reply1.UserId = model.NewId() + root1Reply1.Message = "zz" + model.NewId() + "b" + root1Reply1.ParentId = root1.Id + root1Reply1.RootId = root1.Id + root1Reply1 = (<-ss.Post().Save(root1Reply1)).Data.(*model.Post) time.Sleep(2 * time.Millisecond) - o4 := &model.Post{} - o4.ChannelId = o1.ChannelId - o4.UserId = model.NewId() - o4.Message = "zz" + model.NewId() + "b" - o4 = (<-ss.Post().Save(o4)).Data.(*model.Post) + root1Reply2 := &model.Post{} + root1Reply2.ChannelId = root1.ChannelId + root1Reply2.UserId = model.NewId() + root1Reply2.Message = "zz" + model.NewId() + "b" + root1Reply2.ParentId = root1.Id + root1Reply2.RootId = root1.Id + root1Reply2 = (<-ss.Post().Save(root1Reply2)).Data.(*model.Post) time.Sleep(2 * time.Millisecond) - o5 := &model.Post{} - o5.ChannelId = o1.ChannelId - o5.UserId = model.NewId() - o5.Message = "zz" + model.NewId() + "b" - o5.ParentId = o4.Id - o5.RootId = o4.Id - o5 = (<-ss.Post().Save(o5)).Data.(*model.Post) - - r1 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 4, false)).Data.(*model.PostList) - - if r1.Order[0] != o5.Id { - t.Fatal("invalid order") - } - - if r1.Order[1] != o4.Id { - t.Fatal("invalid order") - } - - if r1.Order[2] != o3.Id { - t.Fatal("invalid order") - } - - if r1.Order[3] != o2a.Id { - t.Fatal("invalid order") - } - - if len(r1.Posts) != 6 { //the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3) - t.Fatal("wrong size") - } - - if r1.Posts[o1.Id].Message != o1.Message { - t.Fatal("Missing parent") - } + root1Reply3 := &model.Post{} + root1Reply3.ChannelId = root1.ChannelId + root1Reply3.UserId = model.NewId() + root1Reply3.Message = "zz" + model.NewId() + "b" + root1Reply3.ParentId = root1.Id + root1Reply3.RootId = root1.Id + root1Reply3 = (<-ss.Post().Save(root1Reply3)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) - r2 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 4, true)).Data.(*model.PostList) + root2 := &model.Post{} + root2.ChannelId = root1.ChannelId + root2.UserId = model.NewId() + root2.Message = "zz" + model.NewId() + "b" + root2 = (<-ss.Post().Save(root2)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) - if r2.Order[0] != o5.Id { - t.Fatal("invalid order") - } + root2Reply1 := &model.Post{} + root2Reply1.ChannelId = root1.ChannelId + root2Reply1.UserId = model.NewId() + root2Reply1.Message = "zz" + model.NewId() + "b" + root2Reply1.ParentId = root2.Id + root2Reply1.RootId = root2.Id + root2Reply1 = (<-ss.Post().Save(root2Reply1)).Data.(*model.Post) - if r2.Order[1] != o4.Id { - t.Fatal("invalid order") - } + r1 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 4, false)).Data.(*model.PostList) - if r2.Order[2] != o3.Id { - t.Fatal("invalid order") + expectedOrder := []string{ + root2Reply1.Id, + root2.Id, + root1Reply3.Id, + root1Reply2.Id, } - if r2.Order[3] != o2a.Id { - t.Fatal("invalid order") + expectedPosts := []*model.Post{ + root1, + root1Reply1, + root1Reply2, + root1Reply3, + root2, + root2Reply1, } - if len(r2.Posts) != 6 { //the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3) - t.Fatal("wrong size") - } + assert.Equal(t, expectedOrder, r1.Order) + assertPosts(expectedPosts, r1.Posts) - if r2.Posts[o1.Id].Message != o1.Message { - t.Fatal("Missing parent") - } + r2 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 4, true)).Data.(*model.PostList) + assert.Equal(t, expectedOrder, r2.Order) + assertPosts(expectedPosts, r2.Posts) // Run once to fill cache - <-ss.Post().GetPosts(o1.ChannelId, 0, 30, true) + <-ss.Post().GetPosts(root1.ChannelId, 0, 30, true) + expectedOrder = []string{ + root2Reply1.Id, + root2.Id, + root1Reply3.Id, + root1Reply2.Id, + root1Reply1.Id, + root1.Id, + } - o6 := &model.Post{} - o6.ChannelId = o1.ChannelId - o6.UserId = model.NewId() - o6.Message = "zz" + model.NewId() + "b" - o6 = (<-ss.Post().Save(o6)).Data.(*model.Post) + root3 := &model.Post{} + root3.ChannelId = root1.ChannelId + root3.UserId = model.NewId() + root3.Message = "zz" + model.NewId() + "b" + root3 = (<-ss.Post().Save(root3)).Data.(*model.Post) - // Should only be 6 since we hit the cache - r3 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 30, true)).Data.(*model.PostList) - assert.Equal(t, 6, len(r3.Order)) + // Response should be the same despite the new post since we hit the cache + r3 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 30, true)).Data.(*model.PostList) + assert.Equal(t, expectedOrder, r3.Order) + assertPosts(expectedPosts, r3.Posts) - ss.Post().InvalidateLastPostTimeCache(o1.ChannelId) + ss.Post().InvalidateLastPostTimeCache(root1.ChannelId) // Cache was invalidated, we should get all the posts - r4 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 30, true)).Data.(*model.PostList) - assert.Equal(t, 7, len(r4.Order)) + r4 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 30, true)).Data.(*model.PostList) + expectedOrder = []string{ + root3.Id, + root2Reply1.Id, + root2.Id, + root1Reply3.Id, + root1Reply2.Id, + root1Reply1.Id, + root1.Id, + } + expectedPosts = []*model.Post{ + root1, + root1Reply1, + root1Reply2, + root1Reply3, + root2, + root2Reply1, + root3, + } + + assert.Equal(t, expectedOrder, r4.Order) + assertPosts(expectedPosts, r4.Posts) + + // Replies past the window should be included if the root post itself is in the window + root3Reply1 := &model.Post{} + root3Reply1.ChannelId = root1.ChannelId + root3Reply1.UserId = model.NewId() + root3Reply1.Message = "zz" + model.NewId() + "b" + root3Reply1.ParentId = root3.Id + root3Reply1.RootId = root3.Id + root3Reply1 = (<-ss.Post().Save(root3Reply1)).Data.(*model.Post) + + r5 := (<-ss.Post().GetPosts(root1.ChannelId, 1, 5, false)).Data.(*model.PostList) + expectedOrder = []string{ + root3.Id, + root2Reply1.Id, + root2.Id, + root1Reply3.Id, + root1Reply2.Id, + } + expectedPosts = []*model.Post{ + root1, + root1Reply1, + root1Reply2, + root1Reply3, + root2, + root2Reply1, + root3, + root3Reply1, + } + + assert.Equal(t, expectedOrder, r5.Order) + assertPosts(expectedPosts, r5.Posts) } func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { diff --git a/utils/api.go b/utils/api.go index 0f2640829..b5e490eb7 100644 --- a/utils/api.go +++ b/utils/api.go @@ -59,6 +59,7 @@ func RenderWebError(w http.ResponseWriter, r *http.Request, status int, params u return } + w.Header().Set("Content-Type", "text/html") w.WriteHeader(status) fmt.Fprintln(w, `<!DOCTYPE html><html><head></head>`) fmt.Fprintln(w, `<body onload="window.location = '`+template.HTMLEscapeString(template.JSEscapeString(destination))+`'">`) diff --git a/utils/config.go b/utils/config.go index 8e9bafc6e..8befef94d 100644 --- a/utils/config.go +++ b/utils/config.go @@ -449,6 +449,9 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != "" props["HasImageProxy"] = strconv.FormatBool(hasImageProxy) + props["EnableThemeSelection"] = "true" + props["AllowCustomThemes"] = "true" + if license != nil { props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly) props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer) diff --git a/utils/file_backend_s3.go b/utils/file_backend_s3.go index b0601bc8a..75282897f 100644 --- a/utils/file_backend_s3.go +++ b/utils/file_backend_s3.go @@ -253,12 +253,9 @@ func CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError { return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest) } + // if S3 endpoint is not set call the set defaults to set that if len(settings.AmazonS3Endpoint) == 0 { - return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_endpoint", nil, "", http.StatusBadRequest) - } - - if len(settings.AmazonS3Region) == 0 { - return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_region", nil, "", http.StatusBadRequest) + settings.SetDefaults() } return nil diff --git a/utils/file_backend_s3_test.go b/utils/file_backend_s3_test.go index ff42a4d19..a8834f226 100644 --- a/utils/file_backend_s3_test.go +++ b/utils/file_backend_s3_test.go @@ -19,14 +19,14 @@ func TestCheckMandatoryS3Fields(t *testing.T) { cfg.AmazonS3Bucket = "test-mm" err = CheckMandatoryS3Fields(&cfg) - if err == nil || err.Message != "api.admin.test_s3.missing_s3_endpoint" { - t.Fatal("should've failed with missing s3 endpoint") + if err != nil { + t.Fatal("should've not failed") } - cfg.AmazonS3Endpoint = "s3.newendpoint.com" + cfg.AmazonS3Endpoint = "" err = CheckMandatoryS3Fields(&cfg) - if err == nil || err.Message != "api.admin.test_s3.missing_s3_region" { - t.Fatal("should've failed with missing s3 region") + if err != nil || cfg.AmazonS3Endpoint != "s3.amazonaws.com" { + t.Fatal("should've not failed because it should set the endpoint to the default") } } diff --git a/utils/mail.go b/utils/mail.go index 3b9f4bd9d..c59406a18 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -26,16 +26,27 @@ func encodeRFC2047Word(s string) string { return mime.BEncoding.Encode("utf-8", s) } +type SmtpConnectionInfo struct { + SmtpUsername string + SmtpPassword string + SmtpServer string + SmtpPort string + SkipCertVerification bool + ConnectionSecurity string + Auth bool +} + type authChooser struct { smtp.Auth - Config *model.Config + connectionInfo *SmtpConnectionInfo } func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) { - a.Auth = LoginAuth(a.Config.EmailSettings.SMTPUsername, a.Config.EmailSettings.SMTPPassword, a.Config.EmailSettings.SMTPServer+":"+a.Config.EmailSettings.SMTPPort) + smtpAddress := a.connectionInfo.SmtpServer + ":" + a.connectionInfo.SmtpPort + a.Auth = LoginAuth(a.connectionInfo.SmtpUsername, a.connectionInfo.SmtpPassword, smtpAddress) for _, method := range server.Auth { if method == "PLAIN" { - a.Auth = smtp.PlainAuth("", a.Config.EmailSettings.SMTPUsername, a.Config.EmailSettings.SMTPPassword, a.Config.EmailSettings.SMTPServer+":"+a.Config.EmailSettings.SMTPPort) + a.Auth = smtp.PlainAuth("", a.connectionInfo.SmtpUsername, a.connectionInfo.SmtpPassword, a.connectionInfo.SmtpServer+":"+a.connectionInfo.SmtpPort) break } } @@ -76,22 +87,23 @@ func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { return nil, nil } -func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { +func ConnectToSMTPServerAdvanced(connectionInfo *SmtpConnectionInfo) (net.Conn, *model.AppError) { var conn net.Conn var err error - if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS { + smtpAddress := connectionInfo.SmtpServer + ":" + connectionInfo.SmtpPort + if connectionInfo.ConnectionSecurity == model.CONN_SECURITY_TLS { tlsconfig := &tls.Config{ - InsecureSkipVerify: *config.EmailSettings.SkipServerCertificateVerification, - ServerName: config.EmailSettings.SMTPServer, + InsecureSkipVerify: connectionInfo.SkipCertVerification, + ServerName: connectionInfo.SmtpServer, } - conn, err = tls.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort, tlsconfig) + conn, err = tls.Dial("tcp", smtpAddress, tlsconfig) if err != nil { return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError) } } else { - conn, err = net.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort) + conn, err = net.Dial("tcp", smtpAddress) if err != nil { return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -100,14 +112,24 @@ func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { return conn, nil } -func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) { - c, err := smtp.NewClient(conn, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort) +func ConnectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { + return ConnectToSMTPServerAdvanced( + &SmtpConnectionInfo{ + ConnectionSecurity: config.EmailSettings.ConnectionSecurity, + SkipCertVerification: *config.EmailSettings.SkipServerCertificateVerification, + SmtpServer: config.EmailSettings.SMTPServer, + SmtpPort: config.EmailSettings.SMTPPort, + }, + ) +} + +func NewSMTPClientAdvanced(conn net.Conn, hostname string, connectionInfo *SmtpConnectionInfo) (*smtp.Client, *model.AppError) { + c, err := smtp.NewClient(conn, connectionInfo.SmtpServer+":"+connectionInfo.SmtpPort) if err != nil { l4g.Error(T("utils.mail.new_client.open.error"), err) return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError) } - hostname := GetHostnameFromSiteURL(*config.ServiceSettings.SiteURL) if hostname != "" { err := c.Hello(hostname) if err != nil { @@ -116,35 +138,51 @@ func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.Ap } } - if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_STARTTLS { + if connectionInfo.ConnectionSecurity == model.CONN_SECURITY_STARTTLS { tlsconfig := &tls.Config{ - InsecureSkipVerify: *config.EmailSettings.SkipServerCertificateVerification, - ServerName: config.EmailSettings.SMTPServer, + InsecureSkipVerify: connectionInfo.SkipCertVerification, + ServerName: connectionInfo.SmtpServer, } c.StartTLS(tlsconfig) } - if *config.EmailSettings.EnableSMTPAuth { - if err = c.Auth(&authChooser{Config: config}); err != nil { + if connectionInfo.Auth { + if err = c.Auth(&authChooser{connectionInfo: connectionInfo}); err != nil { return nil, model.NewAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error(), http.StatusInternalServerError) } } return c, nil } +func NewSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) { + return NewSMTPClientAdvanced( + conn, + GetHostnameFromSiteURL(*config.ServiceSettings.SiteURL), + &SmtpConnectionInfo{ + ConnectionSecurity: config.EmailSettings.ConnectionSecurity, + SkipCertVerification: *config.EmailSettings.SkipServerCertificateVerification, + SmtpServer: config.EmailSettings.SMTPServer, + SmtpPort: config.EmailSettings.SMTPPort, + Auth: *config.EmailSettings.EnableSMTPAuth, + SmtpUsername: config.EmailSettings.SMTPUsername, + SmtpPassword: config.EmailSettings.SMTPPassword, + }, + ) +} + func TestConnection(config *model.Config) { if !config.EmailSettings.SendEmailNotifications { return } - conn, err1 := connectToSMTPServer(config) + conn, err1 := ConnectToSMTPServer(config) if err1 != nil { l4g.Error(T("utils.mail.test.configured.error"), T(err1.Message), err1.DetailedError) return } defer conn.Close() - c, err2 := newSMTPClient(conn, config) + c, err2 := NewSMTPClient(conn, config) if err2 != nil { l4g.Error(T("utils.mail.test.configured.error"), T(err2.Message), err2.DetailedError) return @@ -155,19 +193,38 @@ func TestConnection(config *model.Config) { func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool) *model.AppError { fromMail := mail.Address{Name: config.EmailSettings.FeedbackName, Address: config.EmailSettings.FeedbackEmail} - return sendMail(to, to, fromMail, subject, htmlBody, nil, nil, config, enableComplianceFeatures) + + return SendMailUsingConfigAdvanced(to, to, fromMail, subject, htmlBody, nil, nil, config, enableComplianceFeatures) } // allows for sending an email with attachments and differing MIME/SMTP recipients func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config, enableComplianceFeatures bool) *model.AppError { - return sendMail(mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, config, enableComplianceFeatures) -} - -func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config, enableComplianceFeatures bool) *model.AppError { if !config.EmailSettings.SendEmailNotifications || len(config.EmailSettings.SMTPServer) == 0 { return nil } + conn, err := ConnectToSMTPServer(config) + if err != nil { + return err + } + defer conn.Close() + + c, err := NewSMTPClient(conn, config) + if err != nil { + return err + } + defer c.Quit() + defer c.Close() + + fileBackend, err := NewFileBackend(&config.FileSettings, enableComplianceFeatures) + if err != nil { + return err + } + + return SendMail(c, mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, fileBackend) +} + +func SendMail(c *smtp.Client, mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, fileBackend FileBackend) *model.AppError { l4g.Debug(T("utils.mail.send_mail.sending.debug"), mimeTo, subject) htmlMessage := "\r\n<html><body>" + htmlBody + "</body></html>" @@ -197,11 +254,6 @@ func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string m.AddAlternative("text/html", htmlMessage) if attachments != nil { - fileBackend, err := NewFileBackend(&config.FileSettings, enableComplianceFeatures) - if err != nil { - return err - } - for _, fileInfo := range attachments { bytes, err := fileBackend.ReadFile(fileInfo.Path) if err != nil { @@ -217,19 +269,6 @@ func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string } } - conn, err1 := connectToSMTPServer(config) - if err1 != nil { - return err1 - } - defer conn.Close() - - c, err2 := newSMTPClient(conn, config) - if err2 != nil { - return err2 - } - defer c.Quit() - defer c.Close() - if err := c.Mail(from.Address); err != nil { return model.NewAppError("SendMail", "utils.mail.send_mail.from_address.app_error", nil, err.Error(), http.StatusInternalServerError) } diff --git a/utils/mail_test.go b/utils/mail_test.go index 31a4f8996..50cf09dac 100644 --- a/utils/mail_test.go +++ b/utils/mail_test.go @@ -4,24 +4,27 @@ package utils import ( + "fmt" "strings" "testing" + "net/mail" "net/smtp" "github.com/mattermost/mattermost-server/model" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestMailConnection(t *testing.T) { +func TestMailConnectionFromConfig(t *testing.T) { cfg, _, err := LoadConfig("config.json") require.Nil(t, err) - if conn, err := connectToSMTPServer(cfg); err != nil { + if conn, err := ConnectToSMTPServer(cfg); err != nil { t.Log(err) t.Fatal("Should connect to the STMP Server") } else { - if _, err1 := newSMTPClient(conn, cfg); err1 != nil { + if _, err1 := NewSMTPClient(conn, cfg); err1 != nil { t.Log(err) t.Fatal("Should get new smtp client") } @@ -30,7 +33,53 @@ func TestMailConnection(t *testing.T) { cfg.EmailSettings.SMTPServer = "wrongServer" cfg.EmailSettings.SMTPPort = "553" - if _, err := connectToSMTPServer(cfg); err == nil { + if _, err := ConnectToSMTPServer(cfg); err == nil { + t.Log(err) + t.Fatal("Should not to the STMP Server") + } +} + +func TestMailConnectionAdvanced(t *testing.T) { + cfg, _, err := LoadConfig("config.json") + require.Nil(t, err) + + if conn, err := ConnectToSMTPServerAdvanced( + &SmtpConnectionInfo{ + ConnectionSecurity: cfg.EmailSettings.ConnectionSecurity, + SkipCertVerification: *cfg.EmailSettings.SkipServerCertificateVerification, + SmtpServer: cfg.EmailSettings.SMTPServer, + SmtpPort: cfg.EmailSettings.SMTPPort, + }, + ); err != nil { + t.Log(err) + t.Fatal("Should connect to the STMP Server") + } else { + if _, err1 := NewSMTPClientAdvanced( + conn, + GetHostnameFromSiteURL(*cfg.ServiceSettings.SiteURL), + &SmtpConnectionInfo{ + ConnectionSecurity: cfg.EmailSettings.ConnectionSecurity, + SkipCertVerification: *cfg.EmailSettings.SkipServerCertificateVerification, + SmtpServer: cfg.EmailSettings.SMTPServer, + SmtpPort: cfg.EmailSettings.SMTPPort, + Auth: *cfg.EmailSettings.EnableSMTPAuth, + SmtpUsername: cfg.EmailSettings.SMTPUsername, + SmtpPassword: cfg.EmailSettings.SMTPPassword, + }, + ); err1 != nil { + t.Log(err) + t.Fatal("Should get new smtp client") + } + } + + if _, err := ConnectToSMTPServerAdvanced( + &SmtpConnectionInfo{ + ConnectionSecurity: cfg.EmailSettings.ConnectionSecurity, + SkipCertVerification: *cfg.EmailSettings.SkipServerCertificateVerification, + SmtpServer: "wrongServer", + SmtpPort: "553", + }, + ); err == nil { t.Log(err) t.Fatal("Should not to the STMP Server") } @@ -79,7 +128,7 @@ func TestSendMailUsingConfig(t *testing.T) { } } -/*func TestSendMailUsingConfigAdvanced(t *testing.T) { +func TestSendMailUsingConfigAdvanced(t *testing.T) { cfg, _, err := LoadConfig("config.json") require.Nil(t, err) T = GetUserTranslations("en") @@ -171,20 +220,17 @@ func TestSendMailUsingConfig(t *testing.T) { } } } -}*/ +} func TestAuthMethods(t *testing.T) { - config := model.Config{ - EmailSettings: model.EmailSettings{ - EnableSMTPAuth: model.NewBool(false), - SMTPUsername: "test", - SMTPPassword: "fakepass", - SMTPServer: "fakeserver", - SMTPPort: "25", + auth := &authChooser{ + connectionInfo: &SmtpConnectionInfo{ + SmtpUsername: "test", + SmtpPassword: "fakepass", + SmtpServer: "fakeserver", + SmtpPort: "25", }, } - - auth := &authChooser{Config: &config} tests := []struct { desc string server *smtp.ServerInfo |