Merge branch 'master' into advanced-permissions-phase-1

This commit is contained in:
George Goldberg
2018-03-19 10:53:37 +00:00
25 changed files with 571 additions and 342 deletions

View File

@@ -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 {

View File

@@ -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 })
}()
config := model.Config{
EmailSettings: model.EmailSettings{
SMTPServer: "",
SMTPPort: "",
},
}
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 = "" })
_, 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,23 +512,14 @@ func TestS3TestConnection(t *testing.T) {
}
config.FileSettings.AmazonS3Bucket = model.MINIO_BUCKET
_, 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")
}
config.FileSettings.AmazonS3Region = "us-east-1"
_, resp = th.SystemAdminClient.TestS3Connection(&config)
CheckOKStatus(t, resp)
config.FileSettings.AmazonS3Region = ""
_, resp = th.SystemAdminClient.TestS3Connection(&config)
CheckOKStatus(t, resp)
config.FileSettings.AmazonS3Bucket = "Wrong_bucket"
_, resp = th.SystemAdminClient.TestS3Connection(&config)
CheckInternalErrorStatus(t, resp)

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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"))
})
}

View File

@@ -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 {

View File

@@ -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",

View File

@@ -3718,6 +3718,10 @@
"id": "app.plugin.config.app_error",
"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"

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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)
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)
if assert.Equal(t, expectedIds, actualIds) {
assert.Equal(t, expectedMessages, actualMessages)
}
}
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
r1 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 4, false)).Data.(*model.PostList)
r1 := (<-ss.Post().GetPosts(root1.ChannelId, 0, 4, false)).Data.(*model.PostList)
if r1.Order[0] != o5.Id {
t.Fatal("invalid order")
expectedOrder := []string{
root2Reply1.Id,
root2.Id,
root1Reply3.Id,
root1Reply2.Id,
}
if r1.Order[1] != o4.Id {
t.Fatal("invalid order")
expectedPosts := []*model.Post{
root1,
root1Reply1,
root1Reply2,
root1Reply3,
root2,
root2Reply1,
}
if r1.Order[2] != o3.Id {
t.Fatal("invalid order")
}
assert.Equal(t, expectedOrder, r1.Order)
assertPosts(expectedPosts, r1.Posts)
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")
}
r2 := (<-ss.Post().GetPosts(o1.ChannelId, 0, 4, true)).Data.(*model.PostList)
if r2.Order[0] != o5.Id {
t.Fatal("invalid order")
}
if r2.Order[1] != o4.Id {
t.Fatal("invalid order")
}
if r2.Order[2] != o3.Id {
t.Fatal("invalid order")
}
if r2.Order[3] != o2a.Id {
t.Fatal("invalid order")
}
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")
}
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) {

View File

@@ -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))+`'">`)

View File

@@ -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)

View File

@@ -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

View File

@@ -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")
}
}

View File

@@ -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)
}

View File

@@ -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