summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGeorge Goldberg <george@gberg.me>2018-03-02 15:55:03 +0000
committerGeorge Goldberg <george@gberg.me>2018-03-02 15:55:03 +0000
commit901acc9703ae58b625b44e7abfd02333b9bab951 (patch)
tree1a8fc17a85544bc7b8064874923e2fe6e3f44354
parent21afaf4bedcad578d4f876bb315d1072ccd296e6 (diff)
parent2b3b6051d265edf131d006b2eb14f55284faf1e5 (diff)
downloadchat-901acc9703ae58b625b44e7abfd02333b9bab951.tar.gz
chat-901acc9703ae58b625b44e7abfd02333b9bab951.tar.bz2
chat-901acc9703ae58b625b44e7abfd02333b9bab951.zip
Merge branch 'master' into advanced-permissions-phase-1
-rw-r--r--Makefile2
-rw-r--r--api/apitestlib.go1
-rw-r--r--api/file.go2
-rw-r--r--api/user.go6
-rw-r--r--api4/apitestlib.go49
-rw-r--r--api4/context.go18
-rw-r--r--api4/file.go69
-rw-r--r--api4/file_test.go129
-rw-r--r--api4/params.go14
-rw-r--r--api4/system.go31
-rw-r--r--api4/system_test.go64
-rw-r--r--api4/team.go83
-rw-r--r--api4/team_test.go81
-rw-r--r--api4/user.go4
-rw-r--r--app/app.go2
-rw-r--r--app/config.go17
-rw-r--r--app/diagnostics.go2
-rw-r--r--app/email.go2
-rw-r--r--app/email_batching.go18
-rw-r--r--app/file.go39
-rw-r--r--app/ldap.go4
-rw-r--r--app/login.go3
-rw-r--r--app/notification.go179
-rw-r--r--app/notification_test.go286
-rw-r--r--app/oauth.go13
-rw-r--r--app/server.go29
-rw-r--r--app/team.go104
-rw-r--r--app/team_test.go88
-rw-r--r--app/user.go18
-rw-r--r--app/web_hub.go133
-rw-r--r--build/release.mk14
-rw-r--r--cmd/platform/user.go189
-rw-r--r--config/default.json4
-rw-r--r--einterfaces/account_migration.go3
-rw-r--r--i18n/de.json66
-rw-r--r--i18n/en.json134
-rw-r--r--i18n/es.json68
-rw-r--r--i18n/fr.json116
-rw-r--r--i18n/it.json64
-rw-r--r--i18n/ja.json68
-rw-r--r--i18n/ko.json70
-rw-r--r--i18n/nl.json188
-rw-r--r--i18n/pl.json64
-rw-r--r--i18n/pt-BR.json70
-rw-r--r--i18n/ru.json80
-rw-r--r--i18n/tr.json68
-rw-r--r--i18n/zh-CN.json116
-rw-r--r--i18n/zh-TW.json74
-rw-r--r--model/client4.go76
-rw-r--r--model/config.go20
-rw-r--r--model/scheduled_task.go97
-rw-r--r--model/scheduled_task_test.go163
-rw-r--r--model/team.go27
-rw-r--r--model/version.go1
-rw-r--r--model/websocket_message.go25
-rw-r--r--model/websocket_message_test.go48
-rw-r--r--plugin/rpcplugin/sandbox/sandbox_linux.go2
-rwxr-xr-xscripts/prereq-check.sh4
-rw-r--r--store/sqlstore/channel_store.go6
-rw-r--r--store/sqlstore/team_store.go11
-rw-r--r--store/sqlstore/upgrade.go13
-rw-r--r--store/store.go1
-rw-r--r--store/storetest/mocks/TeamStore.go16
-rw-r--r--store/storetest/team_store.go26
-rw-r--r--utils/api.go2
-rw-r--r--utils/config.go11
-rw-r--r--utils/file_backend_s3.go44
-rw-r--r--utils/file_backend_s3_test.go32
-rw-r--r--utils/file_backend_test.go23
-rw-r--r--utils/lru.go113
-rw-r--r--utils/lru_test.go33
-rw-r--r--utils/mail.go58
-rw-r--r--utils/mail_test.go68
-rw-r--r--web/web_test.go1
74 files changed, 2709 insertions, 1158 deletions
diff --git a/Makefile b/Makefile
index 366c27057..490522c06 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build package run stop run-client run-server stop-client stop-server restart restart-server restart-client start-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist setup-mac prepare-enteprise run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows internal-test-web-client vet run-server-for-web-client-tests
+.PHONY: build package run stop run-client run-server stop-client stop-server restart restart-server restart-client start-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist setup-mac prepare-enteprise run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows package-common package-linux package-osx package-windows internal-test-web-client vet run-server-for-web-client-tests
ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
diff --git a/api/apitestlib.go b/api/apitestlib.go
index 6e2b8c045..699b0eb90 100644
--- a/api/apitestlib.go
+++ b/api/apitestlib.go
@@ -100,6 +100,7 @@ func setupTestHelper(enterprise bool) *TestHelper {
*cfg.TeamSettings.MaxUsersPerTeam = 50
*cfg.RateLimitSettings.Enable = false
cfg.EmailSettings.SendEmailNotifications = true
+ *cfg.ServiceSettings.EnableAPIv3 = true
})
prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress
if testStore != nil {
diff --git a/api/file.go b/api/file.go
index 3b8984816..63432eda5 100644
--- a/api/file.go
+++ b/api/file.go
@@ -76,7 +76,7 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- resStruct, err := c.App.UploadFiles(c.TeamId, channelId, c.Session.UserId, m.File["files"], m.Value["client_ids"])
+ resStruct, err := c.App.UploadMultipartFiles(c.TeamId, channelId, c.Session.UserId, m.File["files"], m.Value["client_ids"])
if err != nil {
c.Err = err
return
diff --git a/api/user.go b/api/user.go
index ad4f12ef3..14cc881dc 100644
--- a/api/user.go
+++ b/api/user.go
@@ -752,7 +752,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if sent, err := c.App.SendPasswordReset(email, utils.GetSiteURL()); err != nil {
+ if sent, err := c.App.SendPasswordReset(email, c.App.GetSiteURL()); err != nil {
c.Err = err
return
} else if sent {
@@ -1046,7 +1046,7 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if err := c.App.SendMfaChangeEmail(user.Email, activate, user.Locale, utils.GetSiteURL()); err != nil {
+ if err := c.App.SendMfaChangeEmail(user.Email, activate, user.Locale, c.App.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1180,7 +1180,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
}
c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
c.App.Go(func() {
- if err := c.App.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, utils.GetSiteURL()); err != nil {
+ if err := c.App.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
diff --git a/api4/apitestlib.go b/api4/apitestlib.go
index ac89a1f71..a788d4311 100644
--- a/api4/apitestlib.go
+++ b/api4/apitestlib.go
@@ -12,7 +12,6 @@ import (
"net/http"
"os"
"reflect"
- "runtime/debug"
"strconv"
"strings"
"sync"
@@ -497,6 +496,8 @@ func GenerateTestId() string {
}
func CheckUserSanitization(t *testing.T, user *model.User) {
+ t.Helper()
+
if user.Password != "" {
t.Fatal("password wasn't blank")
}
@@ -511,6 +512,8 @@ func CheckUserSanitization(t *testing.T, user *model.User) {
}
func CheckTeamSanitization(t *testing.T, team *model.Team) {
+ t.Helper()
+
if team.Email != "" {
t.Fatal("email wasn't blank")
}
@@ -521,13 +524,13 @@ func CheckTeamSanitization(t *testing.T, team *model.Team) {
}
func CheckEtag(t *testing.T, data interface{}, resp *model.Response) {
+ t.Helper()
+
if !reflect.ValueOf(data).IsNil() {
- debug.PrintStack()
t.Fatal("etag data was not nil")
}
if resp.StatusCode != http.StatusNotModified {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusNotModified))
t.Fatal("wrong status code for etag")
@@ -535,15 +538,17 @@ func CheckEtag(t *testing.T, data interface{}, resp *model.Response) {
}
func CheckNoError(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.Error != nil {
- debug.PrintStack()
t.Fatal("Expected no error, got " + resp.Error.Error())
}
}
func CheckCreatedStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.StatusCode != http.StatusCreated {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusCreated))
t.Fatal("wrong status code")
@@ -551,14 +556,14 @@ func CheckCreatedStatus(t *testing.T, resp *model.Response) {
}
func CheckForbiddenStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.Error == nil {
- debug.PrintStack()
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusForbidden))
return
}
if resp.StatusCode != http.StatusForbidden {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusForbidden))
t.Fatal("wrong status code")
@@ -566,14 +571,14 @@ func CheckForbiddenStatus(t *testing.T, resp *model.Response) {
}
func CheckUnauthorizedStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.Error == nil {
- debug.PrintStack()
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusUnauthorized))
return
}
if resp.StatusCode != http.StatusUnauthorized {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusUnauthorized))
t.Fatal("wrong status code")
@@ -581,14 +586,14 @@ func CheckUnauthorizedStatus(t *testing.T, resp *model.Response) {
}
func CheckNotFoundStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.Error == nil {
- debug.PrintStack()
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusNotFound))
return
}
if resp.StatusCode != http.StatusNotFound {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusNotFound))
t.Fatal("wrong status code")
@@ -596,14 +601,14 @@ func CheckNotFoundStatus(t *testing.T, resp *model.Response) {
}
func CheckBadRequestStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.Error == nil {
- debug.PrintStack()
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusBadRequest))
return
}
if resp.StatusCode != http.StatusBadRequest {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusBadRequest))
t.Fatal("wrong status code")
@@ -611,14 +616,14 @@ func CheckBadRequestStatus(t *testing.T, resp *model.Response) {
}
func CheckNotImplementedStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.Error == nil {
- debug.PrintStack()
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusNotImplemented))
return
}
if resp.StatusCode != http.StatusNotImplemented {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusNotImplemented))
t.Fatal("wrong status code")
@@ -626,6 +631,8 @@ func CheckNotImplementedStatus(t *testing.T, resp *model.Response) {
}
func CheckOKStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
CheckNoError(t, resp)
if resp.StatusCode != http.StatusOK {
@@ -634,14 +641,14 @@ func CheckOKStatus(t *testing.T, resp *model.Response) {
}
func CheckErrorMessage(t *testing.T, resp *model.Response, errorId string) {
+ t.Helper()
+
if resp.Error == nil {
- debug.PrintStack()
t.Fatal("should have errored with message:" + errorId)
return
}
if resp.Error.Id != errorId {
- debug.PrintStack()
t.Log("actual: " + resp.Error.Id)
t.Log("expected: " + errorId)
t.Fatal("incorrect error message")
@@ -649,14 +656,14 @@ func CheckErrorMessage(t *testing.T, resp *model.Response, errorId string) {
}
func CheckInternalErrorStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.Error == nil {
- debug.PrintStack()
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusInternalServerError))
return
}
if resp.StatusCode != http.StatusInternalServerError {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusInternalServerError))
t.Fatal("wrong status code")
@@ -664,14 +671,14 @@ func CheckInternalErrorStatus(t *testing.T, resp *model.Response) {
}
func CheckPayLoadTooLargeStatus(t *testing.T, resp *model.Response) {
+ t.Helper()
+
if resp.Error == nil {
- debug.PrintStack()
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusRequestEntityTooLarge))
return
}
if resp.StatusCode != http.StatusRequestEntityTooLarge {
- debug.PrintStack()
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusRequestEntityTooLarge))
t.Fatal("wrong status code")
diff --git a/api4/context.go b/api4/context.go
index 19778dda3..62fe55758 100644
--- a/api4/context.go
+++ b/api4/context.go
@@ -212,8 +212,10 @@ func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
func (c *Context) LogError(err *model.AppError) {
- // filter out endless reconnects
- if c.Path == "/api/v3/users/websocket" && err.StatusCode == 401 || err.Id == "web.check_browser_compatibility.app_error" {
+ // Filter out 404s, endless reconnects and browser compatibility errors
+ if err.StatusCode == http.StatusNotFound ||
+ (c.Path == "/api/v3/users/websocket" && err.StatusCode == 401) ||
+ err.Id == "web.check_browser_compatibility.app_error" {
c.LogDebug(err)
} else {
l4g.Error(utils.TDefault("api.context.log.error"), c.Path, err.Where, err.StatusCode,
@@ -447,6 +449,18 @@ func (c *Context) RequireFileId() *Context {
return c
}
+func (c *Context) RequireFilename() *Context {
+ if c.Err != nil {
+ return c
+ }
+
+ if len(c.Params.Filename) == 0 {
+ c.SetInvalidUrlParam("filename")
+ }
+
+ return c
+}
+
func (c *Context) RequirePluginId() *Context {
if c.Err != nil {
return c
diff --git a/api4/file.go b/api4/file.go
index acc4c78e5..0b0973b30 100644
--- a/api4/file.go
+++ b/api4/file.go
@@ -4,6 +4,7 @@
package api4
import (
+ "io"
"net/http"
"net/url"
"strconv"
@@ -65,32 +66,62 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
+ var resStruct *model.FileUploadResponse
+ var appErr *model.AppError
+
+ if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil && err != http.ErrNotMultipart {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
- }
+ } else if err == http.ErrNotMultipart {
+ defer r.Body.Close()
- m := r.MultipartForm
+ c.RequireChannelId()
+ c.RequireFilename()
- props := m.Value
- if len(props["channel_id"]) == 0 {
- c.SetInvalidParam("channel_id")
- return
- }
- channelId := props["channel_id"][0]
- if len(channelId) == 0 {
- c.SetInvalidParam("channel_id")
- return
- }
+ if c.Err != nil {
+ return
+ }
- if !c.App.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) {
- c.SetPermissionError(model.PERMISSION_UPLOAD_FILE)
- return
+ channelId := c.Params.ChannelId
+ filename := c.Params.Filename
+
+ if !c.App.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) {
+ c.SetPermissionError(model.PERMISSION_UPLOAD_FILE)
+ return
+ }
+
+ resStruct, appErr = c.App.UploadFiles(
+ FILE_TEAM_ID,
+ channelId,
+ c.Session.UserId,
+ []io.ReadCloser{r.Body},
+ []string{filename},
+ []string{},
+ )
+ } else {
+ m := r.MultipartForm
+
+ props := m.Value
+ if len(props["channel_id"]) == 0 {
+ c.SetInvalidParam("channel_id")
+ return
+ }
+ channelId := props["channel_id"][0]
+ if len(channelId) == 0 {
+ c.SetInvalidParam("channel_id")
+ return
+ }
+
+ if !c.App.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) {
+ c.SetPermissionError(model.PERMISSION_UPLOAD_FILE)
+ return
+ }
+
+ resStruct, appErr = c.App.UploadMultipartFiles(FILE_TEAM_ID, channelId, c.Session.UserId, m.File["files"], m.Value["client_ids"])
}
- resStruct, err := c.App.UploadFiles(FILE_TEAM_ID, channelId, c.Session.UserId, m.File["files"], m.Value["client_ids"])
- if err != nil {
- c.Err = err
+ if appErr != nil {
+ c.Err = appErr
return
}
diff --git a/api4/file_test.go b/api4/file_test.go
index 7010b3039..a28420c76 100644
--- a/api4/file_test.go
+++ b/api4/file_test.go
@@ -14,7 +14,7 @@ import (
"github.com/mattermost/mattermost-server/store"
)
-func TestUploadFile(t *testing.T) {
+func TestUploadFileAsMultipart(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
Client := th.Client
@@ -119,7 +119,132 @@ func TestUploadFile(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = false })
_, resp = th.SystemAdminClient.UploadFile(data, channel.Id, "test.png")
- if resp.StatusCode != http.StatusNotImplemented && resp.StatusCode != 0 {
+ if resp.StatusCode == 0 {
+ t.Log("file upload request failed completely")
+ } else if resp.StatusCode != http.StatusNotImplemented {
+ // This should return an HTTP 501, but it occasionally causes the http client itself to error
+ t.Fatalf("should've returned HTTP 501 or failed completely, got %v instead", resp.StatusCode)
+ }
+}
+
+func TestUploadFileAsRequestBody(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+ Client := th.Client
+
+ user := th.BasicUser
+ channel := th.BasicChannel
+
+ var uploadInfo *model.FileInfo
+ var data []byte
+ var err error
+ if data, err = readTestFile("test.png"); err != nil {
+ t.Fatal(err)
+ } else if fileResp, resp := Client.UploadFileAsRequestBody(data, channel.Id, "test.png"); resp.Error != nil {
+ t.Fatal(resp.Error)
+ } else if len(fileResp.FileInfos) != 1 {
+ t.Fatal("should've returned a single file infos")
+ } else {
+ uploadInfo = fileResp.FileInfos[0]
+ }
+
+ // The returned file info from the upload call will be missing some fields that will be stored in the database
+ if uploadInfo.CreatorId != user.Id {
+ t.Fatal("file should be assigned to user")
+ } else if uploadInfo.PostId != "" {
+ t.Fatal("file shouldn't have a post")
+ } else if uploadInfo.Path != "" {
+ t.Fatal("file path should not be set on returned info")
+ } else if uploadInfo.ThumbnailPath != "" {
+ t.Fatal("file thumbnail path should not be set on returned info")
+ } else if uploadInfo.PreviewPath != "" {
+ t.Fatal("file preview path should not be set on returned info")
+ }
+
+ var info *model.FileInfo
+ if result := <-th.App.Srv.Store.FileInfo().Get(uploadInfo.Id); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ info = result.Data.(*model.FileInfo)
+ }
+
+ if info.Id != uploadInfo.Id {
+ t.Fatal("file id from response should match one stored in database")
+ } else if info.CreatorId != user.Id {
+ t.Fatal("file should be assigned to user")
+ } else if info.PostId != "" {
+ t.Fatal("file shouldn't have a post")
+ } else if info.Path == "" {
+ t.Fatal("file path should be set in database")
+ } else if info.ThumbnailPath == "" {
+ t.Fatal("file thumbnail path should be set in database")
+ } else if info.PreviewPath == "" {
+ t.Fatal("file preview path should be set in database")
+ }
+
+ date := time.Now().Format("20060102")
+
+ // This also makes sure that the relative path provided above is sanitized out
+ expectedPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test.png", date, FILE_TEAM_ID, channel.Id, user.Id, info.Id)
+ if info.Path != expectedPath {
+ t.Logf("file is saved in %v", info.Path)
+ t.Fatalf("file should've been saved in %v", expectedPath)
+ }
+
+ expectedThumbnailPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test_thumb.jpg", date, FILE_TEAM_ID, channel.Id, user.Id, info.Id)
+ if info.ThumbnailPath != expectedThumbnailPath {
+ t.Logf("file thumbnail is saved in %v", info.ThumbnailPath)
+ t.Fatalf("file thumbnail should've been saved in %v", expectedThumbnailPath)
+ }
+
+ expectedPreviewPath := fmt.Sprintf("%v/teams/%v/channels/%v/users/%v/%v/test_preview.jpg", date, FILE_TEAM_ID, channel.Id, user.Id, info.Id)
+ if info.PreviewPath != expectedPreviewPath {
+ t.Logf("file preview is saved in %v", info.PreviewPath)
+ t.Fatalf("file preview should've been saved in %v", expectedPreviewPath)
+ }
+
+ // Wait a bit for files to ready
+ time.Sleep(2 * time.Second)
+
+ if err := th.cleanupTestFile(info); err != nil {
+ t.Fatal(err)
+ }
+
+ _, resp := Client.UploadFileAsRequestBody(data, model.NewId(), "test.png")
+ CheckForbiddenStatus(t, resp)
+
+ _, resp = Client.UploadFileAsRequestBody(data, "../../junk", "test.png")
+ if resp.StatusCode == 0 {
+ t.Log("file upload request failed completely")
+ } else if resp.StatusCode != http.StatusBadRequest {
+ // This should return an HTTP 400, but it occasionally causes the http client itself to error
+ t.Fatalf("should've returned HTTP 400 or failed completely, got %v instead", resp.StatusCode)
+ }
+
+ _, resp = th.SystemAdminClient.UploadFileAsRequestBody(data, model.NewId(), "test.png")
+ CheckForbiddenStatus(t, resp)
+
+ _, resp = th.SystemAdminClient.UploadFileAsRequestBody(data, "../../junk", "test.png")
+ if resp.StatusCode == 0 {
+ t.Log("file upload request failed completely")
+ } else if resp.StatusCode != http.StatusBadRequest {
+ // This should return an HTTP 400, but it occasionally causes the http client itself to error
+ t.Fatalf("should've returned HTTP 400 or failed completely, got %v instead", resp.StatusCode)
+ }
+
+ _, resp = th.SystemAdminClient.UploadFileAsRequestBody(data, channel.Id, "test.png")
+ CheckNoError(t, resp)
+
+ enableFileAttachments := *th.App.Config().FileSettings.EnableFileAttachments
+ defer func() {
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = enableFileAttachments })
+ }()
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = false })
+
+ _, resp = th.SystemAdminClient.UploadFileAsRequestBody(data, channel.Id, "test.png")
+ if resp.StatusCode == 0 {
+ t.Log("file upload request failed completely")
+ } else if resp.StatusCode != http.StatusNotImplemented {
// This should return an HTTP 501, but it occasionally causes the http client itself to error
t.Fatalf("should've returned HTTP 501 or failed completely, got %v instead", resp.StatusCode)
}
diff --git a/api4/params.go b/api4/params.go
index 357036e80..e8e3f25e7 100644
--- a/api4/params.go
+++ b/api4/params.go
@@ -27,6 +27,7 @@ type ApiParams struct {
ChannelId string
PostId string
FileId string
+ Filename string
PluginId string
CommandId string
HookId string
@@ -56,6 +57,7 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams {
params := &ApiParams{}
props := mux.Vars(r)
+ query := r.URL.Query()
if val, ok := props["user_id"]; ok {
params.UserId = val
@@ -75,6 +77,8 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams {
if val, ok := props["channel_id"]; ok {
params.ChannelId = val
+ } else {
+ params.ChannelId = query.Get("channel_id")
}
if val, ok := props["post_id"]; ok {
@@ -85,6 +89,8 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams {
params.FileId = val
}
+ params.Filename = query.Get("filename")
+
if val, ok := props["plugin_id"]; ok {
params.PluginId = val
}
@@ -161,17 +167,17 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams {
params.RoleName = val
}
- if val, err := strconv.Atoi(r.URL.Query().Get("page")); err != nil || val < 0 {
+ if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 {
params.Page = PAGE_DEFAULT
} else {
params.Page = val
}
- if val, err := strconv.ParseBool(r.URL.Query().Get("permanent")); err != nil {
+ if val, err := strconv.ParseBool(query.Get("permanent")); err != nil {
params.Permanent = val
}
- if val, err := strconv.Atoi(r.URL.Query().Get("per_page")); err != nil || val < 0 {
+ if val, err := strconv.Atoi(query.Get("per_page")); err != nil || val < 0 {
params.PerPage = PER_PAGE_DEFAULT
} else if val > PER_PAGE_MAXIMUM {
params.PerPage = PER_PAGE_MAXIMUM
@@ -179,7 +185,7 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams {
params.PerPage = val
}
- if val, err := strconv.Atoi(r.URL.Query().Get("logs_per_page")); err != nil || val < 0 {
+ if val, err := strconv.Atoi(query.Get("logs_per_page")); err != nil || val < 0 {
params.LogsPerPage = LOGS_PER_PAGE_DEFAULT
} else if val > LOGS_PER_PAGE_MAXIMUM {
params.LogsPerPage = LOGS_PER_PAGE_MAXIMUM
diff --git a/api4/system.go b/api4/system.go
index 2355cb476..aab65bf20 100644
--- a/api4/system.go
+++ b/api4/system.go
@@ -29,6 +29,7 @@ func (api *API) InitSystem() {
api.BaseRoutes.ApiRoot.Handle("/audits", api.ApiSessionRequired(getAudits)).Methods("GET")
api.BaseRoutes.ApiRoot.Handle("/email/test", api.ApiSessionRequired(testEmail)).Methods("POST")
+ api.BaseRoutes.ApiRoot.Handle("/file/s3_test", api.ApiSessionRequired(testS3)).Methods("POST")
api.BaseRoutes.ApiRoot.Handle("/database/recycle", api.ApiSessionRequired(databaseRecycle)).Methods("POST")
api.BaseRoutes.ApiRoot.Handle("/caches/invalidate", api.ApiSessionRequired(invalidateCaches)).Methods("POST")
@@ -384,3 +385,33 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(rows.ToJson()))
}
+
+func testS3(c *Context, w http.ResponseWriter, r *http.Request) {
+ cfg := model.ConfigFromJson(r.Body)
+ if cfg == nil {
+ cfg = c.App.Config()
+ }
+
+ if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ err := utils.CheckMandatoryS3Fields(&cfg.FileSettings)
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ license := c.App.License()
+ backend, appErr := utils.NewFileBackend(&cfg.FileSettings, license != nil && *license.Features.Compliance)
+ if appErr == nil {
+ appErr = backend.TestConnection()
+ }
+ if appErr != nil {
+ c.Err = appErr
+ return
+ }
+
+ ReturnStatusOK(w)
+}
diff --git a/api4/system_test.go b/api4/system_test.go
index 01b4934ae..e39486b77 100644
--- a/api4/system_test.go
+++ b/api4/system_test.go
@@ -1,7 +1,9 @@
package api4
import (
+ "fmt"
"net/http"
+ "os"
"strings"
"testing"
@@ -466,3 +468,65 @@ func TestGetAnalyticsOld(t *testing.T) {
_, resp = Client.GetAnalyticsOld("", th.BasicTeam.Id)
CheckUnauthorizedStatus(t, resp)
}
+
+func TestS3TestConnection(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+ Client := th.Client
+
+ s3Host := os.Getenv("CI_HOST")
+ if s3Host == "" {
+ s3Host = "dockerhost"
+ }
+
+ s3Port := os.Getenv("CI_MINIO_PORT")
+ if s3Port == "" {
+ s3Port = "9001"
+ }
+
+ s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port)
+ config := model.Config{
+ FileSettings: model.FileSettings{
+ DriverName: model.NewString(model.IMAGE_DRIVER_S3),
+ AmazonS3AccessKeyId: model.MINIO_ACCESS_KEY,
+ AmazonS3SecretAccessKey: model.MINIO_SECRET_KEY,
+ AmazonS3Bucket: "",
+ AmazonS3Endpoint: "",
+ AmazonS3SSL: model.NewBool(false),
+ },
+ }
+
+ _, resp := Client.TestS3Connection(&config)
+ CheckForbiddenStatus(t, resp)
+
+ _, resp = th.SystemAdminClient.TestS3Connection(&config)
+ CheckBadRequestStatus(t, resp)
+ if resp.Error.Message != "S3 Bucket is required" {
+ t.Fatal("should return error - missing s3 bucket")
+ }
+
+ 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.AmazonS3Bucket = "Wrong_bucket"
+ _, resp = th.SystemAdminClient.TestS3Connection(&config)
+ CheckInternalErrorStatus(t, resp)
+ if resp.Error.Message != "Error checking if bucket exists." {
+ t.Fatal("should return error ")
+ }
+}
diff --git a/api4/team.go b/api4/team.go
index d770aee22..8e4c5c312 100644
--- a/api4/team.go
+++ b/api4/team.go
@@ -6,6 +6,7 @@ package api4
import (
"bytes"
"encoding/base64"
+ "fmt"
"net/http"
"strconv"
@@ -28,6 +29,10 @@ func (api *API) InitTeam() {
api.BaseRoutes.Team.Handle("", api.ApiSessionRequired(deleteTeam)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/patch", api.ApiSessionRequired(patchTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("/stats", api.ApiSessionRequired(getTeamStats)).Methods("GET")
+
+ api.BaseRoutes.Team.Handle("/image", api.ApiSessionRequiredTrustRequester(getTeamIcon)).Methods("GET")
+ api.BaseRoutes.Team.Handle("/image", api.ApiSessionRequired(setTeamIcon)).Methods("POST")
+
api.BaseRoutes.TeamMembers.Handle("", api.ApiSessionRequired(getTeamMembers)).Methods("GET")
api.BaseRoutes.TeamMembers.Handle("/ids", api.ApiSessionRequired(getTeamMembersByIds)).Methods("POST")
api.BaseRoutes.TeamMembersForUser.Handle("", api.ApiSessionRequired(getTeamMembersForUser)).Methods("GET")
@@ -729,3 +734,81 @@ func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(result)))
}
}
+
+func getTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireTeamId()
+ if c.Err != nil {
+ return
+ }
+
+ if !c.App.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_VIEW_TEAM) {
+ c.SetPermissionError(model.PERMISSION_VIEW_TEAM)
+ return
+ }
+
+ if team, err := c.App.GetTeam(c.Params.TeamId); err != nil {
+ c.Err = err
+ return
+ } else {
+ etag := strconv.FormatInt(team.LastTeamIconUpdate, 10)
+
+ if c.HandleEtag(etag, "Get Team Icon", w, r) {
+ return
+ }
+
+ if img, err := c.App.GetTeamIcon(team); err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Header().Set("Content-Type", "image/png")
+ w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, public", 24*60*60)) // 24 hrs
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+ w.Write(img)
+ }
+ }
+}
+
+func setTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.RequireTeamId()
+ if c.Err != nil {
+ return
+ }
+
+ if !c.App.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_MANAGE_TEAM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_TEAM)
+ return
+ }
+
+ if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
+ c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, "", http.StatusBadRequest)
+ return
+ }
+
+ if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
+ c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.parse.app_error", nil, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ m := r.MultipartForm
+
+ imageArray, ok := m.File["image"]
+ if !ok {
+ c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.no_file.app_error", nil, "", http.StatusBadRequest)
+ return
+ }
+
+ if len(imageArray) <= 0 {
+ c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.array.app_error", nil, "", http.StatusBadRequest)
+ return
+ }
+
+ imageData := imageArray[0]
+
+ if err := c.App.SetTeamIcon(c.Params.TeamId, imageData); err != nil {
+ c.Err = err
+ return
+ }
+
+ c.LogAudit("")
+ ReturnStatusOK(w)
+}
diff --git a/api4/team_test.go b/api4/team_test.go
index fa139faae..faac81312 100644
--- a/api4/team_test.go
+++ b/api4/team_test.go
@@ -15,6 +15,8 @@ import (
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestCreateTeam(t *testing.T) {
@@ -1903,3 +1905,82 @@ func TestGetTeamInviteInfo(t *testing.T) {
_, resp = Client.GetTeamInviteInfo("junk")
CheckNotFoundStatus(t, resp)
}
+
+func TestSetTeamIcon(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+ Client := th.Client
+ team := th.BasicTeam
+
+ data, err := readTestFile("test.png")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ th.LoginTeamAdmin()
+
+ ok, resp := Client.SetTeamIcon(team.Id, data)
+ if !ok {
+ t.Fatal(resp.Error)
+ }
+ CheckNoError(t, resp)
+
+ ok, resp = Client.SetTeamIcon(model.NewId(), data)
+ if ok {
+ t.Fatal("Should return false, set team icon not allowed")
+ }
+ CheckForbiddenStatus(t, resp)
+
+ th.LoginBasic()
+
+ _, resp = Client.SetTeamIcon(team.Id, data)
+ if resp.StatusCode == http.StatusForbidden {
+ CheckForbiddenStatus(t, resp)
+ } else if resp.StatusCode == http.StatusUnauthorized {
+ CheckUnauthorizedStatus(t, resp)
+ } else {
+ t.Fatal("Should have failed either forbidden or unauthorized")
+ }
+
+ Client.Logout()
+
+ _, resp = Client.SetTeamIcon(team.Id, data)
+ if resp.StatusCode == http.StatusForbidden {
+ CheckForbiddenStatus(t, resp)
+ } else if resp.StatusCode == http.StatusUnauthorized {
+ CheckUnauthorizedStatus(t, resp)
+ } else {
+ t.Fatal("Should have failed either forbidden or unauthorized")
+ }
+
+ teamBefore, err := th.App.GetTeam(team.Id)
+ require.Nil(t, err)
+
+ _, resp = th.SystemAdminClient.SetTeamIcon(team.Id, data)
+ CheckNoError(t, resp)
+
+ teamAfter, err := th.App.GetTeam(team.Id)
+ require.Nil(t, err)
+ assert.True(t, teamBefore.LastTeamIconUpdate < teamAfter.LastTeamIconUpdate, "LastTeamIconUpdate should have been updated for team")
+
+ info := &model.FileInfo{Path: "teams/" + team.Id + "/teamIcon.png"}
+ if err := th.cleanupTestFile(info); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestGetTeamIcon(t *testing.T) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer th.TearDown()
+ Client := th.Client
+ team := th.BasicTeam
+
+ // should always fail because no initial image and no auto creation
+ _, resp := Client.GetTeamIcon(team.Id, "")
+ CheckNotFoundStatus(t, resp)
+
+ Client.Logout()
+
+ _, resp = Client.GetTeamIcon(team.Id, "")
+ CheckUnauthorizedStatus(t, resp)
+}
diff --git a/api4/user.go b/api4/user.go
index cfb2a5b3f..f82a6e3d5 100644
--- a/api4/user.go
+++ b/api4/user.go
@@ -13,7 +13,6 @@ import (
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
- "github.com/mattermost/mattermost-server/utils"
)
func (api *API) InitUser() {
@@ -894,7 +893,7 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if sent, err := c.App.SendPasswordReset(email, utils.GetSiteURL()); err != nil {
+ if sent, err := c.App.SendPasswordReset(email, c.App.GetSiteURL()); err != nil {
c.Err = err
return
} else if sent {
@@ -1076,6 +1075,7 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
MaxAge: maxAge,
Expires: expiresAt,
HttpOnly: true,
+ Domain: c.App.GetCookieDomain(),
Secure: secure,
}
diff --git a/app/app.go b/app/app.go
index 9d44c358c..5bc418e0d 100644
--- a/app/app.go
+++ b/app/app.go
@@ -66,6 +66,8 @@ type App struct {
clientLicenseValue atomic.Value
licenseListeners map[string]func()
+ siteURL string
+
newStore func() store.Store
htmlTemplateWatcher *utils.HTMLTemplateWatcher
diff --git a/app/config.go b/app/config.go
index b4925e8fb..35a0c9a3f 100644
--- a/app/config.go
+++ b/app/config.go
@@ -12,7 +12,9 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "net/url"
"runtime/debug"
+ "strings"
l4g "github.com/alecthomas/log4go"
@@ -53,7 +55,7 @@ func (a *App) LoadConfig(configFile string) *model.AppError {
a.config.Store(cfg)
- utils.SetSiteURL(*cfg.ServiceSettings.SiteURL)
+ a.siteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/")
a.InvokeConfigListeners(old, cfg)
return nil
@@ -254,3 +256,16 @@ func (a *App) Desanitize(cfg *model.Config) {
cfg.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i]
}
}
+
+func (a *App) GetCookieDomain() string {
+ if *a.Config().ServiceSettings.AllowCookiesForSubdomains {
+ if siteURL, err := url.Parse(*a.Config().ServiceSettings.SiteURL); err == nil {
+ return siteURL.Hostname()
+ }
+ }
+ return ""
+}
+
+func (a *App) GetSiteURL() string {
+ return a.siteURL
+}
diff --git a/app/diagnostics.go b/app/diagnostics.go
index bbc72e63e..ddea3289f 100644
--- a/app/diagnostics.go
+++ b/app/diagnostics.go
@@ -243,6 +243,8 @@ func (a *App) trackConfig() {
"isdefault_image_proxy_type": isDefault(*cfg.ServiceSettings.ImageProxyType, ""),
"isdefault_image_proxy_url": isDefault(*cfg.ServiceSettings.ImageProxyURL, ""),
"isdefault_image_proxy_options": isDefault(*cfg.ServiceSettings.ImageProxyOptions, ""),
+ "websocket_url": isDefault(*cfg.ServiceSettings.WebsocketURL, ""),
+ "allow_cookies_for_subdomains": *cfg.ServiceSettings.AllowCookiesForSubdomains,
})
a.SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{
diff --git a/app/email.go b/app/email.go
index 54a272a3b..8ee3e79e2 100644
--- a/app/email.go
+++ b/app/email.go
@@ -191,7 +191,7 @@ func (a *App) SendUserAccessTokenAddedEmail(email, locale string) *model.AppErro
bodyPage := a.NewEmailTemplate("password_change_body", locale)
bodyPage.Props["Title"] = T("api.templates.user_access_token_body.title")
bodyPage.Html["Info"] = utils.TranslateAsHtml(T, "api.templates.user_access_token_body.info",
- map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "SiteURL": utils.GetSiteURL()})
+ map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"], "SiteURL": a.GetSiteURL()})
if err := a.SendMail(email, subject, bodyPage.Render()); err != nil {
return model.NewAppError("SendUserAccessTokenAddedEmail", "api.user.send_user_access_token.error", nil, err.Error(), http.StatusInternalServerError)
diff --git a/app/email_batching.go b/app/email_batching.go
index 2a33d7d3e..07adda674 100644
--- a/app/email_batching.go
+++ b/app/email_batching.go
@@ -7,6 +7,7 @@ import (
"fmt"
"html/template"
"strconv"
+ "sync"
"time"
"github.com/mattermost/mattermost-server/model"
@@ -57,6 +58,8 @@ type EmailBatchingJob struct {
app *App
newNotifications chan *batchedNotification
pendingNotifications map[string][]*batchedNotification
+ task *model.ScheduledTask
+ taskMutex sync.Mutex
}
func NewEmailBatchingJob(a *App, bufferSize int) *EmailBatchingJob {
@@ -68,12 +71,17 @@ func NewEmailBatchingJob(a *App, bufferSize int) *EmailBatchingJob {
}
func (job *EmailBatchingJob) Start() {
- if task := model.GetTaskByName(EMAIL_BATCHING_TASK_NAME); task != nil {
- task.Cancel()
- }
-
l4g.Debug(utils.T("api.email_batching.start.starting"), *job.app.Config().EmailSettings.EmailBatchingInterval)
- model.CreateRecurringTask(EMAIL_BATCHING_TASK_NAME, job.CheckPendingEmails, time.Duration(*job.app.Config().EmailSettings.EmailBatchingInterval)*time.Second)
+ newTask := model.CreateRecurringTask(EMAIL_BATCHING_TASK_NAME, job.CheckPendingEmails, time.Duration(*job.app.Config().EmailSettings.EmailBatchingInterval)*time.Second)
+
+ job.taskMutex.Lock()
+ oldTask := job.task
+ job.task = newTask
+ job.taskMutex.Unlock()
+
+ if oldTask != nil {
+ oldTask.Cancel()
+ }
}
func (job *EmailBatchingJob) Add(user *model.User, post *model.Post, team *model.Team) bool {
diff --git a/app/file.go b/app/file.go
index bb20585bb..06ee61c92 100644
--- a/app/file.go
+++ b/app/file.go
@@ -280,11 +280,38 @@ func GeneratePublicLinkHash(fileId, salt string) string {
return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
}
-func (a *App) UploadFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string) (*model.FileUploadResponse, *model.AppError) {
+func (a *App) UploadMultipartFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string) (*model.FileUploadResponse, *model.AppError) {
+ files := make([]io.ReadCloser, len(fileHeaders))
+ filenames := make([]string, len(fileHeaders))
+
+ for i, fileHeader := range fileHeaders {
+ file, fileErr := fileHeader.Open()
+ if fileErr != nil {
+ return nil, model.NewAppError("UploadFiles", "api.file.upload_file.bad_parse.app_error", nil, fileErr.Error(), http.StatusBadRequest)
+ }
+
+ // Will be closed after UploadFiles returns
+ defer file.Close()
+
+ files[i] = file
+ filenames[i] = fileHeader.Filename
+ }
+
+ return a.UploadFiles(teamId, channelId, userId, files, filenames, clientIds)
+}
+
+// Uploads some files to the given team and channel as the given user. files and filenames should have
+// the same length. clientIds should either not be provided or have the same length as files and filenames.
+// The provided files should be closed by the caller so that they are not leaked.
+func (a *App) UploadFiles(teamId string, channelId string, userId string, files []io.ReadCloser, filenames []string, clientIds []string) (*model.FileUploadResponse, *model.AppError) {
if len(*a.Config().FileSettings.DriverName) == 0 {
return nil, model.NewAppError("uploadFile", "api.file.upload_file.storage.app_error", nil, "", http.StatusNotImplemented)
}
+ if len(filenames) != len(files) || (len(clientIds) > 0 && len(clientIds) != len(files)) {
+ return nil, model.NewAppError("UploadFiles", "api.file.upload_file.incorrect_number_of_files.app_error", nil, "", http.StatusBadRequest)
+ }
+
resStruct := &model.FileUploadResponse{
FileInfos: []*model.FileInfo{},
ClientIds: []string{},
@@ -294,18 +321,12 @@ func (a *App) UploadFiles(teamId string, channelId string, userId string, fileHe
thumbnailPathList := []string{}
imageDataList := [][]byte{}
- for i, fileHeader := range fileHeaders {
- file, fileErr := fileHeader.Open()
- if fileErr != nil {
- return nil, model.NewAppError("UploadFiles", "api.file.upload_file.bad_parse.app_error", nil, fileErr.Error(), http.StatusBadRequest)
- }
- defer file.Close()
-
+ for i, file := range files {
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
data := buf.Bytes()
- info, err := a.DoUploadFile(time.Now(), teamId, channelId, userId, fileHeader.Filename, data)
+ info, err := a.DoUploadFile(time.Now(), teamId, channelId, userId, filenames[i], data)
if err != nil {
return nil, err
}
diff --git a/app/ldap.go b/app/ldap.go
index 179529c52..ff7a5ed21 100644
--- a/app/ldap.go
+++ b/app/ldap.go
@@ -67,7 +67,7 @@ func (a *App) SwitchEmailToLdap(email, password, code, ldapId, ldapPassword stri
}
a.Go(func() {
- if err := a.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -113,7 +113,7 @@ func (a *App) SwitchLdapToEmail(ldapPassword, code, email, newPassword string) (
T := utils.GetUserTranslations(user.Locale)
a.Go(func() {
- if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
diff --git a/app/login.go b/app/login.go
index ecc0f0163..e01566bcd 100644
--- a/app/login.go
+++ b/app/login.go
@@ -113,6 +113,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
secure = true
}
+ domain := a.GetCookieDomain()
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
sessionCookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
@@ -121,6 +122,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
MaxAge: maxAge,
Expires: expiresAt,
HttpOnly: true,
+ Domain: domain,
Secure: secure,
}
@@ -130,6 +132,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
Path: "/",
MaxAge: maxAge,
Expires: expiresAt,
+ Domain: domain,
Secure: secure,
}
diff --git a/app/notification.go b/app/notification.go
index 1318308f8..8cb63fbaf 100644
--- a/app/notification.go
+++ b/app/notification.go
@@ -362,7 +362,7 @@ func (a *App) sendNotificationEmail(post *model.Post, user *model.User, channel
emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
}
- teamURL := utils.GetSiteURL() + "/" + team.Name
+ teamURL := a.GetSiteURL() + "/" + team.Name
var bodyText = a.getNotificationEmailBody(user, post, channel, senderName, team.Name, teamURL, emailNotificationContentsType, translateFunc)
a.Go(func() {
@@ -421,7 +421,7 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post,
bodyPage = a.NewEmailTemplate("post_body_generic", recipient.Locale)
}
- bodyPage.Props["SiteURL"] = utils.GetSiteURL()
+ bodyPage.Props["SiteURL"] = a.GetSiteURL()
if teamName != "select_team" {
bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id
} else {
@@ -566,8 +566,6 @@ func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *
channelName = senderName
}
- userLocale := utils.GetUserTranslations(user.Locale)
-
msg := model.PushNotification{}
if badge := <-a.Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil {
msg.Badge = 1
@@ -596,44 +594,10 @@ func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *
msg.FromWebhook = fw
}
- if *a.Config().EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION {
- msg.Category = model.CATEGORY_CAN_REPLY
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Message = senderName + ": " + model.ClearMentionTags(post.Message)
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(post.Message)
- }
- } else if *a.Config().EmailSettings.PushNotificationContents == model.GENERIC_NO_CHANNEL_NOTIFICATION {
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Category = model.CATEGORY_CAN_REPLY
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
- } else if wasMentioned || channel.Type == model.CHANNEL_GROUP {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention_no_channel")
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention_no_channel")
- }
- } else {
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Category = model.CATEGORY_CAN_REPLY
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
- } else if wasMentioned || channel.Type == model.CHANNEL_GROUP {
- msg.Category = model.CATEGORY_CAN_REPLY
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
- }
- }
-
- // If the post only has images then push an appropriate message
- if len(post.Message) == 0 && post.FileIds != nil && len(post.FileIds) > 0 {
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_dm")
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + channelName
- }
- }
+ userLocale := utils.GetUserTranslations(user.Locale)
+ hasFiles := post.FileIds != nil && len(post.FileIds) > 0
- //l4g.Debug("Sending push notification for user %v with msg of '%v'", user.Id, msg.Message)
+ msg.Message, msg.Category = a.getPushNotificationMessage(post.Message, wasMentioned, hasFiles, senderName, channelName, channel.Type, userLocale)
for _, session := range sessions {
tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
@@ -655,6 +619,58 @@ func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *
return nil
}
+func (a *App) getPushNotificationMessage(postMessage string, wasMentioned bool, hasFiles bool, senderName string, channelName string, channelType string, userLocale i18n.TranslateFunc) (string, string) {
+ message := ""
+ category := ""
+
+ contentsConfig := *a.Config().EmailSettings.PushNotificationContents
+
+ if contentsConfig == model.FULL_NOTIFICATION {
+ category = model.CATEGORY_CAN_REPLY
+
+ if channelType == model.CHANNEL_DIRECT {
+ message = senderName + ": " + model.ClearMentionTags(postMessage)
+ } else {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(postMessage)
+ }
+ } else if contentsConfig == model.GENERIC_NO_CHANNEL_NOTIFICATION {
+ if channelType == model.CHANNEL_DIRECT {
+ category = model.CATEGORY_CAN_REPLY
+
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
+ } else if wasMentioned {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention_no_channel")
+ } else {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention_no_channel")
+ }
+ } else {
+ if channelType == model.CHANNEL_DIRECT {
+ category = model.CATEGORY_CAN_REPLY
+
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
+ } else if wasMentioned {
+ category = model.CATEGORY_CAN_REPLY
+
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
+ } else {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
+ }
+ }
+
+ // If the post only has images then push an appropriate message
+ if len(postMessage) == 0 && hasFiles {
+ if channelType == model.CHANNEL_DIRECT {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_dm")
+ } else if contentsConfig == model.GENERIC_NO_CHANNEL_NOTIFICATION {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_no_channel")
+ } else {
+ message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + channelName
+ }
+ }
+
+ return message, category
+}
+
func (a *App) ClearPushNotification(userId string, channelId string) {
a.Go(func() {
// Sleep is to allow the read replicas a chance to fully sync
@@ -819,44 +835,52 @@ func GetExplicitMentions(message string, keywords map[string][]string) *Explicit
ret.MentionedUserIds[id] = true
}
}
+ checkForMention := func(word string) bool {
+ isMention := false
+
+ if word == "@here" {
+ ret.HereMentioned = true
+ }
+
+ if word == "@channel" {
+ ret.ChannelMentioned = true
+ }
+ if word == "@all" {
+ ret.AllMentioned = true
+ }
+
+ // Non-case-sensitive check for regular keys
+ if ids, match := keywords[strings.ToLower(word)]; match {
+ addMentionedUsers(ids)
+ isMention = true
+ }
+
+ // Case-sensitive check for first name
+ if ids, match := keywords[word]; match {
+ addMentionedUsers(ids)
+ isMention = true
+ }
+
+ return isMention
+ }
processText := func(text string) {
for _, word := range strings.FieldsFunc(text, func(c rune) bool {
// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
}) {
- isMention := false
-
// skip word with format ':word:' with an assumption that it is an emoji format only
if word[0] == ':' && word[len(word)-1] == ':' {
continue
}
- if word == "@here" {
- ret.HereMentioned = true
- }
-
- if word == "@channel" {
- ret.ChannelMentioned = true
- }
-
- if word == "@all" {
- ret.AllMentioned = true
- }
-
- // Non-case-sensitive check for regular keys
- if ids, match := keywords[strings.ToLower(word)]; match {
- addMentionedUsers(ids)
- isMention = true
- }
-
- // Case-sensitive check for first name
- if ids, match := keywords[word]; match {
- addMentionedUsers(ids)
- isMention = true
+ if checkForMention(word) {
+ continue
}
- if isMention {
+ // remove trailing '.', as that is the end of a sentence
+ word = strings.TrimSuffix(word, ".")
+ if checkForMention(word) {
continue
}
@@ -867,27 +891,10 @@ func GetExplicitMentions(message string, keywords map[string][]string) *Explicit
})
for _, splitWord := range splitWords {
- if splitWord == "@here" {
- ret.HereMentioned = true
- }
-
- if splitWord == "@all" {
- ret.AllMentioned = true
- }
-
- if splitWord == "@channel" {
- ret.ChannelMentioned = true
- }
-
- // Non-case-sensitive check for regular keys
- if ids, match := keywords[strings.ToLower(splitWord)]; match {
- addMentionedUsers(ids)
+ if checkForMention(splitWord) {
+ continue
}
-
- // Case-sensitive check for first name
- if ids, match := keywords[splitWord]; match {
- addMentionedUsers(ids)
- } else if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
+ if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
username := splitWord[1:]
ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username)
}
diff --git a/app/notification_test.go b/app/notification_test.go
index 11f4df685..5fc1d152c 100644
--- a/app/notification_test.go
+++ b/app/notification_test.go
@@ -109,6 +109,33 @@ func TestGetExplicitMentions(t *testing.T) {
},
},
},
+ "OnePersonWithPeriodAtEndOfUsername": {
+ Message: "this is a message for @user.name.",
+ Keywords: map[string][]string{"@user.name.": {id1}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
+ "OnePersonWithPeriodAtEndOfUsernameButNotSimilarName": {
+ Message: "this is a message for @user.name.",
+ Keywords: map[string][]string{"@user.name.": {id1}, "@user.name": {id2}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
+ "OnePersonAtEndOfSentence": {
+ Message: "this is a message for @user.",
+ Keywords: map[string][]string{"@user": {id1}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
"OnePersonWithoutAtMention": {
Message: "this is a message for @user",
Keywords: map[string][]string{"this": {id1}},
@@ -179,6 +206,24 @@ func TestGetExplicitMentions(t *testing.T) {
},
},
},
+ "AtUserWithPeriodAtEndOfSentence": {
+ Message: "this is a message for @user.period.",
+ Keywords: map[string][]string{"@user.period": {id1}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
+ "UserWithPeriodAtEndOfSentence": {
+ Message: "this is a message for user.period.",
+ Keywords: map[string][]string{"user.period": {id1}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ },
+ },
+ },
"PotentialOutOfChannelUser": {
Message: "this is an message for @potential and @user",
Keywords: map[string][]string{"@user": {id1}},
@@ -1166,3 +1211,244 @@ func TestGetNotificationEmailBodyGenericNotificationDirectChannel(t *testing.T)
t.Fatal("Expected email text '" + teamURL + "'. Got " + body)
}
}
+
+func TestGetPushNotificationMessage(t *testing.T) {
+ th := Setup()
+ defer th.TearDown()
+
+ for name, tc := range map[string]struct {
+ Message string
+ WasMentioned bool
+ HasFiles bool
+ Locale string
+ PushNotificationContents string
+ ChannelType string
+
+ ExpectedMessage string
+ ExpectedCategory string
+ }{
+ "full message, public channel, no mention": {
+ Message: "this is a message",
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, public channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, private channel, no mention": {
+ Message: "this is a message",
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, private channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, group message channel, no mention": {
+ Message: "this is a message",
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, group message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user in channel: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, direct message channel, no mention": {
+ Message: "this is a message",
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "full message, direct message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user: this is a message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, public channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user posted in channel",
+ },
+ "generic message with channel, public channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user mentioned you in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, private channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user posted in channel",
+ },
+ "generic message with channel, private channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user mentioned you in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, group message channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user posted in channel",
+ },
+ "generic message with channel, group message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user mentioned you in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, direct message channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user sent you a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message with channel, direct message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NOTIFICATION,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user sent you a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message without channel, public channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user posted a message",
+ },
+ "generic message without channel, public channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user mentioned you",
+ },
+ "generic message without channel, private channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user posted a message",
+ },
+ "generic message without channel, private channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user mentioned you",
+ },
+ "generic message without channel, group message channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user posted a message",
+ },
+ "generic message without channel, group message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user mentioned you",
+ },
+ "generic message without channel, direct message channel, no mention": {
+ Message: "this is a message",
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user sent you a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "generic message without channel, direct message channel, mention": {
+ Message: "this is a message",
+ WasMentioned: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user sent you a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files, public channel": {
+ HasFiles: true,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user uploaded one or more files in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files, private channel": {
+ HasFiles: true,
+ ChannelType: model.CHANNEL_PRIVATE,
+ ExpectedMessage: "user uploaded one or more files in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files, group message channel": {
+ HasFiles: true,
+ ChannelType: model.CHANNEL_GROUP,
+ ExpectedMessage: "user uploaded one or more files in channel",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files, direct message channel": {
+ HasFiles: true,
+ ChannelType: model.CHANNEL_DIRECT,
+ ExpectedMessage: "user uploaded one or more files in a direct message",
+ ExpectedCategory: model.CATEGORY_CAN_REPLY,
+ },
+ "only files without channel, public channel": {
+ HasFiles: true,
+ PushNotificationContents: model.GENERIC_NO_CHANNEL_NOTIFICATION,
+ ChannelType: model.CHANNEL_OPEN,
+ ExpectedMessage: "user uploaded one or more files",
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ locale := tc.Locale
+ if locale == "" {
+ locale = "en"
+ }
+
+ pushNotificationContents := tc.PushNotificationContents
+ if pushNotificationContents == "" {
+ pushNotificationContents = model.FULL_NOTIFICATION
+ }
+
+ th.App.UpdateConfig(func(cfg *model.Config) {
+ *cfg.EmailSettings.PushNotificationContents = pushNotificationContents
+ })
+
+ if actualMessage, actualCategory := th.App.getPushNotificationMessage(
+ tc.Message,
+ tc.WasMentioned,
+ tc.HasFiles,
+ "user",
+ "channel",
+ tc.ChannelType,
+ utils.GetUserTranslations(locale),
+ ); actualMessage != tc.ExpectedMessage {
+ t.Fatalf("Received incorrect push notification message `%v`, expected `%v`", actualMessage, tc.ExpectedMessage)
+ } else if actualCategory != tc.ExpectedCategory {
+ t.Fatalf("Received incorrect push notification category `%v`, expected `%v`", actualCategory, tc.ExpectedCategory)
+ }
+ })
+ }
+}
diff --git a/app/oauth.go b/app/oauth.go
index 5a66f542e..630fd3e2d 100644
--- a/app/oauth.go
+++ b/app/oauth.go
@@ -527,7 +527,7 @@ func (a *App) CompleteSwitchWithOAuth(service string, userData io.ReadCloser, em
}
a.Go(func() {
- if err := a.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -600,7 +600,12 @@ func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, servi
props["token"] = stateToken.Token
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
- redirectUri := utils.GetSiteURL() + "/signup/" + service + "/complete"
+ siteUrl := a.GetSiteURL()
+ if strings.TrimSpace(siteUrl) == "" {
+ siteUrl = GetProtocol(r) + "://" + r.Host
+ }
+
+ redirectUri := siteUrl + "/signup/" + service + "/complete"
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state)
@@ -736,7 +741,7 @@ func (a *App) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email,
stateProps["email"] = email
if service == model.USER_AUTH_SERVICE_SAML {
- return utils.GetSiteURL() + "/login/sso/saml?action=" + model.OAUTH_ACTION_EMAIL_TO_SSO + "&email=" + utils.UrlEncode(email), nil
+ return a.GetSiteURL() + "/login/sso/saml?action=" + model.OAUTH_ACTION_EMAIL_TO_SSO + "&email=" + utils.UrlEncode(email), nil
} else {
if authUrl, err := a.GetAuthorizationCode(w, r, service, stateProps, ""); err != nil {
return "", err
@@ -768,7 +773,7 @@ func (a *App) SwitchOAuthToEmail(email, password, requesterId string) (string, *
T := utils.GetUserTranslations(user.Locale)
a.Go(func() {
- if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
diff --git a/app/server.go b/app/server.go
index afa282ad6..93804a372 100644
--- a/app/server.go
+++ b/app/server.go
@@ -171,20 +171,25 @@ func (a *App) StartServer() error {
}
if *a.Config().ServiceSettings.Forward80To443 {
- if *a.Config().ServiceSettings.UseLetsEncrypt {
- go http.ListenAndServe(":http", m.HTTPHandler(nil))
+ if host, _, err := net.SplitHostPort(addr); err != nil {
+ l4g.Error("Unable to setup forwarding: " + err.Error())
} else {
- go func() {
- redirectListener, err := net.Listen("tcp", ":80")
- if err != nil {
- listener.Close()
- l4g.Error("Unable to setup forwarding: " + err.Error())
- return
- }
- defer redirectListener.Close()
+ httpListenAddress := net.JoinHostPort(host, "http")
- http.Serve(redirectListener, http.HandlerFunc(redirectHTTPToHTTPS))
- }()
+ if *a.Config().ServiceSettings.UseLetsEncrypt {
+ go http.ListenAndServe(httpListenAddress, m.HTTPHandler(nil))
+ } else {
+ go func() {
+ redirectListener, err := net.Listen("tcp", httpListenAddress)
+ if err != nil {
+ l4g.Error("Unable to setup forwarding: " + err.Error())
+ return
+ }
+ defer redirectListener.Close()
+
+ http.Serve(redirectListener, http.HandlerFunc(redirectHTTPToHTTPS))
+ }()
+ }
}
}
diff --git a/app/team.go b/app/team.go
index dc10760f8..a6e79e9a6 100644
--- a/app/team.go
+++ b/app/team.go
@@ -4,13 +4,18 @@
package app
import (
+ "bytes"
"fmt"
+ "image"
+ "image/png"
+ "mime/multipart"
"net/http"
"net/url"
"strconv"
"strings"
l4g "github.com/alecthomas/log4go"
+ "github.com/disintegration/imaging"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
@@ -306,10 +311,16 @@ func (a *App) joinUserToTeam(team *model.Team, user *model.User) (*model.TeamMem
return rtm, true, nil
}
- if tmr := <-a.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
- return nil, false, tmr.Err
+ if membersCount := <-a.Srv.Store.Team().GetActiveMemberCount(tm.TeamId); membersCount.Err != nil {
+ return nil, false, membersCount.Err
+ } else if membersCount.Data.(int64) >= int64(*a.Config().TeamSettings.MaxUsersPerTeam) {
+ return nil, false, model.NewAppError("joinUserToTeam", "app.team.join_user_to_team.max_accounts.app_error", nil, "teamId="+tm.TeamId, http.StatusBadRequest)
} else {
- return tmr.Data.(*model.TeamMember), false, nil
+ if tmr := <-a.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
+ return nil, false, tmr.Err
+ } else {
+ return tmr.Data.(*model.TeamMember), false, nil
+ }
}
} else {
// Membership appears to be missing. Lets try to add.
@@ -739,7 +750,7 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
- a.SendInviteEmails(team, user.GetDisplayName(nameFormat), emailList, utils.GetSiteURL())
+ a.SendInviteEmails(team, user.GetDisplayName(nameFormat), emailList, a.GetSiteURL())
return nil
}
@@ -917,3 +928,88 @@ func (a *App) SanitizeTeams(session model.Session, teams []*model.Team) []*model
return teams
}
+
+func (a *App) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) {
+ if len(*a.Config().FileSettings.DriverName) == 0 {
+ return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.filesettings_no_driver.app_error", nil, "", http.StatusNotImplemented)
+ } else {
+ path := "teams/" + team.Id + "/teamIcon.png"
+ if data, err := a.ReadFile(path); err != nil {
+ return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.read_file.app_error", nil, err.Error(), http.StatusNotFound)
+ } else {
+ return data, nil
+ }
+ }
+}
+
+func (a *App) SetTeamIcon(teamId string, imageData *multipart.FileHeader) *model.AppError {
+ file, err := imageData.Open()
+ if err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, err.Error(), http.StatusBadRequest)
+ }
+ defer file.Close()
+ return a.SetTeamIconFromFile(teamId, file)
+}
+
+func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.AppError {
+
+ team, getTeamErr := a.GetTeam(teamId)
+
+ if getTeamErr != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.get_team.app_error", nil, getTeamErr.Error(), http.StatusBadRequest)
+ }
+
+ if len(*a.Config().FileSettings.DriverName) == 0 {
+ return model.NewAppError("setTeamIcon", "api.team.set_team_icon.storage.app_error", nil, "", http.StatusNotImplemented)
+ }
+
+ // Decode image config first to check dimensions before loading the whole thing into memory later on
+ config, _, err := image.DecodeConfig(file)
+ if err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode_config.app_error", nil, err.Error(), http.StatusBadRequest)
+ } else if config.Width*config.Height > model.MaxImageSize {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, err.Error(), http.StatusBadRequest)
+ }
+
+ file.Seek(0, 0)
+
+ // Decode image into Image object
+ img, _, err := image.Decode(file)
+ if err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, err.Error(), http.StatusBadRequest)
+ }
+
+ file.Seek(0, 0)
+
+ orientation, _ := getImageOrientation(file)
+ img = makeImageUpright(img, orientation)
+
+ // Scale team icon
+ teamIconWidthAndHeight := 128
+ img = imaging.Fill(img, teamIconWidthAndHeight, teamIconWidthAndHeight, imaging.Center, imaging.Lanczos)
+
+ buf := new(bytes.Buffer)
+ err = png.Encode(buf, img)
+ if err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+
+ path := "teams/" + teamId + "/teamIcon.png"
+
+ if err := a.WriteFile(buf.Bytes(), path); err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.write_file.app_error", nil, "", http.StatusInternalServerError)
+ }
+
+ curTime := model.GetMillis()
+
+ if result := <-a.Srv.Store.Team().UpdateLastTeamIconUpdate(teamId, curTime); result.Err != nil {
+ return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.update.app_error", nil, result.Err.Error(), http.StatusBadRequest)
+ }
+
+ // manually set time to avoid possible cluster inconsistencies
+ team.LastTeamIconUpdate = curTime
+
+ a.sendTeamEvent(team, model.WEBSOCKET_EVENT_UPDATE_TEAM)
+
+ return nil
+}
diff --git a/app/team_test.go b/app/team_test.go
index 7cb20b6f6..cdfec12da 100644
--- a/app/team_test.go
+++ b/app/team_test.go
@@ -460,3 +460,91 @@ func TestAddUserToTeamByHashMismatchedInviteId(t *testing.T) {
assert.Nil(t, team)
assert.Equal(t, "api.user.create_user.signup_link_mismatched_invite_id.app_error", err.Id)
}
+
+func TestJoinUserToTeam(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ id := model.NewId()
+ team := &model.Team{
+ DisplayName: "dn_" + id,
+ Name: "name" + id,
+ Email: "success+" + id + "@simulator.amazonses.com",
+ Type: model.TEAM_OPEN,
+ }
+
+ if _, err := th.App.CreateTeam(team); err != nil {
+ t.Log(err)
+ t.Fatal("Should create a new team")
+ }
+
+ maxUsersPerTeam := th.App.Config().TeamSettings.MaxUsersPerTeam
+ defer func() {
+ th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = maxUsersPerTeam })
+ th.App.PermanentDeleteTeam(team)
+ }()
+ one := 1
+ th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxUsersPerTeam = &one })
+
+ t.Run("new join", func(t *testing.T) {
+ user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser, _ := th.App.CreateUser(&user)
+ defer th.App.PermanentDeleteUser(&user)
+
+ if _, alreadyAdded, err := th.App.joinUserToTeam(team, ruser); alreadyAdded || err != nil {
+ t.Fatal("Should return already added equal to false and no error")
+ }
+ })
+
+ t.Run("join when you are a member", func(t *testing.T) {
+ user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser, _ := th.App.CreateUser(&user)
+ defer th.App.PermanentDeleteUser(&user)
+
+ th.App.joinUserToTeam(team, ruser)
+ if _, alreadyAdded, err := th.App.joinUserToTeam(team, ruser); !alreadyAdded || err != nil {
+ t.Fatal("Should return already added and no error")
+ }
+ })
+
+ t.Run("re-join after leaving", func(t *testing.T) {
+ user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser, _ := th.App.CreateUser(&user)
+ defer th.App.PermanentDeleteUser(&user)
+
+ th.App.joinUserToTeam(team, ruser)
+ th.App.LeaveTeam(team, ruser, ruser.Id)
+ if _, alreadyAdded, err := th.App.joinUserToTeam(team, ruser); alreadyAdded || err != nil {
+ t.Fatal("Should return already added equal to false and no error")
+ }
+ })
+
+ t.Run("new join with limit problem", func(t *testing.T) {
+ user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser1, _ := th.App.CreateUser(&user1)
+ user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser2, _ := th.App.CreateUser(&user2)
+ defer th.App.PermanentDeleteUser(&user1)
+ defer th.App.PermanentDeleteUser(&user2)
+ th.App.joinUserToTeam(team, ruser1)
+ if _, _, err := th.App.joinUserToTeam(team, ruser2); err == nil {
+ t.Fatal("Should fail")
+ }
+ })
+
+ t.Run("re-join alfter leaving with limit problem", func(t *testing.T) {
+ user1 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser1, _ := th.App.CreateUser(&user1)
+ user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: "passwd1", AuthService: ""}
+ ruser2, _ := th.App.CreateUser(&user2)
+ defer th.App.PermanentDeleteUser(&user1)
+ defer th.App.PermanentDeleteUser(&user2)
+
+ th.App.joinUserToTeam(team, ruser1)
+ th.App.LeaveTeam(team, ruser1, ruser1.Id)
+ th.App.joinUserToTeam(team, ruser2)
+ if _, _, err := th.App.joinUserToTeam(team, ruser1); err == nil {
+ t.Fatal("Should fail")
+ }
+ })
+}
diff --git a/app/user.go b/app/user.go
index 70ed83c0b..2b160f9f5 100644
--- a/app/user.go
+++ b/app/user.go
@@ -106,7 +106,7 @@ func (a *App) CreateUserWithInviteId(user *model.User, inviteId string) (*model.
a.AddDirectChannels(team.Id, ruser)
- if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
@@ -119,7 +119,7 @@ func (a *App) CreateUserAsAdmin(user *model.User) (*model.User, *model.AppError)
return nil, err
}
- if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
@@ -143,7 +143,7 @@ func (a *App) CreateUserFromSignup(user *model.User) (*model.User, *model.AppErr
return nil, err
}
- if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
@@ -1027,7 +1027,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
if sendNotifications {
if rusers[0].Email != rusers[1].Email {
a.Go(func() {
- if err := a.SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1041,7 +1041,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
if rusers[0].Username != rusers[1].Username {
a.Go(func() {
- if err := a.SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1091,7 +1091,7 @@ func (a *App) UpdateMfa(activate bool, userId, token string) *model.AppError {
return
}
- if err := a.SendMfaChangeEmail(user.Email, activate, user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1129,7 +1129,7 @@ func (a *App) UpdatePasswordSendEmail(user *model.User, newPassword, method stri
}
a.Go(func() {
- if err := a.SendPasswordChangeEmail(user.Email, method, user.Locale, utils.GetSiteURL()); err != nil {
+ if err := a.SendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
})
@@ -1346,9 +1346,9 @@ func (a *App) SendEmailVerification(user *model.User) *model.AppError {
}
if _, err := a.GetStatus(user.Id); err != nil {
- return a.SendVerifyEmail(user.Email, user.Locale, utils.GetSiteURL(), token.Token)
+ return a.SendVerifyEmail(user.Email, user.Locale, a.GetSiteURL(), token.Token)
} else {
- return a.SendEmailChangeVerifyEmail(user.Email, user.Locale, utils.GetSiteURL(), token.Token)
+ return a.SendEmailChangeVerifyEmail(user.Email, user.Locale, a.GetSiteURL(), token.Token)
}
}
diff --git a/app/web_hub.go b/app/web_hub.go
index eeae13e09..c1c8cb7bb 100644
--- a/app/web_hub.go
+++ b/app/web_hub.go
@@ -30,7 +30,6 @@ type Hub struct {
// See https://github.com/mattermost/mattermost-server/pull/7281
connectionCount int64
app *App
- connections []*WebConn
connectionIndex int
register chan *WebConn
unregister chan *WebConn
@@ -47,7 +46,6 @@ func (a *App) NewWebHub() *Hub {
app: a,
register: make(chan *WebConn, 1),
unregister: make(chan *WebConn, 1),
- connections: make([]*WebConn, 0, model.SESSION_CACHE_SIZE),
broadcast: make(chan *model.WebSocketEvent, BROADCAST_QUEUE_SIZE),
stop: make(chan struct{}),
didStop: make(chan struct{}),
@@ -170,8 +168,14 @@ func (a *App) Publish(message *model.WebSocketEvent) {
}
func (a *App) PublishSkipClusterSend(message *model.WebSocketEvent) {
- for _, hub := range a.Hubs {
- hub.Broadcast(message)
+ if message.Broadcast.UserId != "" {
+ if len(a.Hubs) != 0 {
+ a.GetHubForUserId(message.Broadcast.UserId).Broadcast(message)
+ }
+ } else {
+ for _, hub := range a.Hubs {
+ hub.Broadcast(message)
+ }
}
}
@@ -362,80 +366,53 @@ func (h *Hub) Start() {
var doRecover func()
doStart = func() {
-
h.goroutineId = getGoroutineId()
l4g.Debug("Hub for index %v is starting with goroutine %v", h.connectionIndex, h.goroutineId)
+ connections := newHubConnectionIndex()
+
for {
select {
case webCon := <-h.register:
- h.connections = append(h.connections, webCon)
- atomic.StoreInt64(&h.connectionCount, int64(len(h.connections)))
-
+ connections.Add(webCon)
+ atomic.StoreInt64(&h.connectionCount, int64(len(connections.All())))
case webCon := <-h.unregister:
- userId := webCon.UserId
-
- found := false
- indexToDel := -1
- for i, webConCandidate := range h.connections {
- if webConCandidate == webCon {
- indexToDel = i
- continue
- }
- if userId == webConCandidate.UserId {
- found = true
- if indexToDel != -1 {
- break
- }
- }
- }
+ connections.Remove(webCon)
- if indexToDel != -1 {
- // Delete the webcon we are unregistering
- h.connections[indexToDel] = h.connections[len(h.connections)-1]
- h.connections = h.connections[:len(h.connections)-1]
- }
-
- if len(userId) == 0 {
+ if len(webCon.UserId) == 0 {
continue
}
- if !found {
+ if len(connections.ForUser(webCon.UserId)) == 0 {
h.app.Go(func() {
- h.app.SetStatusOffline(userId, false)
+ h.app.SetStatusOffline(webCon.UserId, false)
})
}
-
case userId := <-h.invalidateUser:
- for _, webCon := range h.connections {
- if webCon.UserId == userId {
- webCon.InvalidateCache()
- }
+ for _, webCon := range connections.ForUser(userId) {
+ webCon.InvalidateCache()
}
-
case msg := <-h.broadcast:
- for _, webCon := range h.connections {
+ candidates := connections.All()
+ if msg.Broadcast.UserId != "" {
+ candidates = connections.ForUser(msg.Broadcast.UserId)
+ }
+ msg.PrecomputeJSON()
+ for _, webCon := range candidates {
if webCon.ShouldSendEvent(msg) {
select {
case webCon.Send <- msg:
default:
l4g.Error(fmt.Sprintf("webhub.broadcast: cannot send, closing websocket for userId=%v", webCon.UserId))
close(webCon.Send)
- for i, webConCandidate := range h.connections {
- if webConCandidate == webCon {
- h.connections[i] = h.connections[len(h.connections)-1]
- h.connections = h.connections[:len(h.connections)-1]
- break
- }
- }
+ connections.Remove(webCon)
}
}
}
-
case <-h.stop:
userIds := make(map[string]bool)
- for _, webCon := range h.connections {
+ for _, webCon := range connections.All() {
userIds[webCon.UserId] = true
webCon.Close()
}
@@ -444,7 +421,6 @@ func (h *Hub) Start() {
h.app.SetStatusOffline(userId, false)
}
- h.connections = make([]*WebConn, 0, model.SESSION_CACHE_SIZE)
h.ExplicitStop = true
close(h.didStop)
@@ -474,3 +450,60 @@ func (h *Hub) Start() {
go doRecoverableStart()
}
+
+type hubConnectionIndexIndexes struct {
+ connections int
+ connectionsByUserId int
+}
+
+// hubConnectionIndex provides fast addition, removal, and iteration of web connections.
+type hubConnectionIndex struct {
+ connections []*WebConn
+ connectionsByUserId map[string][]*WebConn
+ connectionIndexes map[*WebConn]*hubConnectionIndexIndexes
+}
+
+func newHubConnectionIndex() *hubConnectionIndex {
+ return &hubConnectionIndex{
+ connections: make([]*WebConn, 0, model.SESSION_CACHE_SIZE),
+ connectionsByUserId: make(map[string][]*WebConn),
+ connectionIndexes: make(map[*WebConn]*hubConnectionIndexIndexes),
+ }
+}
+
+func (i *hubConnectionIndex) Add(wc *WebConn) {
+ i.connections = append(i.connections, wc)
+ i.connectionsByUserId[wc.UserId] = append(i.connectionsByUserId[wc.UserId], wc)
+ i.connectionIndexes[wc] = &hubConnectionIndexIndexes{
+ connections: len(i.connections) - 1,
+ connectionsByUserId: len(i.connectionsByUserId[wc.UserId]) - 1,
+ }
+}
+
+func (i *hubConnectionIndex) Remove(wc *WebConn) {
+ indexes, ok := i.connectionIndexes[wc]
+ if !ok {
+ return
+ }
+
+ last := i.connections[len(i.connections)-1]
+ i.connections[indexes.connections] = last
+ i.connections = i.connections[:len(i.connections)-1]
+ i.connectionIndexes[last].connections = indexes.connections
+
+ userConnections := i.connectionsByUserId[wc.UserId]
+ last = userConnections[len(userConnections)-1]
+ userConnections[indexes.connectionsByUserId] = last
+ i.connectionsByUserId[wc.UserId] = userConnections[:len(userConnections)-1]
+ i.connectionIndexes[last].connectionsByUserId = indexes.connectionsByUserId
+
+ delete(i.connectionIndexes, wc)
+}
+
+func (i *hubConnectionIndex) ForUser(id string) []*WebConn {
+ return i.connectionsByUserId[id]
+}
+
+func (i *hubConnectionIndex) All() []*WebConn {
+ return i.connections
+}
diff --git a/build/release.mk b/build/release.mk
index 5eaee8022..8bd4f9afd 100644
--- a/build/release.mk
+++ b/build/release.mk
@@ -20,9 +20,7 @@ build-client:
cd $(BUILD_WEBAPP_DIR) && $(MAKE) build
-package:
- @ echo Packaging mattermost
-
+package-common:
@# Remove any old files
rm -Rf $(DIST_ROOT)
@@ -59,9 +57,7 @@ endif
cp NOTICE.txt $(DIST_PATH)
cp README.md $(DIST_PATH)
- @# ----- PLATFORM SPECIFIC -----
-
- @# Make osx package
+package-osx: package-common
@# Copy binary
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_amd64")
cp $(GOPATH)/bin/platform $(DIST_PATH)/bin # from native bin dir, not cross-compiled
@@ -73,7 +69,7 @@ endif
@# Cleanup
rm -f $(DIST_PATH)/bin/platform
- @# Make windows package
+package-windows: package-common
@# Copy binary
ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64")
cp $(GOPATH)/bin/platform.exe $(DIST_PATH)/bin # from native bin dir, not cross-compiled
@@ -85,7 +81,7 @@ endif
@# Cleanup
rm -f $(DIST_PATH)/bin/platform.exe
- @# Make linux package
+package-linux: package-common
@# Copy binary
ifeq ($(BUILDER_GOOS_GOARCH),"linux_amd64")
cp $(GOPATH)/bin/platform $(DIST_PATH)/bin # from native bin dir, not cross-compiled
@@ -96,3 +92,5 @@ endif
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-linux-amd64.tar.gz mattermost
@# Don't clean up native package so dev machines will have an unzipped package available
@#rm -f $(DIST_PATH)/bin/platform
+
+package: package-linux package-windows package-osx
diff --git a/cmd/platform/user.go b/cmd/platform/user.go
index 0913609f1..e2a8c9748 100644
--- a/cmd/platform/user.go
+++ b/cmd/platform/user.go
@@ -3,9 +3,12 @@
package main
import (
+ "encoding/json"
"errors"
"fmt"
+ "io/ioutil"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/model"
"github.com/spf13/cobra"
@@ -87,25 +90,41 @@ var deleteAllUsersCmd = &cobra.Command{
}
var migrateAuthCmd = &cobra.Command{
- Use: "migrate_auth [from_auth] [to_auth] [match_field]",
- Short: "Mass migrate user accounts authentication type",
- Long: `Migrates accounts from one authentication provider to another. For example, you can upgrade your authentication provider from email to ldap.
+ Use: "migrate_auth [from_auth] [to_auth] [migration-options]",
+ Short: "Mass migrate user accounts authentication type",
+ Long: `Migrates accounts from one authentication provider to another. For example, you can upgrade your authentication provider from email to ldap.`,
+ Example: " user migrate_auth email saml users.json",
+ Args: func(cmd *cobra.Command, args []string) error {
+ if len(args) < 2 {
+ return errors.New("Auth migration requires at least 2 arguments.")
+ }
-from_auth:
- The authentication service to migrate users accounts from.
- Supported options: email, gitlab, saml.
+ toAuth := args[1]
-to_auth:
- The authentication service to migrate users to.
- Supported options: ldap.
+ if toAuth != "ldap" && toAuth != "saml" {
+ return errors.New("Invalid to_auth parameter, must be saml or ldap.")
+ }
-match_field:
- The field that is guaranteed to be the same in both authentication services. For example, if the users emails are consistent set to email.
- Supported options: email, username.
+ if toAuth == "ldap" && len(args) != 3 {
+ return errors.New("Ldap migration requires 3 arguments.")
+ }
-Will display any accounts that are not migrated successfully.`,
- Example: " user migrate_auth email ladp email",
- RunE: migrateAuthCmdF,
+ autoFlag, _ := cmd.Flags().GetBool("auto")
+
+ if toAuth == "saml" && autoFlag {
+ if len(args) != 2 {
+ return errors.New("Saml migration requires two arguments when using the --auto flag. See help text for details.")
+ }
+ }
+
+ if toAuth == "saml" && !autoFlag {
+ if len(args) != 3 {
+ return errors.New("Saml migration requires three arguments when not using the --auto flag. See help text for details.")
+ }
+ }
+ return nil
+ },
+ RunE: migrateAuthCmdF,
}
var verifyUserCmd = &cobra.Command{
@@ -138,7 +157,69 @@ func init() {
deleteAllUsersCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.")
- migrateAuthCmd.Flags().Bool("force", false, "Force the migration to occour even if there are duplicates on the LDAP server. Duplicates will not be migrated.")
+ migrateAuthCmd.Flags().Bool("force", false, "Force the migration to occur even if there are duplicates on the LDAP server. Duplicates will not be migrated. (ldap only)")
+ migrateAuthCmd.Flags().Bool("auto", false, "Automatically migrate all users. Assumes the usernames and emails are identical between Mattermost and SAML services. (saml only)")
+ migrateAuthCmd.Flags().Bool("dryRun", false, "Run a simulation of the migration process without changing the database.")
+ migrateAuthCmd.SetUsageTemplate(`Usage:
+ platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags]
+
+Examples:
+{{.Example}}
+
+Arguments:
+ from_auth:
+ The authentication service to migrate users accounts from.
+ Supported options: email, gitlab, ldap, saml.
+
+ to_auth:
+ The authentication service to migrate users to.
+ Supported options: ldap, saml.
+
+ migration-options:
+ Migration specific options, full command help for more information.
+
+Flags:
+{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}
+
+Global Flags:
+{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}
+`)
+ migrateAuthCmd.SetHelpTemplate(`Usage:
+ platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags]
+
+Examples:
+{{.Example}}
+
+Arguments:
+ from_auth:
+ The authentication service to migrate users accounts from.
+ Supported options: email, gitlab, ldap, saml.
+
+ to_auth:
+ The authentication service to migrate users to.
+ Supported options: ldap, saml.
+
+ migration-options (ldap):
+ match_field:
+ The field that is guaranteed to be the same in both authentication services. For example, if the users emails are consistent set to email.
+ Supported options: email, username.
+
+ migration-options (saml):
+ users_file:
+ The path of a json file with the usernames and emails of all users to migrate to SAML. The username and email must be the same that the SAML service provider store. And the email must match with the email in mattermost database.
+
+ Example json content:
+ {
+ "usr1@email.com": "usr.one",
+ "usr2@email.com": "usr.two"
+ }
+
+Flags:
+{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}
+
+Global Flags:
+{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}
+`)
userCmd.AddCommand(
userActivateCmd,
@@ -416,27 +497,25 @@ func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error {
}
func migrateAuthCmdF(cmd *cobra.Command, args []string) error {
+ if args[1] == "saml" {
+ return migrateAuthToSamlCmdF(cmd, args)
+ }
+ return migrateAuthToLdapCmdF(cmd, args)
+}
+
+func migrateAuthToLdapCmdF(cmd *cobra.Command, args []string) error {
a, err := initDBCommandContextCobra(cmd)
if err != nil {
return err
}
- if len(args) != 3 {
- return errors.New("Expected three arguments. See help text for details.")
- }
-
fromAuth := args[0]
- toAuth := args[1]
- matchField := args[2]
+ matchField := args[1]
if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "saml") {
return errors.New("Invalid from_auth argument")
}
- if len(toAuth) == 0 || toAuth != "ldap" {
- return errors.New("Invalid to_auth argument")
- }
-
// Email auth in Mattermost system is represented by ""
if fromAuth == "email" {
fromAuth = ""
@@ -447,9 +526,10 @@ func migrateAuthCmdF(cmd *cobra.Command, args []string) error {
}
forceFlag, _ := cmd.Flags().GetBool("force")
+ dryRunFlag, _ := cmd.Flags().GetBool("dryRun")
if migrate := a.AccountMigration; migrate != nil {
- if err := migrate.MigrateToLdap(fromAuth, matchField, forceFlag); err != nil {
+ if err := migrate.MigrateToLdap(fromAuth, matchField, forceFlag, dryRunFlag); err != nil {
return errors.New("Error while migrating users: " + err.Error())
}
@@ -459,6 +539,61 @@ func migrateAuthCmdF(cmd *cobra.Command, args []string) error {
return nil
}
+func migrateAuthToSamlCmdF(cmd *cobra.Command, args []string) error {
+ a, err := initDBCommandContextCobra(cmd)
+ if err != nil {
+ return err
+ }
+
+ dryRunFlag, _ := cmd.Flags().GetBool("dryRun")
+ autoFlag, _ := cmd.Flags().GetBool("auto")
+
+ matchesFile := ""
+ matches := map[string]string{}
+ if !autoFlag {
+ matchesFile = args[1]
+
+ file, e := ioutil.ReadFile(matchesFile)
+ if e != nil {
+ return errors.New("Invalid users file.")
+ }
+ if json.Unmarshal(file, &matches) != nil {
+ return errors.New("Invalid users file.")
+ }
+ }
+
+ fromAuth := args[0]
+
+ if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "ldap") {
+ return errors.New("Invalid from_auth argument")
+ }
+
+ if autoFlag && !dryRunFlag {
+ var confirm string
+ CommandPrettyPrintln("You are about to perform an automatic \"" + fromAuth + " to saml\" migration. This must only be done if your current Mattermost users with " + fromAuth + " auth have the same username and email in your SAML service. Otherwise, provide the usernames and emails from your SAML Service using the \"users file\" without the \"--auto\" option.\n\nDo you want to proceed with automatic migration anyway? (YES/NO):")
+ fmt.Scanln(&confirm)
+
+ if confirm != "YES" {
+ return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
+ }
+ }
+
+ // Email auth in Mattermost system is represented by ""
+ if fromAuth == "email" {
+ fromAuth = ""
+ }
+
+ if migrate := a.AccountMigration; migrate != nil {
+ if err := migrate.MigrateToSaml(fromAuth, matches, autoFlag, dryRunFlag); err != nil {
+ return errors.New("Error while migrating users: " + err.Error())
+ }
+ l4g.Close()
+ CommandPrettyPrintln("Sucessfully migrated accounts.")
+ }
+
+ return nil
+}
+
func verifyUserCmdF(cmd *cobra.Command, args []string) error {
a, err := initDBCommandContextCobra(cmd)
if err != nil {
diff --git a/config/default.json b/config/default.json
index 0fa8f235f..82b5fc57d 100644
--- a/config/default.json
+++ b/config/default.json
@@ -1,6 +1,7 @@
{
"ServiceSettings": {
"SiteURL": "http://localhost:8065",
+ "WebsocketURL": "",
"LicenseFileLocation": "",
"ListenAddress": ":8065",
"ConnectionSecurity": "",
@@ -21,7 +22,7 @@
"EnableOnlyAdminIntegrations": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
- "EnableAPIv3": true,
+ "EnableAPIv3": false,
"EnableLinkPreviews": false,
"EnableTesting": false,
"EnableDeveloper": false,
@@ -32,6 +33,7 @@
"EnforceMultifactorAuthentication": false,
"EnableUserAccessTokens": false,
"AllowCorsFrom": "",
+ "AllowCookiesForSubdomains": false,
"SessionLengthWebInDays": 30,
"SessionLengthMobileInDays": 30,
"SessionLengthSSOInDays": 30,
diff --git a/einterfaces/account_migration.go b/einterfaces/account_migration.go
index 0db516d75..ab61544ff 100644
--- a/einterfaces/account_migration.go
+++ b/einterfaces/account_migration.go
@@ -6,5 +6,6 @@ package einterfaces
import "github.com/mattermost/mattermost-server/model"
type AccountMigrationInterface interface {
- MigrateToLdap(fromAuthService string, forignUserFieldNameToMatch string, force bool) *model.AppError
+ MigrateToLdap(fromAuthService string, forignUserFieldNameToMatch string, force bool, dryRun bool) *model.AppError
+ MigrateToSaml(fromAuthService string, usersMap map[string]string, auto bool, dryRun bool) *model.AppError
}
diff --git a/i18n/de.json b/i18n/de.json
index b506e7018..b58cd73b1 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -132,10 +132,6 @@
"translation": "Dateiupload nicht möglich. Datei ist zu groß."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Vorlagen in %v werden verarbeitet"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Das Server-Template %v konnte nicht verarbeitet werden"
},
@@ -2184,7 +2180,7 @@
},
{
"id": "api.team.add_user_to_team.added",
- "translation": "%v wurde von %v zum Team hinzugefügt"
+ "translation": "%v wurde von %v zum Team hinzugefügt."
},
{
"id": "api.team.add_user_to_team.missing_parameter.app_error",
@@ -2203,10 +2199,6 @@
"translation": "Der Link ist abgelaufen"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "Der Einladungslink scheint nicht gültig zu sein"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "Diese URL ist nicht verfügbar. Bitte wähle eine andere."
},
@@ -2307,6 +2299,14 @@
"translation": "%v hat das Team verlassen."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Fehler beim Senden des Kanalzwecks"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Versuche Team permanent zu löschen %v id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "Der Registrierungslink scheint nicht gültig zu sein"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "Der Einladungslink scheint nicht gültig zu sein"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Ungültiger Teamname"
},
@@ -3067,6 +3071,10 @@
"translation": "Konnte Payload des eingehenden Webhooks nicht verarbeiten."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Initialisiere Webhook-API-Routen"
},
@@ -3691,6 +3699,10 @@
"translation": "Plugins und/oder Plugin-Uploads wurden deaktiviert."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Dieses Team hat die maximale Anzahl erlaubter Konten erreicht. Kontaktieren Sie Ihren Systemadministrator, um eine höhere Begrenzung setzen zu lassen."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Persönliche Zugriffs-Token sind auf diesem Server deaktiviert. Bitte kontaktieren Sie ihren Systemadministrator für Details."
},
@@ -4759,6 +4771,10 @@
"translation": "Ungültige Thumbnauilbreite in Dateieinstellungen. Muss eine positive Zahl sein."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Ungültige Diensteinstellungen für Gruppierung von ungelesenen Kanälen. Muss 'disabled', 'default_on' oder default_off' sein."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Ungültiger Bild-Proxy-Typ für Diensteinstellungen."
},
@@ -4867,6 +4883,10 @@
"translation": "Nachrichten-Export-Aufgabe \"ExportFromTimestamp\" muss ein Zeitstempel (angegeben in Sekunden seit der Unix-Epoche) sein. Nur Nachrichten nach diesem Zeitpunkt werden exportiert."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Nachrichten-Export-Aufgabe \"FileLocation\" muss ein beschreibbares Verzeichnis sein in welches die Exportdaten geschrieben werden"
},
@@ -4875,6 +4895,10 @@
"translation": "Nachrichten-Export-Aufgabe \"FileLocation\" muss ein Unterverzeichnis von \"FileSettings.Directory\" sein"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Minimale Passwortlänge muss eine ganze Zahl größer oder gleich zu {{.MinLength}} und weniger oder gleich zu {{.MaxLength}} sein."
},
@@ -7087,6 +7111,10 @@
"translation": "Fehler beim Öffnen einer Verbindung zum SMTP-Server %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Fehler beim Schließen der Verbindung zum SMTP-Server"
},
@@ -7155,10 +7183,6 @@
"translation": "Fehler beim Erstellen der Ordner Überwachung %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Fehler beim Erstellen der Ordner Überwachung %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Fehler beim Finden des Benutzerprofils mit id=%v erzwinge Abmeldung"
},
@@ -7263,18 +7287,10 @@
"translation": "Templates in %v werden geparst"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Die Templates in %v konnte nicht verarbeitet werden"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "Ungültige Nachrichten-ID"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Erneute Analyze des Templates aufgrund veränderter Datei %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "Der Link zum zurücksetzen des Passwortes ist abgelaufen"
},
@@ -7291,10 +7307,6 @@
"translation": "Registrieren"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "Der Registrierungslink scheint nicht gültig zu sein"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "Der Registrierungslink ist abgelaufen"
},
@@ -7311,10 +7323,6 @@
"translation": "Der Registrierungslink ist abgelaufen"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "Der Registrierungslink scheint nicht gültig zu sein"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "Dieser Teamtyp erlaubt keine offnen Einladungen"
},
diff --git a/i18n/en.json b/i18n/en.json
index 549537735..bb906ae6c 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -112,6 +112,18 @@
"translation": "Mattermost - Testing Email Settings"
},
{
+ "id": "api.admin.test_s3.missing_s3_bucket",
+ "translation": "S3 Bucket is required"
+ },
+ {
+ "id": "api.admin.test_s3.missing_s3_region",
+ "translation": "S3 Region is required"
+ },
+ {
+ "id": "api.admin.test_s3.missing_s3_endpoint",
+ "translation": "S3 Endpoint is required"
+ },
+ {
"id": "api.admin.upload_brand_image.array.app_error",
"translation": "Empty array under 'image' in request"
},
@@ -1373,6 +1385,10 @@
"translation": "Unable to upload file. Header cannot be parsed."
},
{
+ "id": "api.file.upload_file.incorrect_number_of_files.app_error",
+ "translation": "Unable to upload files. Incorrect number of files specified."
+ },
+ {
"id": "api.file.upload_file.large_image.app_error",
"translation": "File above maximum dimensions could not be uploaded: {{.Filename}}"
},
@@ -1812,11 +1828,15 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " Uploaded one or more files in "
+ "translation": " uploaded one or more files in "
+ },
+ {
+ "id": "api.post.send_notifications_and_forget.push_image_only_no_channel",
+ "translation": " uploaded one or more files"
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
- "translation": " Uploaded one or more files in a direct message"
+ "translation": " uploaded one or more files in a direct message"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -2183,6 +2203,50 @@
"translation": "The number of running goroutines is over the health threshold %v of %v"
},
{
+ "id": "api.team.set_team_icon.get_team.app_error",
+ "translation": "An error occurred getting the team"
+ },
+ {
+ "id": "api.team.set_team_icon.storage.app_error",
+ "translation": "Unable to upload team icon. Image storage is not configured."
+ },
+ {
+ "id": "api.team.set_team_icon.too_large.app_error",
+ "translation": "Unable to upload team icon. File is too large."
+ },
+ {
+ "id": "api.team.set_team_icon.parse.app_error",
+ "translation": "Could not parse multipart form"
+ },
+ {
+ "id": "api.team.set_team_icon.no_file.app_error",
+ "translation": "No file under 'image' in request"
+ },
+ {
+ "id": "api.team.set_team_icon.array.app_error",
+ "translation": "Empty array under 'image' in request"
+ },
+ {
+ "id": "api.team.set_team_icon.open.app_error",
+ "translation": "Could not open image file"
+ },
+ {
+ "id": "api.team.set_team_icon.decode_config.app_error",
+ "translation": "Could not decode team icon metadata"
+ },
+ {
+ "id": "api.team.set_team_icon.decode.app_error",
+ "translation": "Could not decode team icon"
+ },
+ {
+ "id": "api.team.set_team_icon.encode.app_error",
+ "translation": "Could not encode team icon"
+ },
+ {
+ "id": "api.team.set_team_icon.write_file.app_error",
+ "translation": "Could not save team icon"
+ },
+ {
"id": "api.team.add_user_to_team.added",
"translation": "%v added to the team by %v."
},
@@ -2303,6 +2367,14 @@
"translation": "%v left the team."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Failed to post channel move message."
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Attempting to permanently delete team %v id=%v"
},
@@ -2711,11 +2783,11 @@
"translation": "The signup link has expired"
},
{
- "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "id": "api.user.create_user.signup_link_invalid.app_error",
"translation": "The signup link does not appear to be valid"
},
{
- "id": "api.user.create_user.signup_link_invalid.app_error",
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
"translation": "The signup link does not appear to be valid"
},
{
@@ -3147,14 +3219,6 @@
"translation": "Cannot move a channel unless all its members are already members of the destination team."
},
{
- "id": "api.team.move_channel.success",
- "translation": "This channel has been moved to this team from %v."
- },
- {
- "id": "api.team.move_channel.post.error",
- "translation": "Failed to post channel move message."
- },
- {
"id": "app.channel.post_update_channel_purpose_message.post.error",
"translation": "Failed to post channel purpose message"
},
@@ -3660,7 +3724,7 @@
},
{
"id": "app.plugin.disabled.app_error",
- "translation": "Plugins have been disabled."
+ "translation": "Plugins have been disabled. Please check your logs for details."
},
{
"id": "app.plugin.extract.app_error",
@@ -3711,6 +3775,10 @@
"translation": "Plugins and/or plugin uploads have been disabled."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "This team has reached the maximum number of allowed accounts. Contact your systems administrator to set a higher limit."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Personal access tokens are disabled on this server. Please contact your system administrator for details."
},
@@ -4191,6 +4259,18 @@
"translation": "Unable to find user on AD/LDAP server: "
},
{
+ "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file",
+ "translation": "User not found in the users file."
+ },
+ {
+ "id": "ent.migration.migratetosaml.username_already_used_by_other_user",
+ "translation": "Username already used by another Mattermost user."
+ },
+ {
+ "id": "ent.migration.migratetosaml.email_already_used_by_other_user",
+ "translation": "Email already used by another SAML user."
+ },
+ {
"id": "ent.saml.attribute.app_error",
"translation": "SAML login was unsuccessful because one of the attributes is incorrect. Please contact your System Administrator."
},
@@ -4879,14 +4959,6 @@
"translation": "Message export job BatchSize must be a positive integer"
},
{
- "id": "model.config.is_valid.message_export.export_type.app_error",
- "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
- },
- {
- "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"
- },
- {
"id": "model.config.is_valid.message_export.daily_runtime.app_error",
"translation": "Message export job DailyRuntime must be a 24-hour time stamp in the form HH:MM."
},
@@ -4899,6 +4971,10 @@
"translation": "Message export job ExportFromTimestamp must be a timestamp (expressed in seconds since unix epoch). Only messages sent after this timestamp will be exported."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Message export job FileLocation must be a writable directory that export data will be written to"
},
@@ -4907,6 +4983,10 @@
"translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Minimum password length must be a whole number greater than or equal to {{.MinLength}} and less than or equal to {{.MaxLength}}."
},
@@ -4967,6 +5047,10 @@
"translation": "Site URL must be a valid URL and start with http:// or https://"
},
{
+ "id": "model.config.is_valid.websocket_url.app_error",
+ "translation": "Websocket URL must be a valid URL and start with ws:// or wss://"
+ },
+ {
"id": "model.config.is_valid.site_url_email_batching.app_error",
"translation": "Unable to enable email batching when SiteURL isn't set."
},
@@ -7131,10 +7215,6 @@
"translation": "Failed to authenticate on SMTP server"
},
{
- "id": "utils.mail.sendMail.attachments.write_error",
- "translation": "Failed to write attachment to email"
- },
- {
"id": "utils.mail.new_client.helo.error",
"translation": "Failed to to set the HELO to SMTP server %v"
},
@@ -7143,6 +7223,10 @@
"translation": "Failed to open a connection to SMTP server %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Failed to close connection to SMTP server"
},
diff --git a/i18n/es.json b/i18n/es.json
index c96674885..fbb0c8538 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -132,10 +132,6 @@
"translation": "No se pudo cargar el archivo. El archivo es muy grande."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Analizando el contenido plantillas del servidor en %v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Falla al analizar el contenido de las plantillas de servidor %v"
},
@@ -1812,11 +1808,11 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " Cargó uno o más archivos en "
+ "translation": " Subió uno o más archivos en "
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
- "translation": " Cargó uno o más archivos en un mensaje directo"
+ "translation": " subió uno o más archivos en un mensaje directo"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -2203,10 +2199,6 @@
"translation": "El enlace de registro ha expirado"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "El enlace de registro parece ser inválido"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "Este URL no está disponible. Por favor, prueba con otra."
},
@@ -2307,6 +2299,14 @@
"translation": "%v abandonó el equipo."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "No pudo publicar el mensaje de que el canal fue movido."
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "Este canal ha sido movido a este equipo desde %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Intentando eliminar permanentemente al equipo %v id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "El enlace de registro parece ser inválido"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "El enlace de registro parece ser inválido"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Nombre del equipo inválido"
},
@@ -3067,6 +3071,10 @@
"translation": "No se pudo leer la carga del webhook de entrada."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "No se pudo codificar la carga del multipart en el webhook entrante."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Inicializando rutas del API para los webhooks"
},
@@ -3691,6 +3699,10 @@
"translation": "Los Complementos y/o la carga de complementos han sido deshabilitados."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Este equipo ha alcanzado el número máximo de cuentas permitidas. Contacta a un administrador de sistema para que asigne un límite mayor."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Los tokens de acceso personal están inhabilitados en este servidor. Por favor, póngase en contacto con su administrador del sistema para obtener más detalles."
},
@@ -4759,6 +4771,10 @@
"translation": "El ancho para la imagen de miniatura es inválido en la configuración de archivos. Debe ser un número positivo."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Ajuste invalido para el agrupamiento de canales no leídos en la configuración de servicio. Debe ser 'disabled', 'default_on' o 'default_off'."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Tipo de proxy para las imágenes no válido en la configuración del servicio."
},
@@ -4867,6 +4883,10 @@
"translation": "El ajuste ExportFromTimestamp para realizar el trabajo de Exportación de Mensajes debe ser una marca de tiempo (expresada en segundos desde la época unix). Sólo los mensajes enviados después de esa fecha serán exportados."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "El trabajo de exportación de Mensaje ExportFormat debe ser 'actiance' o 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "El ajuste FileLocation para realizar el trabajo de Exportación de Mensajes debe ser un directorio con permiso de escritura donde la data de exportación será almacenada."
},
@@ -4875,6 +4895,10 @@
"translation": "El ajuste FileLocation para realizar el trabajo de Exportación de Mensajes debe ser un sub-directorio de FileSettings.Directory"
},
{
+ "id": "model.config.is_valid.message_export.global_relay_email_address.app_error",
+ "translation": "El trabajo de exportación de Mensaje GlobalRelayEmailAddress debe estar asignado a una dirección de correo electrónico válido."
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Longitud mínima de la contraseña debe ser un número entero mayor que o igual a {{.MinLength}} y menor o igual que {{.MaxLength}}."
},
@@ -7087,6 +7111,10 @@
"translation": "Falla al abrir la conexión al servidor SMTP %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Error al escribir el archivo adjunto al correo electrónico"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Falla al cerrar la conexión al servidor SMTP"
},
@@ -7155,10 +7183,6 @@
"translation": "Falla al crear el vigilante de directorio %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Falla al vigilar el directorio %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Error obteniendo el pérfil de usuario para id=%v forzando el cierre de sesión"
},
@@ -7263,18 +7287,10 @@
"translation": "Analizando el contenido de las plantillas en %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Falla al analizar el contenido de las plantillas %v"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "El ID del Mensaje es inválido"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Re-analizando el contenido de las plantillas porque el archivo %v fue modificado"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "El enlace de registro ha expirado"
},
@@ -7291,10 +7307,6 @@
"translation": "Registrar"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "El enlace de registro parece ser inválido"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "El enlace de registro ha expirado"
},
@@ -7311,10 +7323,6 @@
"translation": "El enlace de registro ha expirado"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "El enlace de registro parece ser inválido"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "El tipo de equipo no permite realizar invitaciones"
},
diff --git a/i18n/fr.json b/i18n/fr.json
index f2c20c29f..6b6071d41 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -132,10 +132,6 @@
"translation": "Impossible d'envoyer le fichier. Le fichier est trop volumineux."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Analyse des gabarits du serveur %v en cours"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Impossible d'analyser les modèles du serveur %v"
},
@@ -153,7 +149,7 @@
},
{
"id": "api.channel.add_member.added",
- "translation": "%v a été ajouté au canal par %v"
+ "translation": "%v a été ajouté au canal par %v."
},
{
"id": "api.channel.add_member.find_channel.app_error",
@@ -201,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": "Ce canal a été converti en canal public et peut être rejoint par tout membre de l'équipe."
},
{
"id": "api.channel.change_channel_privacy.public_to_private",
- "translation": "This channel has been converted to a Private Channel."
+ "translation": "Ce canal a été converti en canal privé."
},
{
"id": "api.channel.create_channel.direct_channel.app_error",
@@ -1812,11 +1808,11 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " A envoyé un ou plusieurs fichiers sur "
+ "translation": " a envoyé un ou plusieurs fichiers dans "
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
- "translation": " A envoyé un ou plusieurs fichiers dans un message personnel"
+ "translation": " a envoyé un ou plusieurs fichiers dans un message personnel"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -2184,7 +2180,7 @@
},
{
"id": "api.team.add_user_to_team.added",
- "translation": "%v a été ajouté à l'équipe par %v"
+ "translation": "%v a été ajouté à l'équipe par %v."
},
{
"id": "api.team.add_user_to_team.missing_parameter.app_error",
@@ -2203,10 +2199,6 @@
"translation": "Le lien d'inscription a expiré."
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "Le lien d'inscription semble ne pas être valide."
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "Cette URL n'est pas disponible. Veuillez en essayer une autre."
},
@@ -2307,6 +2299,14 @@
"translation": "%v a quitté l'équipe."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Impossible de publier la description du canal"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Tentative de suppression définitive de l'équipe %v id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "Le lien d'enregistrement n'est pas valide."
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "Le lien d'inscription semble ne pas être valide."
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Nom d'équipe incorrect"
},
@@ -2956,7 +2960,7 @@
},
{
"id": "api.user.upload_profile_user.decode_config.app_error",
- "translation": "Impossible de sauvegarder l'image de profile. Le fichier ne semble pas être un fichier d'image valide."
+ "translation": "Impossible de sauvegarder l'image de profil. Le fichier ne semble pas être un fichier d'image valide."
},
{
"id": "api.user.upload_profile_user.encode.app_error",
@@ -3067,6 +3071,10 @@
"translation": "Impossible de lire la charge utile du webhook entrant."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Initialisation des routes de l'API webhook"
},
@@ -3360,7 +3368,7 @@
},
{
"id": "app.import.validate_post_import_data.create_at_zero.error",
- "translation": "La propriété de message CreateAt ne doit pas être 0 si ce champ est défini."
+ "translation": "La propriété de message CreateAt ne doit pas être 0."
},
{
"id": "app.import.validate_post_import_data.message_length.error",
@@ -3380,51 +3388,51 @@
},
{
"id": "app.import.validate_reaction_import_data.create_at_before_parent.error",
- "translation": "Reaction CreateAt property must be greater than the parent post CreateAt."
+ "translation": "La propriété de réponse CreateAt doit être plus grande que la valeur de la propriété CreateAt parente."
},
{
"id": "app.import.validate_reaction_import_data.create_at_missing.error",
- "translation": "La propriété requise pour un message est manquante : create_at."
+ "translation": "La propriété requise de réaction est manquante : create_at."
},
{
"id": "app.import.validate_reaction_import_data.create_at_zero.error",
- "translation": "La propriété de message CreateAt ne doit pas être 0 si ce champ est défini."
+ "translation": "La propriété de réaction CreateAt ne doit pas être 0."
},
{
"id": "app.import.validate_reaction_import_data.emoji_name_length.error",
- "translation": "La propriété Message du message est plus longue que la longueur maximale autorisée."
+ "translation": "La propriété de réaction EmojiName est plus longue que la longueur maximale autorisée."
},
{
"id": "app.import.validate_reaction_import_data.emoji_name_missing.error",
- "translation": "La propriété requise du message est manquante : User."
+ "translation": "La propriété requise de réaction est manquante : EmojiName."
},
{
"id": "app.import.validate_reaction_import_data.user_missing.error",
- "translation": "La propriété requise du message est manquante : User."
+ "translation": "La propriété requise de réaction est manquante : User."
},
{
"id": "app.import.validate_reply_import_data.create_at_before_parent.error",
- "translation": "Reply CreateAt property must be greater than the parent post CreateAt."
+ "translation": "La propriété de réponse CreateAt doit être plus grande que la valeur de la propriété CreateAt parente."
},
{
"id": "app.import.validate_reply_import_data.create_at_missing.error",
- "translation": "La propriété requise pour un message est manquante : create_at."
+ "translation": "La propriété requise de réponse est manquante : create_at."
},
{
"id": "app.import.validate_reply_import_data.create_at_zero.error",
- "translation": "La propriété de message CreateAt ne doit pas être 0 si ce champ est défini."
+ "translation": "La propriété de message CreateAt ne doit pas être 0."
},
{
"id": "app.import.validate_reply_import_data.message_length.error",
- "translation": "La propriété Message du message est plus longue que la longueur maximale autorisée."
+ "translation": "La propriété de réponse Message est plus longue que la longueur maximale autorisée."
},
{
"id": "app.import.validate_reply_import_data.message_missing.error",
- "translation": "La propriété requise du message est manquante : Message."
+ "translation": "La propriété requise de réponse est manquante : Message."
},
{
"id": "app.import.validate_reply_import_data.user_missing.error",
- "translation": "La propriété requise du message est manquante : User."
+ "translation": "La propriété requise de réponse est manquante : User."
},
{
"id": "app.import.validate_team_import_data.allowed_domains_length.error",
@@ -3564,7 +3572,7 @@
},
{
"id": "app.import.validate_user_import_data.profile_image.error",
- "translation": "Invalid profile image."
+ "translation": "Image de profil invalide."
},
{
"id": "app.import.validate_user_import_data.roles_invalid.error",
@@ -3691,6 +3699,10 @@
"translation": "Les plugins et/ou l'envoi de plugins ont été désactivés."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Cette équipe a atteint la limite du nombre maximum de comptes autorisés. Contactez votre administrateur système pour augmenter cette limite."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Les jetons d'accès personnel sont désactivés sur ce serveur. Veuillez contacter votre administrateur système pour plus d'informations."
},
@@ -4644,7 +4656,7 @@
},
{
"id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error",
- "translation": "Invalid atmos/camo image proxy options for service settings. Must be set to your shared key."
+ "translation": "Les paramètres d'options du proxy d'images atmos/camo sont invalides. Votre clé partagée doit être définie comme paramètre."
},
{
"id": "model.config.is_valid.cluster_email_batching.app_error",
@@ -4759,8 +4771,12 @@
"translation": "Largeur des miniatures invalide dans les paramètres de fichiers. Doit être un entier positif."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Le paramètre de groupement de canaux non lus est invalide. Doit être défini sur « disabled », « default_on » ou « default_off »."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
- "translation": "Invalid image proxy type for service settings."
+ "translation": "Paramètre de type de proxy d'image invalide."
},
{
"id": "model.config.is_valid.ldap_basedn",
@@ -4867,6 +4883,10 @@
"translation": "Le paramètre ExportFromTimestamp de la tâche d'exportation de messages doit être un horodatage (exprimé en secondes depuis l'epoch UNIX). Seuls les messages envoyés après cet horodatage seront exportés."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Le paramètre FileLocation de la tâche d'exportation de messages doit être un dossier avec droits d'écriture. Il s'agit du dossier dans lequel les données seront exportées."
},
@@ -4875,6 +4895,10 @@
"translation": "Le paramètre FileLocation de la tâche d'exportation de messages doit être un sous-dossier de FileSettings.Directory."
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "La taille minimale du mot de passe doit être un nombre entier supérieur ou égal à {{.MinLength}} et inférieur ou égal à {{.MaxLength}}."
},
@@ -5404,7 +5428,7 @@
},
{
"id": "model.user.is_valid.position.app_error",
- "translation": "Rôle invalide : ne doit pas faire plus de 35 caractères."
+ "translation": "Rôle invalide : ne doit pas faire plus de 128 caractères."
},
{
"id": "model.user.is_valid.pwd.app_error",
@@ -6848,7 +6872,7 @@
},
{
"id": "store.sql_user_access_token.get_all.app_error",
- "translation": "Impossible de récupérer le jeton d'accès personnel"
+ "translation": "Impossible de récupérer tous les jetons d'accès personnel"
},
{
"id": "store.sql_user_access_token.get_by_token.app_error",
@@ -6864,7 +6888,7 @@
},
{
"id": "store.sql_user_access_token.search.app_error",
- "translation": "Nous avons rencontré une erreur lors de la recherche du jeton d'accès"
+ "translation": "Nous avons rencontré une erreur lors de la recherche des jetons d'accès personnel"
},
{
"id": "store.sql_webhooks.analytics_incoming_count.app_error",
@@ -7087,6 +7111,10 @@
"translation": "Échec de l'ouverture de connexion auprès du serveur SMTP %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Échec de fermeture de connexion auprès du serveur SMTP"
},
@@ -7155,10 +7183,6 @@
"translation": "Échec de la création de l'observateur de dossier %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Échec de l'observateur de dossier %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Erreur lors de la récupération du profil pour id=%v déconnexion forcée"
},
@@ -7263,18 +7287,10 @@
"translation": "Analyse des gabarits sur %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Impossible d'analyser les gabarits %v"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "ID de message invalide"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Re-examen des gabarits en raison de la modification du fichier %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "Le lien de réinitialisation du mot de passe a expiré"
},
@@ -7291,10 +7307,6 @@
"translation": "Inscription"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "Le lien d'inscription ne semble pas être valide"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "Le lien d'inscription a expiré"
},
@@ -7311,10 +7323,6 @@
"translation": "Le lien d'inscription a expiré"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "Le lien d'inscription ne semble pas être valide"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "Le type d'équipe ne permet pas les invitations ouvertes"
},
diff --git a/i18n/it.json b/i18n/it.json
index a58dfa15a..7b6d3f077 100644
--- a/i18n/it.json
+++ b/i18n/it.json
@@ -132,10 +132,6 @@
"translation": "Impossibile caricare il file. Il file è troppo grande."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Analisi dei template del server: %v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Impossibile analizzare i modelli del server %v"
},
@@ -2203,10 +2199,6 @@
"translation": "Il collegamento per l'iscrizione è scaduto"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "Il collegamento per l'iscrizione non sembra essere valido"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "L'URL non è disponibile. Provane un altro."
},
@@ -2307,6 +2299,14 @@
"translation": "%v ha lasciato il gruppo."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Impossibile pubblicare il messaggio di spostamento del canale."
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "Questo canale è stato spostato in questo gruppo da %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Tentativo di eliminazione permanente del gruppo %v id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "Il collegamento per l'iscrizione non sembra essere valido"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "Il collegamento per l'iscrizione non sembra essere valido"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Nome gruppo non valido"
},
@@ -3067,6 +3071,10 @@
"translation": "Impossibile leggere il contenuto del webhook in ingresso."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Impossibile decodificare la richiesta multipart del webhook in ingresso."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Inizializzazione API Routes dei webhook"
},
@@ -3691,6 +3699,10 @@
"translation": "I plugin e/o il caricamento dei plugin è stato disattivato."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Questo gruppo ha raggiunto il limite massimo di utenti ammessi. Contatta il tuo amministratore di sistema per innalzare il limite."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "I Token di accesso personale sono disattivati su questo server. Per favore contatta l'Amministratore di Sistema per ulteriori dettagli."
},
@@ -4759,6 +4771,10 @@
"translation": "Larghezza antemprima non valida per le impostazioni file. Deve essere un numero positivo."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Canali non letti del gruppo non validi per le impostazioni del servizio. Dev'essere 'disabled', 'default_on' oppure 'default_off'."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Tipo proxy immagine non valido nelle impostazioni del servizio."
},
@@ -4867,6 +4883,10 @@
"translation": "Lavoro esportazione messaggi il valore ExportFromTimestamp deve essere un timestamp (espresso in secondi dall'epoca unix). Solo i messaggi successivi a questo valore saranno esportati."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "ExportFormat del lavoro di esportazione messaggi deve essere 'actianve' oppure 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Lavoro esportazione messaggi il valore FileLocation deve essere una cartella accessibile in scrittura in cui verranno salvati i dati esportati"
},
@@ -4875,6 +4895,10 @@
"translation": "Lavoro esportazione messaggi il valore FileLocation deve essere una sottocartella di FileSettings.Directory"
},
{
+ "id": "model.config.is_valid.message_export.global_relay_email_address.app_error",
+ "translation": "GlobalRelayEmailAddress del lavoro di esportazione messaggi deve essere un indirizzo email valido"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Lunghezza minima password deve essere un numero intero maggiore o uguale a {{.MinLength}} e minore o uguale a {{.MaxLength}}."
},
@@ -7087,6 +7111,10 @@
"translation": "Impossibile aprire una connessione al server SMTP %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Impossibile aggiungere l'allegato all'email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Impossibile chiudere la connessione al server SMTP"
},
@@ -7155,10 +7183,6 @@
"translation": "Impossibile creare osservatore cartella %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Fallito nell'osservatore cartella %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Errore nel caricamento del profilo utente id=%v logout forzato"
},
@@ -7263,18 +7287,10 @@
"translation": "Analisi dei template del server: %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Impossibile analizzare i modelli %v"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "ID pubblicazione non valido"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Rianalisi dei modelli in corso a causa della modifica al file %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "Il collegamento per il reset della password è scaduto"
},
@@ -7291,10 +7307,6 @@
"translation": "Iscrizione"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "Il collegamento per l'iscrizione non sembra essere valido"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "Il collegamento per l'iscrizione è scaduto"
},
@@ -7311,10 +7323,6 @@
"translation": "Il collegamento per l'iscrizione è scaduto"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "Il collegamento per l'iscrizione non sembra essere valido"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "La tipologia del gruppo non ammette inviti aperti"
},
diff --git a/i18n/ja.json b/i18n/ja.json
index 3017f94dc..b8fb048a5 100644
--- a/i18n/ja.json
+++ b/i18n/ja.json
@@ -132,10 +132,6 @@
"translation": "画像ファイルをアップロードできません。ファイルが大き過ぎます。"
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "%vにあるサーバーテンプレートを読み込んでいます"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "サーバーテンプレート%vを読み込めませんでした"
},
@@ -1812,11 +1808,11 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " 一つ以上のファイルをアップロードしました "
+ "translation": " ファイルをアップロードしました "
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
- "translation": " 一つ以上のファイルをダイレクトメッセージにアップロードしました"
+ "translation": " ファイルをダイレクトメッセージにアップロードしました"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -2203,10 +2199,6 @@
"translation": "利用登録リンクは有効期限が切れています"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "利用登録リンクが不正です"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "このURLは利用できません。他のURLを試してみてください。"
},
@@ -2307,6 +2299,14 @@
"translation": "%v はチームから脱退しました。"
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "チャンネル移動メッセージを投稿できませんでした"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "このチャンネルは %v からこのチームへ移動されました。"
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "チーム%vを完全に削除しようとしています id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "利用登録リンクが不正です"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "利用登録リンクが不正です"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "不正なチーム名です"
},
@@ -3067,6 +3071,10 @@
"translation": "内向きのウェブフックのペイロードを読み込めませんでした。"
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "内向きのウェブフックのマルチパートペイロードをデコードできませんでした。"
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "ウェブフックAPIルートを初期化しています"
},
@@ -3691,6 +3699,10 @@
"translation": "プラグインのアップロードは無効化されています。"
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "このチームは登録ユーザー数の上限に達しました。システム管理者に上限値の設定を変更するように依頼してください。"
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "このサーバーではパーソナルアクセストークンが無効になっています。詳しくはシステム管理者に問い合わせてください。"
},
@@ -4759,6 +4771,10 @@
"translation": "ファイル設定のサムネイルの幅が不正です。ゼロ以上の数を指定してください。"
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "未読チャンネルのグループ化設定が不正です。'disabled', 'default_on', 'default_off'のいずれかでなくてはなりません。"
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "画像プロキシタイプの設定が不正です。"
},
@@ -4867,6 +4883,10 @@
"translation": "メッセージエクスポート処理の ExportFromTimestamp は(ユニックス標準時からの秒数で表される)タイムスタンプでなくてはなりません。このタイムスタンプ以降に投稿されたメッセージのみエクスポートされます。"
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "メッセージエクスポート処理の ExportFormat は 'actiance' か 'globalrelay' のいずれかでなくてはなりません"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "メッセーエクスポート処理の FileLocation はエクスポートデータが書き込まれる書き込み可能なディレクトリでなくてはなりません"
},
@@ -4875,6 +4895,10 @@
"translation": "メッセージエクスポート処理の FileLocation は FileSettings.Directory のサブディレクトリでなくてはなりません"
},
{
+ "id": "model.config.is_valid.message_export.global_relay_email_address.app_error",
+ "translation": "メッセージエクスポート処理の GlobalRelayEmailAddress には有効な電子メールアドレスが設定されていなくてはなりません"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "最小パスワード長さは、{{.MinLength}}以上、{{.MaxLength}}以下に設定してください。"
},
@@ -7087,6 +7111,10 @@
"translation": "SMTPサーバー%vに接続できませんでした"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "電子メールへ添付を書き込むことができませんでした"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "SMTPサーバーへの接続を終了できませんでした"
},
@@ -7155,10 +7183,6 @@
"translation": "ディレクトリーウォッチャーを作成できませんでした %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "ディレクトリーウォッチャーで失敗しました %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "id=%vのユーザーのプロフィールを取得できません。強制的にログアウトします"
},
@@ -7263,18 +7287,10 @@
"translation": "テンプレート%vを読み直しています"
},
{
- "id": "web.parsing_templates.error",
- "translation": "テンプレート%vの解析に失敗しました"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "不正な投稿IDです"
},
{
- "id": "web.reparse_templates.info",
- "translation": "ファイル%vが変更されたためテンプレートを読み直しています"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "パスワード初期化リンクの期限が切れています"
},
@@ -7291,10 +7307,6 @@
"translation": "利用登録"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "利用登録リンクが不正です"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "利用登録リンクの期限が切れています"
},
@@ -7311,10 +7323,6 @@
"translation": "利用登録リンクの期限が切れています"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "利用登録リンクが不正です"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "このチームは誰でも招待できるような形式ではありません"
},
diff --git a/i18n/ko.json b/i18n/ko.json
index bdd2935a4..b8eff6bca 100644
--- a/i18n/ko.json
+++ b/i18n/ko.json
@@ -132,10 +132,6 @@
"translation": "용량이 커서 파일을 업로드 할 수 없습니다."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "서버 템플릿 %v 파싱 중"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "서버 템플릿 %v 를 파싱하는데 실패하였습니다"
},
@@ -1812,11 +1808,11 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " Uploaded one or more files in "
+ "translation": " uploaded one or more files in "
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
- "translation": " Uploaded one or more files in a direct message"
+ "translation": " uploaded one or more files in a direct message"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -2203,10 +2199,6 @@
"translation": "가입 링크가 만료되었습니다."
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "유효하지 않은 가입 링크입니다."
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "유효하지 않는 URL 입니다. 변경 후 다시 시도해주세요."
},
@@ -2307,6 +2299,14 @@
"translation": "%v left the team."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "들어옴/나감 메시지를 등록하는 데 실패함"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Attempting to permanently delete team %v id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "유효하지 않은 가입 링크입니다."
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "유효하지 않은 가입 링크입니다."
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "올바르지 않은 팀 이름"
},
@@ -3067,6 +3071,10 @@
"translation": "Incoming webhook의 payload를 읽을 수 없습니다."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "webhook API 경로 초기화 중"
},
@@ -3136,7 +3144,7 @@
},
{
"id": "app.channel.post_update_channel_purpose_message.post.error",
- "translation": "Failed to post channel purpose message"
+ "translation": "들어옴/나감 메시지를 등록하는 데 실패함"
},
{
"id": "app.channel.post_update_channel_purpose_message.removed",
@@ -3691,6 +3699,10 @@
"translation": "Plugins and/or plugin uploads have been disabled."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "This team has reached the maximum number of allowed accounts. Contact your systems administrator to set a higher limit."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Personal access tokens are disabled on this server. Please contact your system administrator for details."
},
@@ -4759,6 +4771,10 @@
"translation": "파일 세팅에 대한 잘못된 썸네일 너비값입니다. 0보다 큰 값이여야 합니다."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Invalid group unread channels for service settings. Must be 'disabled', 'default_on', or 'default_off'."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Invalid image proxy type for service settings."
},
@@ -4867,6 +4883,10 @@
"translation": "Message export job ExportFromTimestamp must be a timestamp (expressed in seconds since unix epoch). Only messages sent after this timestamp will be exported."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Message export job FileLocation must be a writable directory that export data will be written to"
},
@@ -4875,6 +4895,10 @@
"translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Minimum password length must be a whole number greater than or equal to {{.MinLength}} and less than or equal to {{.MaxLength}}."
},
@@ -7087,6 +7111,10 @@
"translation": "Failed to open a connection to SMTP server %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Failed to close connection to SMTP server"
},
@@ -7155,10 +7183,6 @@
"translation": "Failed to create directory watcher %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Failed in directory watcher %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "강제 로그아웃 중 id=%v 유저 프로필을 가져오는 중 오류가 발생했습니다"
},
@@ -7263,18 +7287,10 @@
"translation": "%v에서 서버 템플릿 파싱중"
},
{
- "id": "web.parsing_templates.error",
- "translation": "서버 템플릿 %v 를 파싱하는데 실패하였습니다"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "잘못된 글 ID"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Re-parsing templates because of modified file %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "비밀번호 재설정 링크가 만료되었습니다."
},
@@ -7291,10 +7307,6 @@
"translation": "가입"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "유효하지 않은 가입 링크입니다."
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "가입 링크가 만료되었습니다."
},
@@ -7311,10 +7323,6 @@
"translation": "가입 링크가 만료되었습니다."
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "유효하지 않은 가입 링크입니다."
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "The team type doesn't allow open invites"
},
diff --git a/i18n/nl.json b/i18n/nl.json
index 2e0f54ee4..a4ae85069 100644
--- a/i18n/nl.json
+++ b/i18n/nl.json
@@ -49,7 +49,7 @@
},
{
"id": "api.admin.add_certificate.no_file.app_error",
- "translation": "Geen bestand in 'certificate' veld in aanvraag"
+ "translation": "Geen bestand gevonden in 'certificate' veld in verzoek"
},
{
"id": "api.admin.add_certificate.open.app_error",
@@ -57,15 +57,15 @@
},
{
"id": "api.admin.add_certificate.saving.app_error",
- "translation": "Kon het certificaat-bestand niet opslaan"
+ "translation": "Kon het certificaat-bestand niet opslaan."
},
{
"id": "api.admin.file_read_error",
- "translation": "Fout bij lezen van logbestand"
+ "translation": "Fout bij lezen van logbestand."
},
{
"id": "api.admin.get_brand_image.not_available.app_error",
- "translation": "Personaliseren is niet ondersteund, of niet geconfigureerd op deze server"
+ "translation": "Personalisatie is niet ondersteund of niet geconfigureerd op deze server."
},
{
"id": "api.admin.get_brand_image.storage.app_error",
@@ -73,15 +73,15 @@
},
{
"id": "api.admin.init.debug",
- "translation": "Initialisatie van de admin API routes"
+ "translation": "Initialisatie van de admin API routes."
},
{
"id": "api.admin.recycle_db_end.warn",
- "translation": "Klaar met herstarten van de databaseverbinding"
+ "translation": "Klaar met herstarten van de databaseverbinding."
},
{
"id": "api.admin.recycle_db_start.warn",
- "translation": "Proberen om de databaseverbinding te herstarten"
+ "translation": "Proberen om de databaseverbinding te herstarten."
},
{
"id": "api.admin.remove_certificate.delete.app_error",
@@ -89,7 +89,7 @@
},
{
"id": "api.admin.saml.metadata.app_error",
- "translation": "Er is een fout opgetreden tijdens het opstellen van Service Provider Metadata."
+ "translation": "Er is een fout opgetreden tijdens het opzetten van Service Provider Metadata."
},
{
"id": "api.admin.test_email.body",
@@ -132,10 +132,6 @@
"translation": "Kan bestand niet uploaden. Bestand is te groot."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Server-templates aan het verwerken voor %v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Verwerken van server-template %v is mislukt"
},
@@ -153,7 +149,7 @@
},
{
"id": "api.channel.add_member.added",
- "translation": "%v is door %v aan het kanaal toegevoegd"
+ "translation": "%v is door %v aan het kanaal toegevoegd."
},
{
"id": "api.channel.add_member.find_channel.app_error",
@@ -185,11 +181,11 @@
},
{
"id": "api.channel.can_manage_channel.private_restricted_system_admin.app_error",
- "translation": "Alleen systeembeheerders mogen publieke kanalen beheren en aanmaken."
+ "translation": "Alleen systeembeheerders mogen privékanalen beheren en aanmaken."
},
{
"id": "api.channel.can_manage_channel.private_restricted_team_admin.app_error",
- "translation": "Alleen team- en systeembeheerders mogen publieke kanalen beheren en aanmaken."
+ "translation": "Alleen team- en systeembeheerders mogen privékanalen beheren en aanmaken."
},
{
"id": "api.channel.can_manage_channel.public_restricted_system_admin.app_error",
@@ -201,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": "Dit kanaal is omgezet naar een publiek kanaal en is open voor ieder teamlid."
},
{
"id": "api.channel.change_channel_privacy.public_to_private",
- "translation": "This channel has been converted to a Private Channel."
+ "translation": "This kanaal is omgezet naar een privékanaal."
},
{
"id": "api.channel.create_channel.direct_channel.app_error",
@@ -229,15 +225,15 @@
},
{
"id": "api.channel.create_direct_channel.invalid_user.app_error",
- "translation": "Invalid user ID for direct channel creation"
+ "translation": "Onjuiste gebruikers ID voor het aanmaken van een direct kanaal."
},
{
"id": "api.channel.create_group.bad_size.app_error",
- "translation": "Group message channels must contain at least 3 and no more than 8 users"
+ "translation": "Groep bericht kanalen moeten tenminste 3 en niet meer dan 8 gebruikers bevatten"
},
{
"id": "api.channel.create_group.bad_user.app_error",
- "translation": "One of the provided users does not exist"
+ "translation": "Een van de opgegeven gebruikers bestaat niet"
},
{
"id": "api.channel.delete_channel.archived",
@@ -305,7 +301,7 @@
},
{
"id": "api.channel.join_channel.post_and_forget",
- "translation": "%v is het kanaal binnengekomen."
+ "translation": "%v is nu lid van het kanaal."
},
{
"id": "api.channel.leave.default.app_error",
@@ -317,7 +313,7 @@
},
{
"id": "api.channel.leave.last_member.app_error",
- "translation": "U bent het laatste lid, probeer de privégroep te verwijderen in plaats van die te verlaten."
+ "translation": "U bent het laatste lid, probeer de privékanaalte verwijderen in plaats van die te verlaten."
},
{
"id": "api.channel.leave.left",
@@ -329,7 +325,7 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
- "translation": "Kon de gebruiker niet ophalen tijdens het bewaren van de nieuwe kanaalkoptekst %v"
+ "translation": "Kon de gebruiker niet ophalen tijdens het bijwerken van het kanaal weergavenaam veld"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
@@ -337,7 +333,7 @@
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.post.error",
- "translation": "Failed to post update channel header message"
+ "translation": "Kanaalkoptekst bijwerken mislukt"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -1812,11 +1808,11 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " Uploaded one or more files in "
+ "translation": " uploaded one or more files in "
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
- "translation": " Uploaded one or more files in a direct message"
+ "translation": " uploaded one or more files in a direct message"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -2203,10 +2199,6 @@
"translation": "De aanmeld link is verlopen"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "De aanmeld link is niet geldig"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "Deze URL is niet beschikbaar. Probeer een andere."
},
@@ -2307,6 +2299,14 @@
"translation": "%v left the team."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Plaatsen van bericht over binnenkomen/verlaten %v is mislukt"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Poging om team %v permanent te verwijderen, id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "De aanmeld link is niet geldig"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "De aanmeld link is niet geldig"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Ongeldige team naam"
},
@@ -3067,6 +3071,10 @@
"translation": "Kan inhoud van inkomende webhook niet lezen."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Initialisatie van de webhook api"
},
@@ -3136,7 +3144,7 @@
},
{
"id": "app.channel.post_update_channel_purpose_message.post.error",
- "translation": "Failed to post channel purpose message"
+ "translation": "Plaatsen van bericht over binnenkomen/verlaten %v is mislukt"
},
{
"id": "app.channel.post_update_channel_purpose_message.removed",
@@ -3204,27 +3212,27 @@
},
{
"id": "app.import.import_line.null_channel.error",
- "translation": "Import data line has type \"channel\" but the channel object is null."
+ "translation": "Geimporteerde lijn met type \"user\", maar het \"user\" object is null."
},
{
"id": "app.import.import_line.null_direct_channel.error",
- "translation": "Import data line has type \"direct_channel\" but the direct_channel object is null."
+ "translation": "Geimporteerde lijn met type \"user\", maar het \"user\" object is null."
},
{
"id": "app.import.import_line.null_direct_post.error",
- "translation": "Import data line has type \"direct_post\" but the direct_post object is null."
+ "translation": "Geimporteerde lijn met type \"user\", maar het \"user\" object is null."
},
{
"id": "app.import.import_line.null_post.error",
- "translation": "Import data line has type \"post\" but the post object is null."
+ "translation": "Geimporteerde lijn met type \"user\", maar het \"user\" object is null."
},
{
"id": "app.import.import_line.null_team.error",
- "translation": "Import data line has type \"team\" but the team object is null."
+ "translation": "Geimporteerde lijn met type \"user\", maar het \"user\" object is null."
},
{
"id": "app.import.import_line.null_user.error",
- "translation": "Import data line has type \"user\" but the user object is null."
+ "translation": "Geimporteerde lijn met type \"user\", maar het \"user\" object is null."
},
{
"id": "app.import.import_line.unknown_line_type.error",
@@ -3252,7 +3260,7 @@
},
{
"id": "app.import.validate_channel_import_data.create_at_zero.error",
- "translation": "Channel create_at must not be zero if provided."
+ "translation": "Kanaaleigenschap create_at mag niet 0 zijn indien opgegeven."
},
{
"id": "app.import.validate_channel_import_data.display_name_length.error",
@@ -3260,7 +3268,7 @@
},
{
"id": "app.import.validate_channel_import_data.display_name_missing.error",
- "translation": "Missing required channel property: display_name"
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_channel_import_data.header_length.error",
@@ -3276,7 +3284,7 @@
},
{
"id": "app.import.validate_channel_import_data.name_missing.error",
- "translation": "Missing required channel property: name"
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_channel_import_data.purpose_length.error",
@@ -3284,7 +3292,7 @@
},
{
"id": "app.import.validate_channel_import_data.team_missing.error",
- "translation": "Missing required channel property: team"
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_channel_import_data.type_invalid.error",
@@ -3292,7 +3300,7 @@
},
{
"id": "app.import.validate_channel_import_data.type_missing.error",
- "translation": "Missing required channel property: type."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_direct_channel_import_data.header_length.error",
@@ -3300,7 +3308,7 @@
},
{
"id": "app.import.validate_direct_channel_import_data.members_required.error",
- "translation": "Missing required direct channel property: members"
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_direct_channel_import_data.members_too_few.error",
@@ -3316,7 +3324,7 @@
},
{
"id": "app.import.validate_direct_post_import_data.channel_members_required.error",
- "translation": "Missing required direct post property: channel_members"
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_direct_post_import_data.channel_members_too_few.error",
@@ -3328,7 +3336,7 @@
},
{
"id": "app.import.validate_direct_post_import_data.create_at_missing.error",
- "translation": "Missing required direct post property: create_at"
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_direct_post_import_data.create_at_zero.error",
@@ -3340,7 +3348,7 @@
},
{
"id": "app.import.validate_direct_post_import_data.message_missing.error",
- "translation": "Missing required direct post property: message"
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_direct_post_import_data.unknown_flagger.error",
@@ -3348,15 +3356,15 @@
},
{
"id": "app.import.validate_direct_post_import_data.user_missing.error",
- "translation": "Missing required direct post property: user"
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_post_import_data.channel_missing.error",
- "translation": "Missing required Post property: Channel."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_post_import_data.create_at_missing.error",
- "translation": "Missing required Post property: create_at."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_post_import_data.create_at_zero.error",
@@ -3368,23 +3376,23 @@
},
{
"id": "app.import.validate_post_import_data.message_missing.error",
- "translation": "Missing required Post property: Message."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_post_import_data.team_missing.error",
- "translation": "Missing required Post property: Team."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_post_import_data.user_missing.error",
- "translation": "Missing required Post property: User."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_reaction_import_data.create_at_before_parent.error",
- "translation": "Reaction CreateAt property must be greater than the parent post CreateAt."
+ "translation": "Reactie CreateAt eigenschap moet groter zijn dan het hoofdbericht CreateAt"
},
{
"id": "app.import.validate_reaction_import_data.create_at_missing.error",
- "translation": "Missing required Reaction property: create_at."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_reaction_import_data.create_at_zero.error",
@@ -3396,19 +3404,19 @@
},
{
"id": "app.import.validate_reaction_import_data.emoji_name_missing.error",
- "translation": "Missing required Reaction property: EmojiName."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_reaction_import_data.user_missing.error",
- "translation": "Missing required Reaction property: User."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_reply_import_data.create_at_before_parent.error",
- "translation": "Reply CreateAt property must be greater than the parent post CreateAt."
+ "translation": "Reactie CreateAt eigenschap moet groter zijn dan het hoofdbericht CreateAt"
},
{
"id": "app.import.validate_reply_import_data.create_at_missing.error",
- "translation": "Missing required Reply property: create_at."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_reply_import_data.create_at_zero.error",
@@ -3420,11 +3428,11 @@
},
{
"id": "app.import.validate_reply_import_data.message_missing.error",
- "translation": "Missing required Reply property: Message."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_reply_import_data.user_missing.error",
- "translation": "Missing required Reply property: User."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_team_import_data.allowed_domains_length.error",
@@ -3432,7 +3440,7 @@
},
{
"id": "app.import.validate_team_import_data.create_at_zero.error",
- "translation": "Team create_at must not be zero if provided."
+ "translation": "Kanaaleigenschap create_at mag niet 0 zijn indien opgegeven."
},
{
"id": "app.import.validate_team_import_data.description_length.error",
@@ -3444,7 +3452,7 @@
},
{
"id": "app.import.validate_team_import_data.display_name_missing.error",
- "translation": "Missing required team property: display_name."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_team_import_data.name_characters.error",
@@ -3456,7 +3464,7 @@
},
{
"id": "app.import.validate_team_import_data.name_missing.error",
- "translation": "Missing required team property: name."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_team_import_data.name_reserved.error",
@@ -3468,7 +3476,7 @@
},
{
"id": "app.import.validate_team_import_data.type_missing.error",
- "translation": "Missing required team property: type."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_user_channels_import_data.channel_name_missing.error",
@@ -3508,7 +3516,7 @@
},
{
"id": "app.import.validate_user_import_data.email_missing.error",
- "translation": "Missing required user property: email."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_user_import_data.first_name_length.error",
@@ -3576,7 +3584,7 @@
},
{
"id": "app.import.validate_user_import_data.username_missing.error",
- "translation": "Missing require user property: username."
+ "translation": "Mist vereiste team kenmerk: naam."
},
{
"id": "app.import.validate_user_teams_import_data.invalid_roles.error",
@@ -3691,6 +3699,10 @@
"translation": "Plugins and/or plugin uploads have been disabled."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Het maximaal aantal leden voor dit team is bereikt. Neem contact op met de beheerder voor een hoger limiet."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Personal access tokens are disabled on this server. Please contact your system administrator for details."
},
@@ -4028,7 +4040,7 @@
},
{
"id": "ent.elasticsearch.test_config.connect_failed",
- "translation": "Connecting to Elasticsearch server failed."
+ "translation": "Connectie naar Elasticsearch server is mislukt."
},
{
"id": "ent.elasticsearch.test_config.indexing_disabled.error",
@@ -4672,7 +4684,7 @@
},
{
"id": "model.config.is_valid.elastic_search.connection_url.app_error",
- "translation": "Elastic Search ConnectionUrl setting must be provided when Elastic Search indexing is enabled."
+ "translation": "Elastic Search Username instelling moet ingevuld zijn wanneer Elastic Search indexing is ingeschakeld."
},
{
"id": "model.config.is_valid.elastic_search.enable_searching.app_error",
@@ -4684,7 +4696,7 @@
},
{
"id": "model.config.is_valid.elastic_search.password.app_error",
- "translation": "Elastic Search Password setting must be provided when Elastic Search indexing is enabled."
+ "translation": "Elastic Search Username instelling moet ingevuld zijn wanneer Elastic Search indexing is ingeschakeld."
},
{
"id": "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error",
@@ -4696,7 +4708,7 @@
},
{
"id": "model.config.is_valid.elastic_search.username.app_error",
- "translation": "Elastic Search Username setting must be provided when Elastic Search indexing is enabled."
+ "translation": "Elastic Search Username instelling moet ingevuld zijn wanneer Elastic Search indexing is ingeschakeld."
},
{
"id": "model.config.is_valid.email_batching_buffer_size.app_error",
@@ -4759,6 +4771,10 @@
"translation": "Ongeldige voorvertonings breedte bij bestands instellingen. Moet een getal groter dan 0 zijn."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Invalid group unread channels for service settings. Must be 'disabled', 'default_on', or 'default_off'."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Invalid image proxy type for service settings."
},
@@ -4867,6 +4883,10 @@
"translation": "Message export job ExportFromTimestamp must be a timestamp (expressed in seconds since unix epoch). Only messages sent after this timestamp will be exported."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Message export job FileLocation must be a writable directory that export data will be written to"
},
@@ -4875,6 +4895,10 @@
"translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Wachtwoord moet een lengte hebben van minimaal {{.MinLength}} en/of maximaal {{.MaxLength}}"
},
@@ -7087,6 +7111,10 @@
"translation": "Kan geen verbinding opzetten naa de SMTP server, fout=%v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Er is een probleem opgetreden bij het afsluiten van de verbinding naar de SMTP server"
},
@@ -7155,10 +7183,6 @@
"translation": "Toevoegen van een directory om te controleren is mislukt %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Fout tijdens het bekijken van de directory %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Probleem bij het ophalen van het gebruikersprofiel voor id=%v, uitlog actie geforceerd"
},
@@ -7263,18 +7287,10 @@
"translation": "Server template aan het verwerken voor %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Server template %v is niet te verwerken"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "Ongeldige bericht ID"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Templates opnieuw verwerken, door aangepast bestand %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "De wachtwoord reset link is niet meer geldig"
},
@@ -7291,10 +7307,6 @@
"translation": "Aanmelden"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "De aanmeld link is niet geldig"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "De aanmeld link is verlopen"
},
@@ -7311,10 +7323,6 @@
"translation": "De aanmeld link is verlopen"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "De aanmeld link is niet geldig"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "Het team type staat open uitnodiging niet toe"
},
diff --git a/i18n/pl.json b/i18n/pl.json
index ccf9fed39..209c00aeb 100644
--- a/i18n/pl.json
+++ b/i18n/pl.json
@@ -132,10 +132,6 @@
"translation": "Nie udało się wgrać obrazu. Plik jest zbyt duży."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Przetwarzanie szablonów serwera w %v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Nie udało się przetworzyć szablonów serwera w %v"
},
@@ -2203,10 +2199,6 @@
"translation": "Odnośnik zapisu jest przeterminowany"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "Odnośnik zapisu nie wydaje się być poprawny"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "Ten adres URL jest niedostępny. Proszę wybrać inny."
},
@@ -2307,6 +2299,14 @@
"translation": "%v opuścił zespół."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Nie udało się wysłać celu kanału"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Próba trwałego usunięcia zespołu %v ID=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "Link rejestracyjny wydaje się być niepoprawny"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "Łącze rejestracji wydaje się być niepoprawne."
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Niepoprawna nazwa zespołu"
},
@@ -3067,6 +3071,10 @@
"translation": "Nie można odczytać zawartości przychodzącego webhook."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Inicjalizowanie tras API webhooków"
},
@@ -3691,6 +3699,10 @@
"translation": "Plugins and/or plugin uploads have been disabled."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Ten zespół osiągnął limit kont. Skontaktuj się z administratorem aby ustanowić wyższy pułap."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Personal access tokens are disabled on this server. Please contact your system administrator for details."
},
@@ -4759,6 +4771,10 @@
"translation": "Nieprawidłowa szerokość miniaturki dla ustawień plików. Musi być liczbą większą od 0."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Invalid group unread channels for service settings. Must be 'disabled', 'default_on', or 'default_off'."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Invalid image proxy type for service settings."
},
@@ -4867,6 +4883,10 @@
"translation": "Message export job ExportFromTimestamp must be a timestamp (expressed in seconds since unix epoch). Only messages sent after this timestamp will be exported."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Message export job FileLocation must be a writable directory that export data will be written to"
},
@@ -4875,6 +4895,10 @@
"translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Minimalna długość hasła musi być liczbą całkowitą większą lub równą od {{.MinLength}} i mniejszą lub równą od {{.MaxLength}}."
},
@@ -7087,6 +7111,10 @@
"translation": "Failed to open a connection to SMTP server %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Failed to close connection to SMTP server"
},
@@ -7155,10 +7183,6 @@
"translation": "Nie udało się utworzyć stróża katalogu %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Nie udało się utworzyć stróża katalogu %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Błąd w trakcie pobierania profilów użytkowników dla id=%v wymuszono wylogowanie"
},
@@ -7263,18 +7287,10 @@
"translation": "Przetworzono szablony serwera w %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Nie udało się przetworzyć szablonów %v"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "Nieprawidłowy ID wiadomości"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Re-parsing templates because of modified file %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "Link resetujący hasło wygasł"
},
@@ -7291,10 +7307,6 @@
"translation": "Rejestracja"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "Link do rejestracji jest niepoprawny"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "Link do rejestracji wygasł"
},
@@ -7311,10 +7323,6 @@
"translation": "Link do rejestracji wygasł"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "Link do rejestracji jest niepoprawny"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "The team type doesn't allow open invites"
},
diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json
index b07cb452e..314131254 100644
--- a/i18n/pt-BR.json
+++ b/i18n/pt-BR.json
@@ -132,10 +132,6 @@
"translation": "Não foi possível enviar o arquivo. Arquivo é muito grande."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Analisando modelos no servidor %v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Falha ao processar os modelos do servidor %v"
},
@@ -1812,11 +1808,11 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " Enviados um ou mais arquivos em "
+ "translation": " enviado um ou mais arquivos em "
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
- "translation": " Enviado um ou mais arquivos em uma mensagem direta"
+ "translation": " enviado um ou mais arquivos em uma mensagem direta"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -2203,10 +2199,6 @@
"translation": "O link de inscrição expirou"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "O link de inscrição não parece ser válido"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "Esta URL não está disponível. Por favor, tente outra."
},
@@ -2307,6 +2299,14 @@
"translation": "%v deixou a equipe."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Falha ao postar a mensagem de mover o canal."
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "Este canal foi movido desta equipe para %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Tentando permanentemente deletar a equipe %v id=%v"
},
@@ -2372,7 +2372,7 @@
},
{
"id": "api.templates.email_info",
- "translation": "Qualquer dúvida, envie-nos a qualquer momento: <a href='mailto:{{.SupportEmail}}' style='text-decoration: none; color:#2389D7;'>{{.SupportEmail}}</a>.<br>Nossos melhores cumprimentos,<br>Equipe {{.SiteName}}<br>"
+ "translation": "Se tiver qualquer dúvida, envie-nos um e-mail a qualquer momento: <a href='mailto:{{.SupportEmail}}' style='text-decoration: none; color:#2389D7;'>{{.SupportEmail}}</a>.<br>Nossos melhores cumprimentos,<br>Equipe {{.SiteName}}<br>"
},
{
"id": "api.templates.email_organization",
@@ -2711,6 +2711,10 @@
"translation": "O link de inscrição não parece ser válido"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "O link de inscrição não parece estar válido"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Inválido nome de equipe"
},
@@ -3067,6 +3071,10 @@
"translation": "Não foi possível ler o payload do webhook de entrada."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Inicializando as rotas de API webhook"
},
@@ -3691,6 +3699,10 @@
"translation": "Plugins e/ou envio de plugin foi desabilitado."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Esta equipe alcançou o número máximo de contas permitidas. Contate o administrador do sistema para ajustar um limite maior."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Os tokens de acesso individual estão desativados neste servidor. Entre em contato com o administrador do sistema para obter detalhes."
},
@@ -4759,6 +4771,10 @@
"translation": "Definição da largura do thumbnail inválida. Deve ser um número positivo."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Grupo de canais não lidos inválido nas configurações do serviço. Deve ser 'disabled', 'default_on', ou 'default_off'."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Tipo de proxy de imagem inválido nas configurações de serviços."
},
@@ -4867,6 +4883,10 @@
"translation": "O trabalho de exportação de mensagens ExportFromTimestamp deve ser data e hora (expresso em segundos unix epoch). Somente mensagens enviadas após está data e hora serão exportadas."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Trabalho de exportação de mensagens FileLocation deve ser um diretório gravável onde os dados de exportação serão gravados"
},
@@ -4875,6 +4895,10 @@
"translation": "Trabalho de exportação de mensagens FileLocation deve ser um subdiretório de FileSettings.Directory"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Tamanho mínimo de senha deve ser um número inteiro maior ou igual a {{.MinLength}} e menor ou igual a {{.MaxLength}}."
},
@@ -7087,6 +7111,10 @@
"translation": "Falha ao abrir a conexão SMTP para o servidor %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Falha ao fechar a conexão com servidor SMTP"
},
@@ -7155,10 +7183,6 @@
"translation": "Falha ao criar o diretório observador %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Falha no diretório observador %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Erro na obtenção do perfil dos usuários para id=%v forçando o logout"
},
@@ -7263,18 +7287,10 @@
"translation": "Analisando modelos no %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Falha ao processar os modelos %v"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "Post ID Inválido"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Re-análise dos modelos por causa da modificação do arquivo %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "O link de redefinição de senha expirou"
},
@@ -7291,10 +7307,6 @@
"translation": "Inscrever-se"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "O link de inscrição não parece ser válido"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "O link de inscrição expirou"
},
@@ -7311,10 +7323,6 @@
"translation": "O link de inscrição expirou"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "O link de inscrição não parece ser válido"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "O tipo da equipe não permite convites abertos"
},
diff --git a/i18n/ru.json b/i18n/ru.json
index b1e37d191..5d1b6be49 100644
--- a/i18n/ru.json
+++ b/i18n/ru.json
@@ -132,10 +132,6 @@
"translation": "Невозможно загрузить файл. Он слишком большой."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Разбор шаблонов сервера на %v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Не удалось разобрать шаблоны сервера %v"
},
@@ -201,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",
@@ -2203,10 +2199,6 @@
"translation": "Ссылка на регистрацию устарела"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "Неправильная ссылка на регистрацию"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "Этот URL-адрес недоступен. Пожалуйста, попробуйте другой."
},
@@ -2307,6 +2299,14 @@
"translation": "%v покинул(а) команду."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Не удалось обновить заголовок канала."
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "Канал перемещен в эту команду из %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "Попытка безвозвратно удалить команду %v id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "Ссылка для регистрации, похоже, неверна."
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "Неправильная ссылка на регистрацию"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Неверное имя команды"
},
@@ -3067,6 +3071,10 @@
"translation": "Не удалось прочитать полезную нагрузку входящего вебхука."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Инициализация API вебхуков"
},
@@ -3248,7 +3256,7 @@
},
{
"id": "app.import.import_user_channels.save_preferences.error",
- "translation": "Error importing user channel memberships. Failed to save preferences."
+ "translation": "Ошибка импорта участия пользователя в каналах. Не удалось сохранить настройки."
},
{
"id": "app.import.validate_channel_import_data.create_at_zero.error",
@@ -3296,7 +3304,7 @@
},
{
"id": "app.import.validate_direct_channel_import_data.header_length.error",
- "translation": "Direct channel header is too long"
+ "translation": "Заголовок личного канала слишком длинный."
},
{
"id": "app.import.validate_direct_channel_import_data.members_required.error",
@@ -3304,11 +3312,11 @@
},
{
"id": "app.import.validate_direct_channel_import_data.members_too_few.error",
- "translation": "Direct channel members list contains too few items"
+ "translation": "В списке пользователей личного канала слишком мало элементов."
},
{
"id": "app.import.validate_direct_channel_import_data.members_too_many.error",
- "translation": "Direct channel members list contains too many items"
+ "translation": "В списке пользователей личного канала слишком много элементов."
},
{
"id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error",
@@ -3320,11 +3328,11 @@
},
{
"id": "app.import.validate_direct_post_import_data.channel_members_too_few.error",
- "translation": "Direct post channel members list contains too few items"
+ "translation": "В списке пользователей личного канала слишком мало элементов."
},
{
"id": "app.import.validate_direct_post_import_data.channel_members_too_many.error",
- "translation": "Direct post channel members list contains too many items"
+ "translation": "В списке пользователей личного канала слишком много элементов."
},
{
"id": "app.import.validate_direct_post_import_data.create_at_missing.error",
@@ -3691,6 +3699,10 @@
"translation": "Plugins and/or plugin uploads have been disabled."
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Эта команда достигла максимального количества разрешенных учетных записей. Свяжитесь с системным администратором для установки большего предела."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Personal access tokens are disabled on this server. Please contact your system administrator for details."
},
@@ -4759,6 +4771,10 @@
"translation": "Неверная ширина миниатюры в настройках файлов. Число должно быть положительным."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Invalid group unread channels for service settings. Must be 'disabled', 'default_on', or 'default_off'."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Invalid image proxy type for service settings."
},
@@ -4867,6 +4883,10 @@
"translation": "Message export job ExportFromTimestamp must be a timestamp (expressed in seconds since unix epoch). Only messages sent after this timestamp will be exported."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "Message export job FileLocation must be a writable directory that export data will be written to"
},
@@ -4875,6 +4895,10 @@
"translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "Минимальная длина пароля должна быть целым числом большим или равным {{.MinLength}}, а также меньшим или равным {{.MaxLength}}."
},
@@ -7087,6 +7111,10 @@
"translation": "Не удалось установить соединение с SMTP сервером %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "Не удалось закрыть соединение с SMTP сервером"
},
@@ -7155,10 +7183,6 @@
"translation": "Не удалось создать наблюдатель каталога %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Сбой в наблюдателе каталога %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Ошибка при получении профиля пользователя для id=%v, принудительный выход"
},
@@ -7263,18 +7287,10 @@
"translation": "Разбор шаблонов на %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Не удалось разобрать шаблоны %v"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "Неверный идентификатор поста"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Повторный разбор шаблонов из-за модификации файла %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "Ссылка для сброса пароля просрочена"
},
@@ -7291,10 +7307,6 @@
"translation": "Регистрация"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "Ссылка для регистрации, похоже, недействительна"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "Ссылка для регистрации устарела"
},
@@ -7311,10 +7323,6 @@
"translation": "Ссылка для регистрации устарела"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "Ссылка для регистрации, похоже, недействительна"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "Тип команды не позволяет открытые приглашения"
},
diff --git a/i18n/tr.json b/i18n/tr.json
index b95c9b542..d33570001 100644
--- a/i18n/tr.json
+++ b/i18n/tr.json
@@ -132,10 +132,6 @@
"translation": "Dosya yüklenemedi. Dosya çok büyük."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "%v üzerindeki sunucu kalıpları işleniyor"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "%v üzerindeki sunucu kalıpları işlenemedi"
},
@@ -1812,11 +1808,11 @@
},
{
"id": "api.post.send_notifications_and_forget.push_image_only",
- "translation": " Şunun içine bir ya da bir kaç dosya yüklendi "
+ "translation": " şunun içine bir ya da bir kaç dosya yükledi "
},
{
"id": "api.post.send_notifications_and_forget.push_image_only_dm",
- "translation": " Bir doğrudan ileti içine bir ya da bir kaç dosya yüklendi"
+ "translation": " bir doğrudan ileti içine bir ya da bir kaç dosya yükledi"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -2203,10 +2199,6 @@
"translation": "Hesap açma bağlantısının süresi geçmiş"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "Hesap açma bağlantısı geçersiz görünüyor"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "Adres kullanılamıyor. Lütfen yeniden deneyin."
},
@@ -2307,6 +2299,14 @@
"translation": "%v takımdan ayrıldı."
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "Kanal amacı iletisi gönderilemedi"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "%v takımı kalıcı olarak silinmeye çalışılıyor. Kod: %v"
},
@@ -2711,6 +2711,10 @@
"translation": "Hesap açma bağlantısı geçersiz görünüyor"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "Hesap açma bağlantısı geçersiz görünüyor"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "Takım adı geçersiz"
},
@@ -3067,6 +3071,10 @@
"translation": "Gelen web bağlantısının yükü okunamadı."
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "Web bağlantısı API rotaları hazırlanıyor"
},
@@ -3691,6 +3699,10 @@
"translation": "Uygulama ekleri ya da uygulama eki yüklemeleri devre dışı bırakılmış. "
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "Bu takım izin verilen en fazla hesap sayısına ulaşmış. Sınırı yükseltmesi için sistem yöneticinizle görüşün."
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "Bu sunucu üzerinde kişisel erişim kodları devre dışı bırakılmış. Lütfen ayrıntılı bilgi almak için sistem yöneticiniz ile görüşün."
},
@@ -4759,6 +4771,10 @@
"translation": "Küçük görsel genişliği dosya ayarı geçersiz. Sıfır ya da pozitif bir sayı olmalı."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Hizmet ayarları okunmamış kanallar gruplansın geçersiz. 'disabled', 'default_on' ya da 'default_off' olmalıdır."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Hizmet ayarlarında görsel vekil sunucu türü geçersiz."
},
@@ -4867,6 +4883,10 @@
"translation": "İleti dışa aktarma görevi DışaAktarmaZamanıBaşlangıcı değeri bir zaman damgası olmalıdır (Unix Epoch başlangıcından itibaren saniye olarak). Yalnız bu zaman damgasından sonra gönderilmiş iletiler dışa aktarılır."
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "İleti dışa aktarma görevi DosyaKonumu değeri dışa aktarılacak verilerin kaydedilebilmesi için yazılabilen bir klasör olmalıdır"
},
@@ -4875,6 +4895,10 @@
"translation": "İleti dışa aktarma görevi DosyaKonumu DosyaAyarları.Klasör değerinin bir alt klasörü olmalıdır"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "En kısa parola uzunluğu {{.MinLength}} ile {{.MaxLength}} arasında bir tamsayı olmalıdır."
},
@@ -7087,6 +7111,10 @@
"translation": "%v SMTP sunucusu üzerinde bağlantı açılamadı"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "SMTP sunucu bağlantısı kapatılamadı"
},
@@ -7155,10 +7183,6 @@
"translation": "Klasör izleyici eklenemedi %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Klasör izleyicide sorun çıktı %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "%v kodlu kullanıcı profili alınırken sorun çıktı, oturum kapatılıyor"
},
@@ -7263,18 +7287,10 @@
"translation": "%v üzerindeki kalıplar işleniyor"
},
{
- "id": "web.parsing_templates.error",
- "translation": "%v üzerindeki kalıplar işlenemedi"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "İleti kodu geçersiz"
},
{
- "id": "web.reparse_templates.info",
- "translation": "%v dosyası değiştirildiğinden kalıplar yeniden işleniyor"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "Parola sıfırlama bağlantısının süresi geçmiş"
},
@@ -7291,10 +7307,6 @@
"translation": "Hesap Açın"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "Hesap açma bağlantısı geçersiz görünüyor"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "Hesap açma bağlantısının süresi geçmiş"
},
@@ -7311,10 +7323,6 @@
"translation": "Hesap açma bağlantısının süresi geçmiş"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "Hesap açma bağlantısı geçersiz görünüyor"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "Takım türü açık çağrılara izin vermiyor"
},
diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json
index fb484b50d..f799bba36 100644
--- a/i18n/zh-CN.json
+++ b/i18n/zh-CN.json
@@ -132,10 +132,6 @@
"translation": "无法上传文件。文件太大。"
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "解析服务模板 %v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "解析服务模板出错 %v"
},
@@ -1162,7 +1158,7 @@
},
{
"id": "api.emoji.delete.delete_reactions.app_error",
- "translation": "无法删除表情符 %v 时删除反应"
+ "translation": "无法删除表情符 %v 时删除互动"
},
{
"id": "api.emoji.delete.permissions.app_error",
@@ -1904,31 +1900,31 @@
},
{
"id": "api.reaction.delete_reaction.mismatched_channel_id.app_error",
- "translation": "因网址中频道 ID 与消息 ID不符而删除反应失败"
+ "translation": "因网址中频道 ID 与消息 ID不符而删除互动失败"
},
{
"id": "api.reaction.init.debug",
- "translation": "正在初始化反应 API 路由"
+ "translation": "正在初始化互动 API 路由"
},
{
"id": "api.reaction.list_reactions.mismatched_channel_id.app_error",
- "translation": "因网址中频道 ID 与消息 ID不符而获取反应失败"
+ "translation": "因网址中频道 ID 与消息 ID不符而获取互动失败"
},
{
"id": "api.reaction.save_reaction.invalid.app_error",
- "translation": "无效反应。"
+ "translation": "无效互动。"
},
{
"id": "api.reaction.save_reaction.mismatched_channel_id.app_error",
- "translation": "因网址中频道 ID 与消息 ID不符而保存反应失败"
+ "translation": "因网址中频道 ID 与消息 ID不符而保存互动失败"
},
{
"id": "api.reaction.save_reaction.user_id.app_error",
- "translation": "您不能保存其他用户的反应。"
+ "translation": "您不能保存其他用户的互动。"
},
{
"id": "api.reaction.send_reaction_event.post.app_error",
- "translation": "发送 websocket 反应事件时获取消息失败"
+ "translation": "发送 websocket 互动事件时获取消息失败"
},
{
"id": "api.saml.save_certificate.app_error",
@@ -2203,10 +2199,6 @@
"translation": "注册链接已过期"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "注册链接无效"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "该URL无效。请尝试其他。"
},
@@ -2307,6 +2299,14 @@
"translation": "%v 离开了团队。"
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "发送频道作用消息失败"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "试图永久删除团队 %v id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "注册链接无效"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "注册链接无效"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "无效团队名称"
},
@@ -3067,6 +3071,10 @@
"translation": "不能读取传入的webhook的负载。"
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "正在初始化 webhook API 路由"
},
@@ -3380,31 +3388,31 @@
},
{
"id": "app.import.validate_reaction_import_data.create_at_before_parent.error",
- "translation": "Reaction CreateAt property must be greater than the parent post CreateAt."
+ "translation": "互动 CreateAt 属性必须大于父消息 CreateAt。"
},
{
"id": "app.import.validate_reaction_import_data.create_at_missing.error",
- "translation": "缺少反应必须属性:create_at。"
+ "translation": "缺少互动必须属性:create_at。"
},
{
"id": "app.import.validate_reaction_import_data.create_at_zero.error",
- "translation": "反应 CreateAt 属性不能为零。"
+ "translation": "互动 CreateAt 属性不能为零。"
},
{
"id": "app.import.validate_reaction_import_data.emoji_name_length.error",
- "translation": "反应 EmojiName 属性超过允许的最大长度。"
+ "translation": "互动 EmojiName 属性超过允许的最大长度。"
},
{
"id": "app.import.validate_reaction_import_data.emoji_name_missing.error",
- "translation": "缺少反应必须属性:User。"
+ "translation": "缺少互动必须属性:User。"
},
{
"id": "app.import.validate_reaction_import_data.user_missing.error",
- "translation": "缺少反应必须属性:User。"
+ "translation": "缺少互动必须属性:User。"
},
{
"id": "app.import.validate_reply_import_data.create_at_before_parent.error",
- "translation": "Reply CreateAt property must be greater than the parent post CreateAt."
+ "translation": "回复 CreateAt 属性必须大于父消息 CreateAt。"
},
{
"id": "app.import.validate_reply_import_data.create_at_missing.error",
@@ -3691,6 +3699,10 @@
"translation": "插件上传已禁用。"
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "这个团队已经达到允许的最大用户数量。请与系统管理员联系以设置更高的限制。"
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "个人访问令牌在本服务器禁用。请联系您的系统管理员了解详情。"
},
@@ -4759,6 +4771,10 @@
"translation": "文件设置时缩略图宽度无效。必须是正数。"
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "无效未读频道分组的服务设定。必须为 'disabled'、'default_on' 或 'default_off'。"
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "无效的图片代理类型服务设定。"
},
@@ -4867,6 +4883,10 @@
"translation": "消息导出任务 ExportFromTimestamp 必须为时间戳 (unix 时间)。只有在此时间后发送的消息会被导出。"
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "消息导出任务 FileLocation 比如为可写的目录"
},
@@ -4875,6 +4895,10 @@
"translation": "消息导出任务 FileLocation 必须为 FileSettings.Directory 的子目录"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "最短密码长度必须为整数大于或等于 {{.MinLength}} 以及小于或等于 {{.MaxLength}}。"
},
@@ -6380,47 +6404,47 @@
},
{
"id": "store.sql_reaction.delete.begin.app_error",
- "translation": "删除反应时无法打开事务"
+ "translation": "删除互动时无法打开事务"
},
{
"id": "store.sql_reaction.delete.commit.app_error",
- "translation": "删除反应时无法提交事务"
+ "translation": "删除互动时无法提交事务"
},
{
"id": "store.sql_reaction.delete.save.app_error",
- "translation": "无法删除反应"
+ "translation": "无法删除互动"
},
{
"id": "store.sql_reaction.delete_all_with_emoj_name.delete_reactions.app_error",
- "translation": "无法用提供的表情符删除反应"
+ "translation": "无法用提供的表情符删除互动"
},
{
"id": "store.sql_reaction.delete_all_with_emoj_name.get_reactions.app_error",
- "translation": "无法用提供的表情符获取反应"
+ "translation": "无法用提供的表情符获取互动"
},
{
"id": "store.sql_reaction.delete_all_with_emoji_name.update_post.warn",
- "translation": "无法删除反应时更新 Post.HasReactions post_id=%v, error=%v"
+ "translation": "无法删除互动时更新 Post.HasReactions post_id=%v, error=%v"
},
{
"id": "store.sql_reaction.get_for_post.app_error",
- "translation": "无法获取消息的反应"
+ "translation": "无法获取消息的互动"
},
{
"id": "store.sql_reaction.permanent_delete_batch.app_error",
- "translation": "批量永久删除反应时遇到错误"
+ "translation": "批量永久删除互动时遇到错误"
},
{
"id": "store.sql_reaction.save.begin.app_error",
- "translation": "无法保存反应时打开事务"
+ "translation": "无法保存互动时打开事务"
},
{
"id": "store.sql_reaction.save.commit.app_error",
- "translation": "无法保存反应时提交事务"
+ "translation": "无法保存互动时提交事务"
},
{
"id": "store.sql_reaction.save.save.app_error",
- "translation": "无法保存反应"
+ "translation": "无法保存互动"
},
{
"id": "store.sql_session.analytics_session_count.app_error",
@@ -7087,6 +7111,10 @@
"translation": "无法打开一个SMTP服务器连接 %v"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "未能关闭连接到SMTP服务器"
},
@@ -7155,10 +7183,6 @@
"translation": "创建目录监视器失败 %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "目录中的监视器失败 %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "获取用户配置文件时出错 id=%v 强制注销"
},
@@ -7263,18 +7287,10 @@
"translation": "解析模板 %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "解析模板失败 %v"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "无效Post ID"
},
{
- "id": "web.reparse_templates.info",
- "translation": "修改文件后重新解析模板 %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "密码重置链接已过期"
},
@@ -7291,10 +7307,6 @@
"translation": "注册"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "注册链接无效"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "注册链接已过期"
},
@@ -7311,10 +7323,6 @@
"translation": "注册链接已过期"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "注册链接无效"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "团队类型不允许公开邀请"
},
diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json
index a7387d88f..292b05879 100644
--- a/i18n/zh-TW.json
+++ b/i18n/zh-TW.json
@@ -132,10 +132,6 @@
"translation": "無法上傳檔案。檔案過大。"
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "解析伺服器樣版:%v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "解析伺服器樣板 %v 失敗"
},
@@ -2203,10 +2199,6 @@
"translation": "註冊連結已過期"
},
{
- "id": "api.team.create_team_from_signup.invalid_link.app_error",
- "translation": "此註冊連結不是有效連結"
- },
- {
"id": "api.team.create_team_from_signup.unavailable.app_error",
"translation": "這個網址不存在。請嘗試其他的。"
},
@@ -2307,6 +2299,14 @@
"translation": "%v 已離開團隊。 "
},
{
+ "id": "api.team.move_channel.post.error",
+ "translation": "發送頻道用途訊息失敗"
+ },
+ {
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
"id": "api.team.permanent_delete_team.attempting.warn",
"translation": "正在嘗試永久刪除團隊 %v id=%v"
},
@@ -2711,6 +2711,10 @@
"translation": "此註冊連結不是有效連結"
},
{
+ "id": "api.user.create_user.signup_link_mismatched_invite_id.app_error",
+ "translation": "此註冊連結不是有效連結"
+ },
+ {
"id": "api.user.create_user.team_name.app_error",
"translation": "無效的團隊名稱"
},
@@ -3067,6 +3071,10 @@
"translation": "無法讀取傳入的 Webhook 的內容。"
},
{
+ "id": "api.webhook.incoming.error",
+ "translation": "Could not decode the multipart payload of incoming webhook."
+ },
+ {
"id": "api.webhook.init.debug",
"translation": "正在初始化 Webhook API 路徑"
},
@@ -3564,7 +3572,7 @@
},
{
"id": "app.import.validate_user_import_data.profile_image.error",
- "translation": "Invalid profile image."
+ "translation": "無效的個人頭像。"
},
{
"id": "app.import.validate_user_import_data.roles_invalid.error",
@@ -3691,6 +3699,10 @@
"translation": "模組 跟/或 上傳模組已被停用。"
},
{
+ "id": "app.team.join_user_to_team.max_accounts.app_error",
+ "translation": "此團隊已達最大使用者數量上限。請聯絡系統管理員調大數量限制。"
+ },
+ {
"id": "app.user_access_token.disabled",
"translation": "個人存取 Token 在此伺服器被停用。詳情請洽管理員。"
},
@@ -4644,7 +4656,7 @@
},
{
"id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error",
- "translation": "Invalid atmos/camo image proxy options for service settings. Must be set to your shared key."
+ "translation": "服務設定中的 atmos/camo 圖片代理無效。必須設定為分享金鑰。"
},
{
"id": "model.config.is_valid.cluster_email_batching.app_error",
@@ -4759,8 +4771,12 @@
"translation": "檔案設定中縮圖寬度無效。必須為正數。"
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "服務設定中的未讀頻道分組無效。必須為 'disabled' 、 'default_on' 或 'default_off'。"
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
- "translation": "Invalid image proxy type for service settings."
+ "translation": "服務設定中的圖片代理類型無效。"
},
{
"id": "model.config.is_valid.ldap_basedn",
@@ -4867,6 +4883,10 @@
"translation": "匯出訊息工作 ExportFromTimestamp 設定必須為時間戳記 (以 Unix Epoch 以來的秒數表示)。只有在此時間戳記之後發送的訊息會被匯出。"
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
"id": "model.config.is_valid.message_export.file_location.app_error",
"translation": "匯出訊息工作 FileLocation 設定必須為可寫入的目錄,匯出資料將寫入此處"
},
@@ -4875,6 +4895,10 @@
"translation": "匯出訊息工作 FileLocation 設定必須為 FileSettings.Directory 的子目錄"
},
{
+ "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"
+ },
+ {
"id": "model.config.is_valid.password_length.app_error",
"translation": "密碼最小長度必須是一個整數且大於或等於{{.MinLength}}同時小於或等於{{.MaxLength}}。"
},
@@ -5404,7 +5428,7 @@
},
{
"id": "model.user.is_valid.position.app_error",
- "translation": "無效的位置:不能超過35個字元。"
+ "translation": "無效的位置:不能超過128個字元。"
},
{
"id": "model.user.is_valid.pwd.app_error",
@@ -6848,7 +6872,7 @@
},
{
"id": "store.sql_user_access_token.get_all.app_error",
- "translation": "無法取得個人存取 Token"
+ "translation": "無法取得全部的個人存取 Token"
},
{
"id": "store.sql_user_access_token.get_by_token.app_error",
@@ -7087,6 +7111,10 @@
"translation": "開啟對 SMTP 伺服器 %v 的連線時失敗"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.send_mail.close.app_error",
"translation": "關閉對 SMTP 伺服器的連線失敗"
},
@@ -7155,10 +7183,6 @@
"translation": "建立目錄監控 %v 失敗"
},
{
- "id": "web.dir_fail.error",
- "translation": "建立目錄監控 %v 失敗"
- },
- {
"id": "web.do_load_channel.error",
"translation": "取得使用者 id=%v 資訊時遇到錯誤,強制登出"
},
@@ -7263,18 +7287,10 @@
"translation": "於 %v 解析樣板"
},
{
- "id": "web.parsing_templates.error",
- "translation": "解析樣板 %v 失敗"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "無效的張貼 ID"
},
{
- "id": "web.reparse_templates.info",
- "translation": "因檔案 %v 變動因此重新解析樣板"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "密碼重設連結已過期"
},
@@ -7291,10 +7307,6 @@
"translation": "註冊"
},
{
- "id": "web.signup_team_complete.invalid_link.app_error",
- "translation": "此註冊連結不是有效連結"
- },
- {
"id": "web.signup_team_complete.link_expired.app_error",
"translation": "註冊連結已過期"
},
@@ -7311,10 +7323,6 @@
"translation": "註冊連結已過期"
},
{
- "id": "web.signup_user_complete.link_invalid.app_error",
- "translation": "此註冊連結不是有效連結"
- },
- {
"id": "web.signup_user_complete.no_invites.app_error",
"translation": "此團隊類型不允許自由加入"
},
diff --git a/model/client4.go b/model/client4.go
index 4b50aa05f..8b17eaa7d 100644
--- a/model/client4.go
+++ b/model/client4.go
@@ -198,6 +198,10 @@ func (c *Client4) GetTestEmailRoute() string {
return fmt.Sprintf("/email/test")
}
+func (c *Client4) GetTestS3Route() string {
+ return fmt.Sprintf("/file/s3_test")
+}
+
func (c *Client4) GetDatabaseRoute() string {
return fmt.Sprintf("/database")
}
@@ -1919,7 +1923,8 @@ func (c *Client4) DoPostAction(postId, actionId string) (bool, *Response) {
// File Section
-// UploadFile will upload a file to a channel, to be later attached to a post.
+// UploadFile will upload a file to a channel using a multipart request, to be later attached to a post.
+// This method is functionally equivalent to Client4.UploadFileAsRequestBody.
func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
@@ -1943,6 +1948,12 @@ func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*F
return c.DoUploadFile(c.GetFilesRoute(), body.Bytes(), writer.FormDataContentType())
}
+// UploadFileAsRequestBody will upload a file to a channel as the body of a request, to be later attached
+// to a post. This method is functionally equivalent to Client4.UploadFile.
+func (c *Client4) UploadFileAsRequestBody(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) {
+ return c.DoUploadFile(c.GetFilesRoute()+fmt.Sprintf("?channel_id=%v&filename=%v", url.QueryEscape(channelId), url.QueryEscape(filename)), data, http.DetectContentType(data))
+}
+
// GetFile gets the bytes for a file by id.
func (c *Client4) GetFile(fileId string) ([]byte, *Response) {
if r, err := c.DoApiGet(c.GetFileRoute(fileId), ""); err != nil {
@@ -2089,6 +2100,16 @@ func (c *Client4) TestEmail() (bool, *Response) {
}
}
+// TestS3Connection will attempt to connect to the AWS S3.
+func (c *Client4) TestS3Connection(config *Config) (bool, *Response) {
+ if r, err := c.DoApiPost(c.GetTestS3Route(), config.ToJson()); err != nil {
+ return false, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return CheckStatusOK(r), BuildResponse(r)
+ }
+}
+
// GetConfig will retrieve the server config with some sanitized items.
func (c *Client4) GetConfig() (*Config, *Response) {
if r, err := c.DoApiGet(c.GetConfigRoute(), ""); err != nil {
@@ -3343,3 +3364,56 @@ func (c *Client4) DeactivatePlugin(id string) (bool, *Response) {
return CheckStatusOK(r), BuildResponse(r)
}
}
+
+// SetTeamIcon sets team icon of the team
+func (c *Client4) SetTeamIcon(teamId string, data []byte) (bool, *Response) {
+
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+
+ if part, err := writer.CreateFormFile("image", "teamIcon.png"); err != nil {
+ return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
+ } else if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
+ return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
+ }
+
+ if err := writer.Close(); err != nil {
+ return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.writer.app_error", nil, err.Error(), http.StatusBadRequest)}
+ }
+
+ rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetTeamRoute(teamId)+"/image", bytes.NewReader(body.Bytes()))
+ rq.Header.Set("Content-Type", writer.FormDataContentType())
+ rq.Close = true
+
+ if len(c.AuthToken) > 0 {
+ rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
+ }
+
+ if rp, err := c.HttpClient.Do(rq); err != nil || rp == nil {
+ // set to http.StatusForbidden(403)
+ return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetTeamRoute(teamId)+"/image", "model.client.connecting.app_error", nil, err.Error(), 403)}
+ } else {
+ defer closeBody(rp)
+
+ if rp.StatusCode >= 300 {
+ return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
+ } else {
+ return CheckStatusOK(rp), BuildResponse(rp)
+ }
+ }
+}
+
+// GetTeamIcon gets the team icon of the team
+func (c *Client4) GetTeamIcon(teamId, etag string) ([]byte, *Response) {
+ if r, err := c.DoApiGet(c.GetTeamRoute(teamId)+"/image", etag); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+
+ if data, err := ioutil.ReadAll(r.Body); err != nil {
+ return nil, BuildErrorResponse(r, NewAppError("GetTeamIcon", "model.client.get_team_icon.app_error", nil, err.Error(), r.StatusCode))
+ } else {
+ return data, BuildResponse(r)
+ }
+ }
+}
diff --git a/model/config.go b/model/config.go
index 93fa5957c..c8cd0f0a1 100644
--- a/model/config.go
+++ b/model/config.go
@@ -165,6 +165,7 @@ const (
type ServiceSettings struct {
SiteURL *string
+ WebsocketURL *string
LicenseFileLocation *string
ListenAddress *string
ConnectionSecurity *string
@@ -196,6 +197,7 @@ type ServiceSettings struct {
EnforceMultifactorAuthentication *bool
EnableUserAccessTokens *bool
AllowCorsFrom *string
+ AllowCookiesForSubdomains *bool
SessionLengthWebInDays *int
SessionLengthMobileInDays *int
SessionLengthSSOInDays *int
@@ -232,6 +234,10 @@ func (s *ServiceSettings) SetDefaults() {
s.SiteURL = NewString(SERVICE_SETTINGS_DEFAULT_SITE_URL)
}
+ if s.WebsocketURL == nil {
+ s.WebsocketURL = NewString("")
+ }
+
if s.LicenseFileLocation == nil {
s.LicenseFileLocation = NewString("")
}
@@ -388,6 +394,10 @@ func (s *ServiceSettings) SetDefaults() {
s.AllowCorsFrom = NewString(SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM)
}
+ if s.AllowCookiesForSubdomains == nil {
+ s.AllowCookiesForSubdomains = NewBool(false)
+ }
+
if s.WebserverMode == nil {
s.WebserverMode = NewString("gzip")
} else if *s.WebserverMode == "regular" {
@@ -1782,6 +1792,10 @@ func (o *Config) IsValid() *AppError {
return NewAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "", http.StatusBadRequest)
}
+ if len(*o.ServiceSettings.SiteURL) == 0 && *o.ServiceSettings.AllowCookiesForSubdomains {
+ return NewAppError("Config.IsValid", "Allowing cookies for subdomains requires SiteURL to be set.", nil, "", http.StatusBadRequest)
+ }
+
if err := o.TeamSettings.isValid(); err != nil {
return err
}
@@ -2089,6 +2103,12 @@ func (ss *ServiceSettings) isValid() *AppError {
}
}
+ if len(*ss.WebsocketURL) != 0 {
+ if _, err := url.ParseRequestURI(*ss.WebsocketURL); err != nil {
+ return NewAppError("Config.IsValid", "model.config.is_valid.websocket_url.app_error", nil, "", http.StatusBadRequest)
+ }
+ }
+
if len(*ss.ListenAddress) == 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "", http.StatusBadRequest)
}
diff --git a/model/scheduled_task.go b/model/scheduled_task.go
index 453828bd2..f3529dedb 100644
--- a/model/scheduled_task.go
+++ b/model/scheduled_task.go
@@ -5,7 +5,6 @@ package model
import (
"fmt"
- "sync"
"time"
)
@@ -15,89 +14,57 @@ type ScheduledTask struct {
Name string `json:"name"`
Interval time.Duration `json:"interval"`
Recurring bool `json:"recurring"`
- function TaskFunc
- timer *time.Timer
-}
-
-var taskMutex = sync.Mutex{}
-var tasks = make(map[string]*ScheduledTask)
-
-func addTask(task *ScheduledTask) {
- taskMutex.Lock()
- defer taskMutex.Unlock()
- tasks[task.Name] = task
-}
-
-func removeTaskByName(name string) {
- taskMutex.Lock()
- defer taskMutex.Unlock()
- delete(tasks, name)
-}
-
-func GetTaskByName(name string) *ScheduledTask {
- taskMutex.Lock()
- defer taskMutex.Unlock()
- if task, ok := tasks[name]; ok {
- return task
- }
- return nil
-}
-
-func GetAllTasks() *map[string]*ScheduledTask {
- taskMutex.Lock()
- defer taskMutex.Unlock()
- return &tasks
+ function func()
+ cancel chan struct{}
+ cancelled chan struct{}
}
func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask {
- task := &ScheduledTask{
- Name: name,
- Interval: timeToExecution,
- Recurring: false,
- function: function,
- }
-
- taskRunner := func() {
- go task.function()
- removeTaskByName(task.Name)
- }
-
- task.timer = time.AfterFunc(timeToExecution, taskRunner)
-
- addTask(task)
-
- return task
+ return createTask(name, function, timeToExecution, false)
}
func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask {
+ return createTask(name, function, interval, true)
+}
+
+func createTask(name string, function TaskFunc, interval time.Duration, recurring bool) *ScheduledTask {
task := &ScheduledTask{
Name: name,
Interval: interval,
- Recurring: true,
+ Recurring: recurring,
function: function,
+ cancel: make(chan struct{}),
+ cancelled: make(chan struct{}),
}
- taskRecurer := func() {
- go task.function()
- task.timer.Reset(task.Interval)
- }
+ go func() {
+ defer close(task.cancelled)
- task.timer = time.AfterFunc(interval, taskRecurer)
+ ticker := time.NewTicker(interval)
+ defer func() {
+ ticker.Stop()
+ }()
- addTask(task)
+ for {
+ select {
+ case <-ticker.C:
+ function()
+ case <-task.cancel:
+ return
+ }
+
+ if !task.Recurring {
+ break
+ }
+ }
+ }()
return task
}
func (task *ScheduledTask) Cancel() {
- task.timer.Stop()
- removeTaskByName(task.Name)
-}
-
-// Executes the task immediatly. A recurring task will be run regularally after interval.
-func (task *ScheduledTask) Execute() {
- task.function()
- task.timer.Reset(task.Interval)
+ close(task.cancel)
+ <-task.cancelled
}
func (task *ScheduledTask) String() string {
diff --git a/model/scheduled_task_test.go b/model/scheduled_task_test.go
index 5af43b1ef..9537a662a 100644
--- a/model/scheduled_task_test.go
+++ b/model/scheduled_task_test.go
@@ -4,185 +4,72 @@
package model
import (
+ "sync/atomic"
"testing"
"time"
+
+ "github.com/stretchr/testify/assert"
)
func TestCreateTask(t *testing.T) {
TASK_NAME := "Test Task"
- TASK_TIME := time.Second * 3
+ TASK_TIME := time.Second * 2
- testValue := 0
+ executionCount := new(int32)
testFunc := func() {
- testValue = 1
+ atomic.AddInt32(executionCount, 1)
}
task := CreateTask(TASK_NAME, testFunc, TASK_TIME)
- if testValue != 0 {
- t.Fatal("Unexpected execuition of task")
- }
+ assert.EqualValues(t, 0, atomic.LoadInt32(executionCount))
time.Sleep(TASK_TIME + time.Second)
- if testValue != 1 {
- t.Fatal("Task did not execute")
- }
-
- if task.Name != TASK_NAME {
- t.Fatal("Bad name")
- }
-
- if task.Interval != TASK_TIME {
- t.Fatal("Bad interval")
- }
-
- if task.Recurring {
- t.Fatal("should not reccur")
- }
+ assert.EqualValues(t, 1, atomic.LoadInt32(executionCount))
+ assert.Equal(t, TASK_NAME, task.Name)
+ assert.Equal(t, TASK_TIME, task.Interval)
+ assert.False(t, task.Recurring)
}
func TestCreateRecurringTask(t *testing.T) {
TASK_NAME := "Test Recurring Task"
- TASK_TIME := time.Second * 3
+ TASK_TIME := time.Second * 2
- testValue := 0
+ executionCount := new(int32)
testFunc := func() {
- testValue += 1
+ atomic.AddInt32(executionCount, 1)
}
task := CreateRecurringTask(TASK_NAME, testFunc, TASK_TIME)
- if testValue != 0 {
- t.Fatal("Unexpected execuition of task")
- }
+ assert.EqualValues(t, 0, atomic.LoadInt32(executionCount))
time.Sleep(TASK_TIME + time.Second)
- if testValue != 1 {
- t.Fatal("Task did not execute")
- }
+ assert.EqualValues(t, 1, atomic.LoadInt32(executionCount))
time.Sleep(TASK_TIME)
- if testValue != 2 {
- t.Fatal("Task did not re-execute")
- }
-
- if task.Name != TASK_NAME {
- t.Fatal("Bad name")
- }
-
- if task.Interval != TASK_TIME {
- t.Fatal("Bad interval")
- }
-
- if !task.Recurring {
- t.Fatal("should reccur")
- }
+ assert.EqualValues(t, 2, atomic.LoadInt32(executionCount))
+ assert.Equal(t, TASK_NAME, task.Name)
+ assert.Equal(t, TASK_TIME, task.Interval)
+ assert.True(t, task.Recurring)
task.Cancel()
}
func TestCancelTask(t *testing.T) {
TASK_NAME := "Test Task"
- TASK_TIME := time.Second * 3
+ TASK_TIME := time.Second
- testValue := 0
+ executionCount := new(int32)
testFunc := func() {
- testValue = 1
+ atomic.AddInt32(executionCount, 1)
}
task := CreateTask(TASK_NAME, testFunc, TASK_TIME)
- if testValue != 0 {
- t.Fatal("Unexpected execuition of task")
- }
+ assert.EqualValues(t, 0, atomic.LoadInt32(executionCount))
task.Cancel()
time.Sleep(TASK_TIME + time.Second)
-
- if testValue != 0 {
- t.Fatal("Unexpected execuition of task")
- }
-}
-
-func TestGetAllTasks(t *testing.T) {
- doNothing := func() {}
-
- CreateTask("Task1", doNothing, time.Hour)
- CreateTask("Task2", doNothing, time.Second)
- CreateRecurringTask("Task3", doNothing, time.Second)
- task4 := CreateRecurringTask("Task4", doNothing, time.Second)
-
- task4.Cancel()
-
- time.Sleep(time.Second * 3)
-
- tasks := *GetAllTasks()
- if len(tasks) != 2 {
- t.Fatal("Wrong number of tasks got: ", len(tasks))
- }
- for _, task := range tasks {
- if task.Name != "Task1" && task.Name != "Task3" {
- t.Fatal("Wrong tasks")
- }
- }
-}
-
-func TestExecuteTask(t *testing.T) {
- TASK_NAME := "Test Task"
- TASK_TIME := time.Second * 5
-
- testValue := 0
- testFunc := func() {
- testValue += 1
- }
-
- task := CreateTask(TASK_NAME, testFunc, TASK_TIME)
- if testValue != 0 {
- t.Fatal("Unexpected execuition of task")
- }
-
- task.Execute()
-
- if testValue != 1 {
- t.Fatal("Task did not execute")
- }
-
- time.Sleep(TASK_TIME + time.Second)
-
- if testValue != 2 {
- t.Fatal("Task re-executed")
- }
-}
-
-func TestExecuteTaskRecurring(t *testing.T) {
- TASK_NAME := "Test Recurring Task"
- TASK_TIME := time.Second * 5
-
- testValue := 0
- testFunc := func() {
- testValue += 1
- }
-
- task := CreateRecurringTask(TASK_NAME, testFunc, TASK_TIME)
- if testValue != 0 {
- t.Fatal("Unexpected execuition of task")
- }
-
- time.Sleep(time.Second * 3)
-
- task.Execute()
- if testValue != 1 {
- t.Fatal("Task did not execute")
- }
-
- time.Sleep(time.Second * 3)
- if testValue != 1 {
- t.Fatal("Task should not have executed before 5 seconds")
- }
-
- time.Sleep(time.Second * 3)
-
- if testValue != 2 {
- t.Fatal("Task did not re-execute after forced execution")
- }
+ assert.EqualValues(t, 0, atomic.LoadInt32(executionCount))
}
diff --git a/model/team.go b/model/team.go
index 5b6eb1fa0..15a708220 100644
--- a/model/team.go
+++ b/model/team.go
@@ -26,19 +26,20 @@ const (
)
type Team struct {
- Id string `json:"id"`
- CreateAt int64 `json:"create_at"`
- UpdateAt int64 `json:"update_at"`
- DeleteAt int64 `json:"delete_at"`
- DisplayName string `json:"display_name"`
- Name string `json:"name"`
- Description string `json:"description"`
- Email string `json:"email"`
- Type string `json:"type"`
- CompanyName string `json:"company_name"`
- AllowedDomains string `json:"allowed_domains"`
- InviteId string `json:"invite_id"`
- AllowOpenInvite bool `json:"allow_open_invite"`
+ Id string `json:"id"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ DisplayName string `json:"display_name"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Email string `json:"email"`
+ Type string `json:"type"`
+ CompanyName string `json:"company_name"`
+ AllowedDomains string `json:"allowed_domains"`
+ InviteId string `json:"invite_id"`
+ AllowOpenInvite bool `json:"allow_open_invite"`
+ LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"`
}
type TeamPatch struct {
diff --git a/model/version.go b/model/version.go
index 1bd7baecc..6e461e5d5 100644
--- a/model/version.go
+++ b/model/version.go
@@ -13,6 +13,7 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
+ "4.7.1",
"4.7.0",
"4.6.0",
"4.5.0",
diff --git a/model/websocket_message.go b/model/websocket_message.go
index a1427e196..aea77b1b6 100644
--- a/model/websocket_message.go
+++ b/model/websocket_message.go
@@ -5,6 +5,7 @@ package model
import (
"encoding/json"
+ "fmt"
"io"
)
@@ -59,11 +60,32 @@ type WebsocketBroadcast struct {
TeamId string `json:"team_id"` // broadcast only occurs for users in this team
}
+type precomputedWebSocketEventJSON struct {
+ Event json.RawMessage
+ Data json.RawMessage
+ Broadcast json.RawMessage
+}
+
type WebSocketEvent struct {
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
Broadcast *WebsocketBroadcast `json:"broadcast"`
Sequence int64 `json:"seq"`
+
+ precomputedJSON *precomputedWebSocketEventJSON
+}
+
+// PrecomputeJSON precomputes and stores the serialized JSON for all fields other than Sequence.
+// This makes ToJson much more efficient when sending the same event to multiple connections.
+func (m *WebSocketEvent) PrecomputeJSON() {
+ event, _ := json.Marshal(m.Event)
+ data, _ := json.Marshal(m.Data)
+ broadcast, _ := json.Marshal(m.Broadcast)
+ m.precomputedJSON = &precomputedWebSocketEventJSON{
+ Event: json.RawMessage(event),
+ Data: json.RawMessage(data),
+ Broadcast: json.RawMessage(broadcast),
+ }
}
func (m *WebSocketEvent) Add(key string, value interface{}) {
@@ -84,6 +106,9 @@ func (o *WebSocketEvent) EventType() string {
}
func (o *WebSocketEvent) ToJson() string {
+ if o.precomputedJSON != nil {
+ return fmt.Sprintf(`{"event": %s, "data": %s, "broadcast": %s, "seq": %d}`, o.precomputedJSON.Event, o.precomputedJSON.Data, o.precomputedJSON.Broadcast, o.Sequence)
+ }
b, _ := json.Marshal(o)
return string(b)
}
diff --git a/model/websocket_message_test.go b/model/websocket_message_test.go
index 1b75d0f6e..10404c299 100644
--- a/model/websocket_message_test.go
+++ b/model/websocket_message_test.go
@@ -6,6 +6,8 @@ package model
import (
"strings"
"testing"
+
+ "github.com/stretchr/testify/assert"
)
func TestWebSocketEvent(t *testing.T) {
@@ -54,3 +56,49 @@ func TestWebSocketResponse(t *testing.T) {
t.Fatal("Ids do not match")
}
}
+
+func TestWebSocketEvent_PrecomputeJSON(t *testing.T) {
+ event := NewWebSocketEvent(WEBSOCKET_EVENT_POSTED, "foo", "bar", "baz", nil)
+ event.Sequence = 7
+
+ before := event.ToJson()
+ event.PrecomputeJSON()
+ after := event.ToJson()
+
+ assert.JSONEq(t, before, after)
+}
+
+var stringSink string
+
+func BenchmarkWebSocketEvent_ToJson(b *testing.B) {
+ event := NewWebSocketEvent(WEBSOCKET_EVENT_POSTED, "foo", "bar", "baz", nil)
+ for i := 0; i < 100; i++ {
+ event.Data[NewId()] = NewId()
+ }
+
+ b.Run("SerializedNTimes", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ stringSink = event.ToJson()
+ }
+ })
+
+ b.Run("PrecomputedNTimes", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ event.PrecomputeJSON()
+ }
+ })
+
+ b.Run("PrecomputedAndSerializedNTimes", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ event.PrecomputeJSON()
+ stringSink = event.ToJson()
+ }
+ })
+
+ event.PrecomputeJSON()
+ b.Run("PrecomputedOnceAndSerializedNTimes", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ stringSink = event.ToJson()
+ }
+ })
+}
diff --git a/plugin/rpcplugin/sandbox/sandbox_linux.go b/plugin/rpcplugin/sandbox/sandbox_linux.go
index dad485f68..c83572c82 100644
--- a/plugin/rpcplugin/sandbox/sandbox_linux.go
+++ b/plugin/rpcplugin/sandbox/sandbox_linux.go
@@ -267,7 +267,7 @@ func pivotRoot(newRoot string) error {
func dropInheritableCapabilities() error {
type capHeader struct {
version uint32
- pid int
+ pid int32
}
type capData struct {
diff --git a/scripts/prereq-check.sh b/scripts/prereq-check.sh
index 1c9ae8405..6f2954273 100755
--- a/scripts/prereq-check.sh
+++ b/scripts/prereq-check.sh
@@ -2,7 +2,7 @@
check_version()
{
local version=$1 check=$2
- local winner=$(echo -e "$version\n$check" | sed '/^$/d' | sort -nr | head -1)
+ local winner=$(echo -e "$version\n$check" | sed '/^$/d' | sort -t. -s -k 1,1nr -k 2,2nr -k 3,3nr -k 4,4nr | head -1)
[[ "$winner" = "$version" ]] && return 0
return 1
}
@@ -46,4 +46,4 @@ DOCKERVERSION=$(docker version --format '{{.Server.Version}}' | sed 's/[a-z-]//g
check_prereq 'node' $REQUIREDNODEVERSION $NODEVERSION
check_prereq 'npm' $REQUIREDNPMVERSION $NPMVERSION
check_prereq 'go' $REQUIREDGOVERSION $GOVERSION
-check_prereq 'docker' $REQUIREDDOCKERVERSION $DOCKERVERSION \ No newline at end of file
+check_prereq 'docker' $REQUIREDDOCKERVERSION $DOCKERVERSION
diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go
index 75a615aee..131e5649b 100644
--- a/store/sqlstore/channel_store.go
+++ b/store/sqlstore/channel_store.go
@@ -946,9 +946,9 @@ func (s SqlChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId str
var data []allChannelMemberNotifyProps
_, err := s.GetReplica().Select(&data, `
- SELECT ChannelMembers.UserId, ChannelMembers.NotifyProps
- FROM Channels, ChannelMembers
- WHERE Channels.Id = ChannelMembers.ChannelId AND ChannelMembers.ChannelId = :ChannelId`, map[string]interface{}{"ChannelId": channelId})
+ SELECT UserId, NotifyProps
+ FROM ChannelMembers
+ WHERE ChannelId = :ChannelId`, map[string]interface{}{"ChannelId": channelId})
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetAllChannelMembersPropsForChannel", "store.sql_channel.get_members.app_error", nil, "channelId="+channelId+", err="+err.Error(), http.StatusInternalServerError)
diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go
index cddfb7c1a..6528b8e4c 100644
--- a/store/sqlstore/team_store.go
+++ b/store/sqlstore/team_store.go
@@ -99,6 +99,7 @@ func (s SqlTeamStore) Update(team *model.Team) store.StoreChannel {
team.CreateAt = oldTeam.CreateAt
team.UpdateAt = model.GetMillis()
team.Name = oldTeam.Name
+ team.LastTeamIconUpdate = oldTeam.LastTeamIconUpdate
if count, err := s.GetMaster().Update(team); err != nil {
result.Err = model.NewAppError("SqlTeamStore.Update", "store.sql_team.update.updating.app_error", nil, "id="+team.Id+", "+err.Error(), http.StatusInternalServerError)
@@ -559,3 +560,13 @@ func (s SqlTeamStore) RemoveAllMembersByUser(userId string) store.StoreChannel {
}
})
}
+
+func (us SqlTeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ if _, err := us.GetMaster().Exec("UPDATE Teams SET LastTeamIconUpdate = :Time, UpdateAt = :Time WHERE Id = :teamId", map[string]interface{}{"Time": curTime, "teamId": teamId}); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.UpdateLastTeamIconUpdate", "store.sql_team.update_last_team_icon_update.app_error", nil, "team_id="+teamId, http.StatusInternalServerError)
+ } else {
+ result.Data = teamId
+ }
+ })
+}
diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go
index 8d1513381..de4dbe095 100644
--- a/store/sqlstore/upgrade.go
+++ b/store/sqlstore/upgrade.go
@@ -16,6 +16,7 @@ import (
const (
VERSION_4_8_0 = "4.8.0"
+ VERSION_4_7_1 = "4.7.1"
VERSION_4_7_0 = "4.7.0"
VERSION_4_6_0 = "4.6.0"
VERSION_4_5_0 = "4.5.0"
@@ -65,6 +66,7 @@ func UpgradeDatabase(sqlStore SqlStore) {
UpgradeDatabaseToVersion45(sqlStore)
UpgradeDatabaseToVersion46(sqlStore)
UpgradeDatabaseToVersion47(sqlStore)
+ UpgradeDatabaseToVersion471(sqlStore)
UpgradeDatabaseToVersion48(sqlStore)
// If the SchemaVersion is empty this this is the first time it has ran
@@ -352,6 +354,16 @@ func UpgradeDatabaseToVersion47(sqlStore SqlStore) {
}
}
+// If any new instances started with 4.7, they would have the bad Email column on the
+// ChannelMemberHistory table. So for those cases we need to do an upgrade between
+// 4.7.0 and 4.7.1
+func UpgradeDatabaseToVersion471(sqlStore SqlStore) {
+ if shouldPerformUpgrade(sqlStore, VERSION_4_7_0, VERSION_4_7_1) {
+ sqlStore.RemoveColumnIfExists("ChannelMemberHistory", "Email")
+ saveSchemaVersion(sqlStore, VERSION_4_7_1)
+ }
+}
+
func UpgradeDatabaseToVersion48(sqlStore SqlStore) {
// This version of Mattermost includes an App-Layer migration which migrates from hard-coded roles configured by
// a number of parameters in `config.json` to a `Roles` table in the database. The migration code can be seen
@@ -359,6 +371,7 @@ func UpgradeDatabaseToVersion48(sqlStore SqlStore) {
//TODO: Uncomment the following condition when version 4.8.0 is released
//if shouldPerformUpgrade(sqlStore, VERSION_4_7_0, VERSION_4_8_0) {
+ sqlStore.CreateColumnIfNotExists("Teams", "LastTeamIconUpdate", "bigint", "bigint", "0")
// saveSchemaVersion(sqlStore, VERSION_4_8_0)
//}
}
diff --git a/store/store.go b/store/store.go
index 9364218c8..5da91c071 100644
--- a/store/store.go
+++ b/store/store.go
@@ -104,6 +104,7 @@ type TeamStore interface {
RemoveMember(teamId string, userId string) StoreChannel
RemoveAllMembersByTeam(teamId string) StoreChannel
RemoveAllMembersByUser(userId string) StoreChannel
+ UpdateLastTeamIconUpdate(teamId string, curTime int64) StoreChannel
}
type ChannelStore interface {
diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go
index bdad7f81b..8a7f030dc 100644
--- a/store/storetest/mocks/TeamStore.go
+++ b/store/storetest/mocks/TeamStore.go
@@ -476,3 +476,19 @@ func (_m *TeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel {
return r0
}
+
+// UpdateLastTeamIconUpdate provides a mock function with given fields: teamId
+func (_m *TeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) store.StoreChannel {
+ ret := _m.Called(teamId)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, int64) store.StoreChannel); ok {
+ r0 = rf(teamId, curTime)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}
diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go
index a32de9dba..cab06f87f 100644
--- a/store/storetest/team_store.go
+++ b/store/storetest/team_store.go
@@ -33,6 +33,7 @@ func TestTeamStore(t *testing.T, ss store.Store) {
t.Run("MemberCount", func(t *testing.T) { testTeamStoreMemberCount(t, ss) })
t.Run("GetChannelUnreadsForAllTeams", func(t *testing.T) { testGetChannelUnreadsForAllTeams(t, ss) })
t.Run("GetChannelUnreadsForTeam", func(t *testing.T) { testGetChannelUnreadsForTeam(t, ss) })
+ t.Run("UpdateLastTeamIconUpdate", func(t *testing.T) { testUpdateLastTeamIconUpdate(t, ss) })
}
func testTeamStoreSave(t *testing.T, ss store.Store) {
@@ -1003,3 +1004,28 @@ func testGetChannelUnreadsForTeam(t *testing.T, ss store.Store) {
}
}
}
+
+func testUpdateLastTeamIconUpdate(t *testing.T, ss store.Store) {
+
+ // team icon initially updated a second ago
+ lastTeamIconUpdateInitial := model.GetMillis() - 1000
+
+ o1 := &model.Team{}
+ o1.DisplayName = "Display Name"
+ o1.Name = "z-z-z" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+ o1.LastTeamIconUpdate = lastTeamIconUpdateInitial
+ o1 = (<-ss.Team().Save(o1)).Data.(*model.Team)
+
+ curTime := model.GetMillis()
+
+ if err := (<-ss.Team().UpdateLastTeamIconUpdate(o1.Id, curTime)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ ro1 := (<-ss.Team().Get(o1.Id)).Data.(*model.Team)
+ if ro1.LastTeamIconUpdate <= lastTeamIconUpdateInitial {
+ t.Fatal("LastTeamIconUpdate not updated")
+ }
+}
diff --git a/utils/api.go b/utils/api.go
index 51524074d..0f2640829 100644
--- a/utils/api.go
+++ b/utils/api.go
@@ -52,7 +52,7 @@ func RenderWebError(w http.ResponseWriter, r *http.Request, status int, params u
http.Error(w, "", http.StatusInternalServerError)
return
}
- destination := strings.TrimRight(GetSiteURL(), "/") + "/error?" + queryString + "&s=" + base64.URLEncoding.EncodeToString(signature)
+ destination := "/error?" + queryString + "&s=" + base64.URLEncoding.EncodeToString(signature)
if status >= 300 && status < 400 {
http.Redirect(w, r, destination, status)
diff --git a/utils/config.go b/utils/config.go
index 10ada1728..b28cf918d 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -34,15 +34,6 @@ const (
)
var originalDisableDebugLvl l4g.Level = l4g.DEBUG
-var siteURL = ""
-
-func GetSiteURL() string {
- return siteURL
-}
-
-func SetSiteURL(url string) {
- siteURL = strings.TrimRight(url, "/")
-}
// FindConfigFile attempts to find an existing configuration file. fileName can be an absolute or
// relative path or name such as "/opt/mattermost/config.json" or simply "config.json". An empty
@@ -353,8 +344,10 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
props["BuildEnterpriseReady"] = model.BuildEnterpriseReady
props["SiteURL"] = strings.TrimRight(*c.ServiceSettings.SiteURL, "/")
+ props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/")
props["SiteName"] = c.TeamSettings.SiteName
props["EnableTeamCreation"] = strconv.FormatBool(*c.TeamSettings.EnableTeamCreation)
+ props["EnableAPIv3"] = strconv.FormatBool(*c.ServiceSettings.EnableAPIv3)
props["EnableUserCreation"] = strconv.FormatBool(c.TeamSettings.EnableUserCreation)
props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer)
props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage
diff --git a/utils/file_backend_s3.go b/utils/file_backend_s3.go
index 7ef150851..b0601bc8a 100644
--- a/utils/file_backend_s3.go
+++ b/utils/file_backend_s3.go
@@ -37,7 +37,10 @@ type S3FileBackend struct {
// disables automatic region lookup.
func (b *S3FileBackend) s3New() (*s3.Client, error) {
var creds *credentials.Credentials
- if b.signV2 {
+
+ if b.accessKey == "" && b.secretKey == "" {
+ creds = credentials.NewIAM("")
+ } else if b.signV2 {
creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV2)
} else {
creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV4)
@@ -138,17 +141,15 @@ func (b *S3FileBackend) WriteFile(f []byte, path string) *model.AppError {
return model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
}
- options := s3.PutObjectOptions{}
- if b.encrypt {
- options.UserMetadata["x-amz-server-side-encryption"] = "AES256"
- }
-
+ var contentType string
if ext := filepath.Ext(path); model.IsFileExtImage(ext) {
- options.ContentType = model.GetImageMimeType(ext)
+ contentType = model.GetImageMimeType(ext)
} else {
- options.ContentType = "binary/octet-stream"
+ contentType = "binary/octet-stream"
}
+ options := s3PutOptions(b.encrypt, contentType)
+
if _, err = s3Clnt.PutObject(b.bucket, path, bytes.NewReader(f), -1, options); err != nil {
return model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
}
@@ -230,8 +231,35 @@ func (b *S3FileBackend) RemoveDirectory(path string) *model.AppError {
return nil
}
+func s3PutOptions(encrypt bool, contentType string) s3.PutObjectOptions {
+ options := s3.PutObjectOptions{}
+ if encrypt {
+ options.UserMetadata = make(map[string]string)
+ options.UserMetadata["x-amz-server-side-encryption"] = "AES256"
+ }
+ options.ContentType = contentType
+
+ return options
+}
+
func s3CopyMetadata(encrypt bool) map[string]string {
metaData := make(map[string]string)
metaData["x-amz-server-side-encryption"] = "AES256"
return metaData
}
+
+func CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
+ if len(settings.AmazonS3Bucket) == 0 {
+ return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest)
+ }
+
+ 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)
+ }
+
+ return nil
+}
diff --git a/utils/file_backend_s3_test.go b/utils/file_backend_s3_test.go
new file mode 100644
index 000000000..ff42a4d19
--- /dev/null
+++ b/utils/file_backend_s3_test.go
@@ -0,0 +1,32 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "testing"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func TestCheckMandatoryS3Fields(t *testing.T) {
+ cfg := model.FileSettings{}
+
+ err := CheckMandatoryS3Fields(&cfg)
+ if err == nil || err.Message != "api.admin.test_s3.missing_s3_bucket" {
+ t.Fatal("should've failed with missing s3 bucket")
+ }
+
+ 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")
+ }
+
+ cfg.AmazonS3Endpoint = "s3.newendpoint.com"
+ err = CheckMandatoryS3Fields(&cfg)
+ if err == nil || err.Message != "api.admin.test_s3.missing_s3_region" {
+ t.Fatal("should've failed with missing s3 region")
+ }
+
+}
diff --git a/utils/file_backend_test.go b/utils/file_backend_test.go
index 46f75574e..2b8e2a527 100644
--- a/utils/file_backend_test.go
+++ b/utils/file_backend_test.go
@@ -36,6 +36,14 @@ func TestLocalFileBackendTestSuite(t *testing.T) {
}
func TestS3FileBackendTestSuite(t *testing.T) {
+ runBackendTest(t, false)
+}
+
+func TestS3FileBackendTestSuiteWithEncryption(t *testing.T) {
+ runBackendTest(t, true)
+}
+
+func runBackendTest(t *testing.T, encrypt bool) {
s3Host := os.Getenv("CI_HOST")
if s3Host == "" {
s3Host = "dockerhost"
@@ -56,6 +64,7 @@ func TestS3FileBackendTestSuite(t *testing.T) {
AmazonS3Bucket: model.MINIO_BUCKET,
AmazonS3Endpoint: s3Endpoint,
AmazonS3SSL: model.NewBool(false),
+ AmazonS3SSE: model.NewBool(encrypt),
},
})
}
@@ -86,6 +95,20 @@ func (s *FileBackendTestSuite) TestReadWriteFile() {
s.EqualValues(readString, "test")
}
+func (s *FileBackendTestSuite) TestReadWriteFileImage() {
+ b := []byte("testimage")
+ path := "tests/" + model.NewId() + ".png"
+
+ s.Nil(s.backend.WriteFile(b, path))
+ defer s.backend.RemoveFile(path)
+
+ read, err := s.backend.ReadFile(path)
+ s.Nil(err)
+
+ readString := string(read)
+ s.EqualValues(readString, "testimage")
+}
+
func (s *FileBackendTestSuite) TestCopyFile() {
b := []byte("test")
path1 := "tests/" + model.NewId()
diff --git a/utils/lru.go b/utils/lru.go
index 576331563..8e896a6dc 100644
--- a/utils/lru.go
+++ b/utils/lru.go
@@ -9,15 +9,14 @@ package utils
import (
"container/list"
- "errors"
"sync"
"time"
)
// Caching Interface
type ObjectCache interface {
- AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64) bool
- AddWithDefaultExpires(key, value interface{}) bool
+ AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64)
+ AddWithDefaultExpires(key, value interface{})
Purge()
Get(key interface{}) (value interface{}, ok bool)
Remove(key interface{})
@@ -32,10 +31,11 @@ type Cache struct {
evictList *list.List
items map[interface{}]*list.Element
lock sync.RWMutex
- onEvicted func(key interface{}, value interface{})
name string
defaultExpiry int64
invalidateClusterEvent string
+ currentGeneration int64
+ len int
}
// entry is used to hold a value in the evictList
@@ -43,25 +43,16 @@ type entry struct {
key interface{}
value interface{}
expireAtSecs int64
+ generation int64
}
// New creates an LRU of the given size
func NewLru(size int) *Cache {
- cache, _ := NewLruWithEvict(size, nil)
- return cache
-}
-
-func NewLruWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) {
- if size <= 0 {
- return nil, errors.New(T("utils.iru.with_evict"))
- }
- c := &Cache{
+ return &Cache{
size: size,
evictList: list.New(),
items: make(map[interface{}]*list.Element, size),
- onEvicted: onEvicted,
}
- return c, nil
}
func NewLruWithParams(size int, name string, defaultExpiry int64, invalidateClusterEvent string) *Cache {
@@ -77,26 +68,19 @@ func (c *Cache) Purge() {
c.lock.Lock()
defer c.lock.Unlock()
- if c.onEvicted != nil {
- for k, v := range c.items {
- c.onEvicted(k, v.Value)
- }
- }
-
- c.evictList = list.New()
- c.items = make(map[interface{}]*list.Element, c.size)
+ c.len = 0
+ c.currentGeneration++
}
-func (c *Cache) Add(key, value interface{}) bool {
- return c.AddWithExpiresInSecs(key, value, 0)
+func (c *Cache) Add(key, value interface{}) {
+ c.AddWithExpiresInSecs(key, value, 0)
}
-func (c *Cache) AddWithDefaultExpires(key, value interface{}) bool {
- return c.AddWithExpiresInSecs(key, value, c.defaultExpiry)
+func (c *Cache) AddWithDefaultExpires(key, value interface{}) {
+ c.AddWithExpiresInSecs(key, value, c.defaultExpiry)
}
-// Add adds a value to the cache. Returns true if an eviction occurred.
-func (c *Cache) AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64) bool {
+func (c *Cache) AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64) {
c.lock.Lock()
defer c.lock.Unlock()
@@ -107,45 +91,46 @@ func (c *Cache) AddWithExpiresInSecs(key, value interface{}, expireAtSecs int64)
// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
- ent.Value.(*entry).value = value
- ent.Value.(*entry).expireAtSecs = expireAtSecs
- return false
+ e := ent.Value.(*entry)
+ e.value = value
+ e.expireAtSecs = expireAtSecs
+ if e.generation != c.currentGeneration {
+ e.generation = c.currentGeneration
+ c.len++
+ }
+ return
}
// Add new item
- ent := &entry{key, value, expireAtSecs}
+ ent := &entry{key, value, expireAtSecs, c.currentGeneration}
entry := c.evictList.PushFront(ent)
c.items[key] = entry
+ c.len++
- evict := c.evictList.Len() > c.size
- // Verify size not exceeded
- if evict {
- c.removeOldest()
+ if c.evictList.Len() > c.size {
+ c.removeElement(c.evictList.Back())
}
- return evict
}
-// Get looks up a key's value from the cache.
func (c *Cache) Get(key interface{}) (value interface{}, ok bool) {
c.lock.Lock()
defer c.lock.Unlock()
if ent, ok := c.items[key]; ok {
+ e := ent.Value.(*entry)
- if ent.Value.(*entry).expireAtSecs > 0 {
- if (time.Now().UnixNano() / int64(time.Second)) > ent.Value.(*entry).expireAtSecs {
- c.removeElement(ent)
- return nil, false
- }
+ if e.generation != c.currentGeneration || (e.expireAtSecs > 0 && (time.Now().UnixNano()/int64(time.Second)) > e.expireAtSecs) {
+ c.removeElement(ent)
+ return nil, false
}
c.evictList.MoveToFront(ent)
return ent.Value.(*entry).value, true
}
- return
+
+ return nil, false
}
-// Remove removes the provided key from the cache.
func (c *Cache) Remove(key interface{}) {
c.lock.Lock()
defer c.lock.Unlock()
@@ -155,25 +140,19 @@ func (c *Cache) Remove(key interface{}) {
}
}
-// RemoveOldest removes the oldest item from the cache.
-func (c *Cache) RemoveOldest() {
- c.lock.Lock()
- defer c.lock.Unlock()
- c.removeOldest()
-}
-
// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *Cache) Keys() []interface{} {
c.lock.RLock()
defer c.lock.RUnlock()
- keys := make([]interface{}, len(c.items))
- ent := c.evictList.Back()
+ keys := make([]interface{}, c.len)
i := 0
- for ent != nil {
- keys[i] = ent.Value.(*entry).key
- ent = ent.Prev()
- i++
+ for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
+ e := ent.Value.(*entry)
+ if e.generation == c.currentGeneration {
+ keys[i] = e.key
+ i++
+ }
}
return keys
@@ -183,7 +162,7 @@ func (c *Cache) Keys() []interface{} {
func (c *Cache) Len() int {
c.lock.RLock()
defer c.lock.RUnlock()
- return c.evictList.Len()
+ return c.len
}
func (c *Cache) Name() string {
@@ -194,20 +173,12 @@ func (c *Cache) GetInvalidateClusterEvent() string {
return c.invalidateClusterEvent
}
-// removeOldest removes the oldest item from the cache.
-func (c *Cache) removeOldest() {
- ent := c.evictList.Back()
- if ent != nil {
- c.removeElement(ent)
- }
-}
-
// removeElement is used to remove a given list element from the cache
func (c *Cache) removeElement(e *list.Element) {
c.evictList.Remove(e)
kv := e.Value.(*entry)
- delete(c.items, kv.key)
- if c.onEvicted != nil {
- c.onEvicted(kv.key, kv.value)
+ if kv.generation == c.currentGeneration {
+ c.len--
}
+ delete(c.items, kv.key)
}
diff --git a/utils/lru_test.go b/utils/lru_test.go
index 987163cd3..4312515b9 100644
--- a/utils/lru_test.go
+++ b/utils/lru_test.go
@@ -11,14 +11,7 @@ import "testing"
import "time"
func TestLRU(t *testing.T) {
- evictCounter := 0
- onEvicted := func(k interface{}, v interface{}) {
- evictCounter += 1
- }
- l, err := NewLruWithEvict(128, onEvicted)
- if err != nil {
- t.Fatalf("err: %v", err)
- }
+ l := NewLru(128)
for i := 0; i < 256; i++ {
l.Add(i, i)
@@ -27,10 +20,6 @@ func TestLRU(t *testing.T) {
t.Fatalf("bad len: %v", l.Len())
}
- if evictCounter != 128 {
- t.Fatalf("bad evict count: %v", evictCounter)
- }
-
for i, k := range l.Keys() {
if v, ok := l.Get(k); !ok || v != k || v != i+128 {
t.Fatalf("bad key: %v", k)
@@ -73,26 +62,6 @@ func TestLRU(t *testing.T) {
}
}
-// test that Add return true/false if an eviction occurred
-func TestLRUAdd(t *testing.T) {
- evictCounter := 0
- onEvicted := func(k interface{}, v interface{}) {
- evictCounter += 1
- }
-
- l, err := NewLruWithEvict(1, onEvicted)
- if err != nil {
- t.Fatalf("err: %v", err)
- }
-
- if l.Add(1, 1) || evictCounter != 0 {
- t.Errorf("should not have an eviction")
- }
- if !l.Add(2, 2) || evictCounter != 1 {
- t.Errorf("should have an eviction")
- }
-}
-
func TestLRUExpire(t *testing.T) {
l := NewLru(128)
diff --git a/utils/mail.go b/utils/mail.go
index 9023f7090..2bc0ce9e1 100644
--- a/utils/mail.go
+++ b/utils/mail.go
@@ -5,6 +5,8 @@ package utils
import (
"crypto/tls"
+ "errors"
+ "io"
"mime"
"net"
"net/mail"
@@ -15,8 +17,6 @@ import (
"net/http"
- "io"
-
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/html2text"
"github.com/mattermost/mattermost-server/model"
@@ -26,6 +26,56 @@ func encodeRFC2047Word(s string) string {
return mime.BEncoding.Encode("utf-8", s)
}
+type authChooser struct {
+ smtp.Auth
+ Config *model.Config
+}
+
+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)
+ 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)
+ break
+ }
+ }
+ return a.Auth.Start(server)
+}
+
+type loginAuth struct {
+ username, password, host string
+}
+
+func LoginAuth(username, password, host string) smtp.Auth {
+ return &loginAuth{username, password, host}
+}
+
+func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+ if !server.TLS {
+ return "", nil, errors.New("unencrypted connection")
+ }
+
+ if server.Name != a.host {
+ return "", nil, errors.New("wrong host name")
+ }
+
+ return "LOGIN", []byte{}, nil
+}
+
+func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+ if more {
+ switch string(fromServer) {
+ case "Username:":
+ return []byte(a.username), nil
+ case "Password:":
+ return []byte(a.password), nil
+ default:
+ return nil, errors.New("Unkown fromServer")
+ }
+ }
+ return nil, nil
+}
+
func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) {
var conn net.Conn
var err error
@@ -75,9 +125,7 @@ func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.Ap
}
if *config.EmailSettings.EnableSMTPAuth {
- auth := smtp.PlainAuth("", config.EmailSettings.SMTPUsername, config.EmailSettings.SMTPPassword, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort)
-
- if err = c.Auth(auth); err != nil {
+ if err = c.Auth(&authChooser{Config: config}); err != nil {
return nil, model.NewAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
diff --git a/utils/mail_test.go b/utils/mail_test.go
index 67d108d45..31a4f8996 100644
--- a/utils/mail_test.go
+++ b/utils/mail_test.go
@@ -7,12 +7,9 @@ import (
"strings"
"testing"
- "net/mail"
-
- "fmt"
+ "net/smtp"
"github.com/mattermost/mattermost-server/model"
- "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -82,7 +79,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")
@@ -174,4 +171,65 @@ func TestSendMailUsingConfigAdvanced(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{Config: &config}
+ tests := []struct {
+ desc string
+ server *smtp.ServerInfo
+ err string
+ }{
+ {
+ desc: "auth PLAIN success",
+ server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"PLAIN"}, TLS: true},
+ },
+ {
+ desc: "auth PLAIN unencrypted connection fail",
+ server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"PLAIN"}, TLS: false},
+ err: "unencrypted connection",
+ },
+ {
+ desc: "auth PLAIN wrong host name",
+ server: &smtp.ServerInfo{Name: "wrongServer:999", Auth: []string{"PLAIN"}, TLS: true},
+ err: "wrong host name",
+ },
+ {
+ desc: "auth LOGIN success",
+ server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"LOGIN"}, TLS: true},
+ },
+ {
+ desc: "auth LOGIN unencrypted connection fail",
+ server: &smtp.ServerInfo{Name: "wrongServer:999", Auth: []string{"LOGIN"}, TLS: true},
+ err: "wrong host name",
+ },
+ {
+ desc: "auth LOGIN wrong host name",
+ server: &smtp.ServerInfo{Name: "fakeserver:25", Auth: []string{"LOGIN"}, TLS: false},
+ err: "unencrypted connection",
+ },
+ }
+
+ for i, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ _, _, err := auth.Start(test.server)
+ got := ""
+ if err != nil {
+ got = err.Error()
+ }
+ if got != test.err {
+ t.Errorf("%d. got error = %q; want %q", i, got, test.err)
+ }
+ })
+ }
}
diff --git a/web/web_test.go b/web/web_test.go
index 39246c9e6..20c42245a 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -61,6 +61,7 @@ func Setup() *app.App {
a.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = true
+ *cfg.ServiceSettings.EnableAPIv3 = true
})
return a