diff options
-rw-r--r-- | api/admin.go | 2 | ||||
-rw-r--r-- | api/channel.go | 186 | ||||
-rw-r--r-- | api/command_join.go | 2 | ||||
-rw-r--r-- | api/command_logout.go | 4 | ||||
-rw-r--r-- | api/command_msg.go | 2 | ||||
-rw-r--r-- | api/context.go | 2 | ||||
-rw-r--r-- | api/emoji.go | 10 | ||||
-rw-r--r-- | api/emoji_test.go | 2 | ||||
-rw-r--r-- | api/file.go | 346 | ||||
-rw-r--r-- | api/file_test.go | 31 | ||||
-rw-r--r-- | api/oauth.go | 34 | ||||
-rw-r--r-- | api/post.go | 21 | ||||
-rw-r--r-- | api/team.go | 5 | ||||
-rw-r--r-- | api/user.go | 682 | ||||
-rw-r--r-- | api/user_test.go | 26 | ||||
-rw-r--r-- | api/webrtc.go | 19 | ||||
-rw-r--r-- | api/webrtc_test.go | 3 | ||||
-rw-r--r-- | app/audit.go | 16 | ||||
-rw-r--r-- | app/channel.go | 187 | ||||
-rw-r--r-- | app/file.go | 340 | ||||
-rw-r--r-- | app/file_test.go | 33 | ||||
-rw-r--r-- | app/notification.go | 394 | ||||
-rw-r--r-- | app/oauth.go | 34 | ||||
-rw-r--r-- | app/preference.go | 16 | ||||
-rw-r--r-- | app/session.go | 52 | ||||
-rw-r--r-- | app/team.go | 8 | ||||
-rw-r--r-- | app/user.go | 421 | ||||
-rw-r--r-- | app/user_test.go | 27 | ||||
-rw-r--r-- | app/webhook.go | 40 | ||||
-rw-r--r-- | app/webtrc.go | 33 | ||||
-rw-r--r-- | cmd/platform/oldcommands.go | 2 | ||||
-rw-r--r-- | cmd/platform/user.go | 2 | ||||
-rw-r--r-- | i18n/en.json | 12 |
33 files changed, 1634 insertions, 1360 deletions
diff --git a/api/admin.go b/api/admin.go index 2b5afc47a..300796bad 100644 --- a/api/admin.go +++ b/api/admin.go @@ -665,7 +665,7 @@ func adminResetMfa(c *Context, w http.ResponseWriter, r *http.Request) { return } - if err := DeactivateMfa(userId); err != nil { + if err := app.DeactivateMfa(userId); err != nil { c.Err = err return } diff --git a/api/channel.go b/api/channel.go index f0d520b4e..590409921 100644 --- a/api/channel.go +++ b/api/channel.go @@ -119,7 +119,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if sc, err := CreateDirectChannel(c.Session.UserId, userId); err != nil { + if sc, err := app.CreateDirectChannel(c.Session.UserId, userId); err != nil { c.Err = err return } else { @@ -127,33 +127,6 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) { } } -func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) { - uc := app.Srv.Store.User().Get(otherUserId) - - if uresult := <-uc; uresult.Err != nil { - return nil, model.NewLocAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, otherUserId) - } - - if result := <-app.Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil { - if result.Err.Id == store.CHANNEL_EXISTS_ERROR { - return result.Data.(*model.Channel), nil - } else { - return nil, result.Err - } - } else { - channel := result.Data.(*model.Channel) - - app.InvalidateCacheForUser(userId) - app.InvalidateCacheForUser(otherUserId) - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil) - message.Add("teammate_id", otherUserId) - go app.Publish(message) - - return channel, nil - } -} - func CanManageChannel(c *Context, channel *model.Channel) bool { if channel.Type == model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) { return false @@ -229,7 +202,9 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } else { if oldChannelDisplayName != channel.DisplayName { - go PostUpdateChannelDisplayNameMessage(c, channel.Id, oldChannelDisplayName, channel.DisplayName) + if err := app.PostUpdateChannelDisplayNameMessage(c.Session.UserId, channel.Id, c.TeamId, oldChannelDisplayName, channel.DisplayName); err != nil { + l4g.Error(err.Error()) + } } c.LogAudit("name=" + channel.Name) w.Write([]byte(oldChannel.ToJson())) @@ -277,76 +252,15 @@ func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = ucresult.Err return } else { - go PostUpdateChannelHeaderMessage(c, channel.Id, oldChannelHeader, channelHeader) + if err := app.PostUpdateChannelHeaderMessage(c.Session.UserId, channel.Id, c.TeamId, oldChannelHeader, channelHeader); err != nil { + l4g.Error(err.Error()) + } c.LogAudit("name=" + channel.Name) w.Write([]byte(channel.ToJson())) } } } -func PostUpdateChannelHeaderMessage(c *Context, channelId string, oldChannelHeader, newChannelHeader string) { - uc := app.Srv.Store.User().Get(c.Session.UserId) - - if uresult := <-uc; uresult.Err != nil { - l4g.Error(utils.T("api.channel.post_update_channel_header_message_and_forget.retrieve_user.error"), uresult.Err) - return - } else { - user := uresult.Data.(*model.User) - - var message string - if oldChannelHeader == "" { - message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader) - } else if newChannelHeader == "" { - message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader) - } else { - message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader) - } - - post := &model.Post{ - ChannelId: channelId, - Message: message, - Type: model.POST_HEADER_CHANGE, - UserId: c.Session.UserId, - Props: model.StringInterface{ - "old_header": oldChannelHeader, - "new_header": newChannelHeader, - }, - } - - if _, err := app.CreatePost(post, c.TeamId, false); err != nil { - l4g.Error(utils.T("api.channel.post_update_channel_header_message_and_forget.join_leave.error"), err) - } - } -} - -func PostUpdateChannelDisplayNameMessage(c *Context, channelId string, oldChannelDisplayName, newChannelDisplayName string) { - uc := app.Srv.Store.User().Get(c.Session.UserId) - - if uresult := <-uc; uresult.Err != nil { - l4g.Error(utils.T("api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error"), uresult.Err) - return - } else { - user := uresult.Data.(*model.User) - - message := fmt.Sprintf(utils.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName) - - post := &model.Post{ - ChannelId: channelId, - Message: message, - Type: model.POST_DISPLAYNAME_CHANGE, - UserId: c.Session.UserId, - Props: model.StringInterface{ - "old_displayname": oldChannelDisplayName, - "new_displayname": newChannelDisplayName, - }, - } - - if _, err := app.CreatePost(post, c.TeamId, false); err != nil { - l4g.Error(utils.T("api.channel.post_update_channel_displayname_message_and_forget.create_post.error"), err) - } - } -} - func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) channelId := props["channel_id"] @@ -473,84 +387,34 @@ func join(c *Context, w http.ResponseWriter, r *http.Request) { channelId := params["channel_id"] channelName := params["channel_name"] - var outChannel *model.Channel = nil + var channel *model.Channel + var err *model.AppError if channelId != "" { - if err, channel := JoinChannelById(c, c.Session.UserId, channelId); err != nil { - c.Err = err - c.Err.StatusCode = http.StatusForbidden - return - } else { - outChannel = channel - } + channel, err = app.GetChannel(channelId) } else if channelName != "" { - if err, channel := JoinChannelByName(c, c.Session.UserId, c.TeamId, channelName); err != nil { - c.Err = err - c.Err.StatusCode = http.StatusForbidden - return - } else { - outChannel = channel - } + channel, err = app.GetChannelByName(channelName, c.TeamId) } else { c.SetInvalidParam("join", "channel_id, channel_name") return } - w.Write([]byte(outChannel.ToJson())) -} - -func JoinChannelByName(c *Context, userId string, teamId string, channelName string) (*model.AppError, *model.Channel) { - channelChannel := app.Srv.Store.Channel().GetByName(teamId, channelName) - userChannel := app.Srv.Store.User().Get(userId) - - return joinChannel(c, channelChannel, userChannel) -} - -func JoinChannelById(c *Context, userId string, channelId string) (*model.AppError, *model.Channel) { - channelChannel := app.Srv.Store.Channel().Get(channelId, true) - userChannel := app.Srv.Store.User().Get(userId) - - return joinChannel(c, channelChannel, userChannel) -} - -func joinChannel(c *Context, channelChannel store.StoreChannel, userChannel store.StoreChannel) (*model.AppError, *model.Channel) { - if cresult := <-channelChannel; cresult.Err != nil { - return cresult.Err, nil - } else if uresult := <-userChannel; uresult.Err != nil { - return uresult.Err, nil - } else { - channel := cresult.Data.(*model.Channel) - user := uresult.Data.(*model.User) - if mresult := <-app.Srv.Store.Channel().GetMember(channel.Id, user.Id); mresult.Err == nil && mresult.Data != nil { - // the user is already in the channel so just return successful - return nil, channel - } + if err != nil { + c.Err = err + return + } + if channel.Type == model.CHANNEL_OPEN { if !HasPermissionToTeamContext(c, channel.TeamId, model.PERMISSION_JOIN_PUBLIC_CHANNELS) { - return c.Err, nil - } - - if channel.Type == model.CHANNEL_OPEN { - if _, err := app.AddUserToChannel(user, channel); err != nil { - return err, nil - } - go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE) - } else { - return model.NewLocAppError("join", "api.channel.join_channel.permissions.app_error", nil, ""), nil + return } - return nil, channel } -} -func PostUserAddRemoveMessage(c *Context, channelId string, message, postType string) { - post := &model.Post{ - ChannelId: channelId, - Message: message, - Type: postType, - UserId: c.Session.UserId, - } - if _, err := app.CreatePost(post, c.TeamId, false); err != nil { - l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) + if err = app.JoinChannel(channel, c.Session.UserId); err != nil { + c.Err = err + return } + + w.Write([]byte(channel.ToJson())) } func leave(c *Context, w http.ResponseWriter, r *http.Request) { @@ -601,7 +465,7 @@ func leave(c *Context, w http.ResponseWriter, r *http.Request) { RemoveUserFromChannel(c.Session.UserId, c.Session.UserId, channel) - go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), model.POST_JOIN_LEAVE) + go app.PostUserAddRemoveMessage(c.Session.UserId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), model.POST_JOIN_LEAVE) result := make(map[string]string) result["id"] = channel.Id @@ -902,7 +766,7 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("name=" + channel.Name + " user_id=" + userId) - go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username), model.POST_ADD_REMOVE) + go app.PostUserAddRemoveMessage(c.Session.UserId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username), model.POST_ADD_REMOVE) <-app.Srv.Store.Channel().UpdateLastViewedAt([]string{id}, oUser.Id) w.Write([]byte(cm.ToJson())) @@ -956,7 +820,7 @@ func removeMember(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("name=" + channel.Name + " user_id=" + userIdToRemove) - go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.remove_member.removed"), oUser.Username), model.POST_ADD_REMOVE) + go app.PostUserAddRemoveMessage(c.Session.UserId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.remove_member.removed"), oUser.Username), model.POST_ADD_REMOVE) result := make(map[string]string) result["channel_id"] = channel.Id diff --git a/api/command_join.go b/api/command_join.go index 2210d2857..bad176656 100644 --- a/api/command_join.go +++ b/api/command_join.go @@ -45,7 +45,7 @@ func (me *JoinProvider) DoCommand(c *Context, args *model.CommandArgs, message s return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } - if err, _ := JoinChannelById(c, c.Session.UserId, channel.Id); err != nil { + if err := app.JoinChannel(channel, c.Session.UserId); err != nil { return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } diff --git a/api/command_logout.go b/api/command_logout.go index 00375bb58..0eaa9a0ba 100644 --- a/api/command_logout.go +++ b/api/command_logout.go @@ -4,6 +4,7 @@ package api import ( + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -38,8 +39,7 @@ func (me *LogoutProvider) DoCommand(c *Context, args *model.CommandArgs, message // We can't actually remove the user's cookie from here so we just dump their session and let the browser figure it out if c.Session.Id != "" { - RevokeSessionById(c, c.Session.Id) - if c.Err != nil { + if err := app.RevokeSessionById(c.Session.Id); err != nil { return FAIL } return SUCCESS diff --git a/api/command_msg.go b/api/command_msg.go index 36a9344ea..86203c2cd 100644 --- a/api/command_msg.go +++ b/api/command_msg.go @@ -66,7 +66,7 @@ func (me *msgProvider) DoCommand(c *Context, args *model.CommandArgs, message st targetChannelId := "" if channel := <-app.Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil { if channel.Err.Id == "store.sql_channel.get_by_name.missing.app_error" { - if directChannel, err := CreateDirectChannel(c.Session.UserId, userProfile.Id); err != nil { + if directChannel, err := app.CreateDirectChannel(c.Session.UserId, userProfile.Id); err != nil { c.Err = err return &model.CommandResponse{Text: c.T("api.command_msg.dm_fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { diff --git a/api/context.go b/api/context.go index c2036ed81..349cbd5b6 100644 --- a/api/context.go +++ b/api/context.go @@ -469,7 +469,7 @@ func (c *Context) CheckTeamId() { return } } else { - // just return because it fail on the HasPermissionToContext and the error is already on the Context c.Err + // HasPermissionToContext automatically fills the Context error return } } diff --git a/api/emoji.go b/api/emoji.go index fd78d5bd6..fb511cd03 100644 --- a/api/emoji.go +++ b/api/emoji.go @@ -163,7 +163,7 @@ func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro if err := gif.EncodeAll(newbuf, resized_gif); err != nil { return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "") } - if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil { + if err := app.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil { return err } } @@ -175,13 +175,13 @@ func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro if err := png.Encode(newbuf, resized_image); err != nil { return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "") } - if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil { + if err := app.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil { return err } } } } else { - if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil { + if err := app.WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil { return err } } @@ -236,7 +236,7 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) { } func deleteEmojiImage(id string) { - if err := MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil { + if err := app.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil { l4g.Error("Failed to rename image when deleting emoji %v", id) } } @@ -275,7 +275,7 @@ func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) { } else { var img []byte - if data, err := ReadFile(getEmojiImagePath(id)); err != nil { + if data, err := app.ReadFile(getEmojiImagePath(id)); err != nil { c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, err.Error()) return } else { diff --git a/api/emoji_test.go b/api/emoji_test.go index 243703193..73440ba15 100644 --- a/api/emoji_test.go +++ b/api/emoji_test.go @@ -317,7 +317,7 @@ func createTestPng(t *testing.T, width int, height int) []byte { func createTestEmoji(t *testing.T, emoji *model.Emoji, imageData []byte) *model.Emoji { emoji = store.Must(app.Srv.Store.Emoji().Save(emoji)).(*model.Emoji) - if err := WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil { + if err := app.WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil { store.Must(app.Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix())) t.Fatalf("failed to write image: %v", err.Error()) } diff --git a/api/file.go b/api/file.go index 179c658cd..b0cedde40 100644 --- a/api/file.go +++ b/api/file.go @@ -5,24 +5,17 @@ package api import ( "bytes" - "crypto/sha256" - "encoding/base64" - "fmt" "image" "image/color" "image/draw" _ "image/gif" "image/jpeg" "io" - "io/ioutil" "net/http" "net/url" - "os" - "path" "path/filepath" "strconv" "strings" - "sync" l4g "github.com/alecthomas/log4go" "github.com/disintegration/imaging" @@ -32,8 +25,6 @@ import ( "github.com/mattermost/platform/utils" "github.com/rwcarlsen/goexif/exif" _ "golang.org/x/image/bmp" - - s3 "github.com/minio/minio-go" ) const ( @@ -182,7 +173,7 @@ func doUploadFile(teamId string, channelId string, userId string, rawFilename st info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg" } - if err := WriteFile(data, info.Path); err != nil { + if err := app.WriteFile(data, info.Path); err != nil { return nil, err } @@ -285,7 +276,7 @@ func generateThumbnailImage(img image.Image, thumbnailPath string, width int, he return } - if err := WriteFile(buf.Bytes(), thumbnailPath); err != nil { + if err := app.WriteFile(buf.Bytes(), thumbnailPath); err != nil { l4g.Error(utils.T("api.file.handle_images_forget.upload_thumb.error"), thumbnailPath, err) return } @@ -306,7 +297,7 @@ func generatePreviewImage(img image.Image, previewPath string, width int) { return } - if err := WriteFile(buf.Bytes(), previewPath); err != nil { + if err := app.WriteFile(buf.Bytes(), previewPath); err != nil { l4g.Error(utils.T("api.file.handle_images_forget.upload_preview.error"), previewPath, err) return } @@ -319,7 +310,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { return } - if data, err := ReadFile(info.Path); err != nil { + if data, err := app.ReadFile(info.Path); err != nil { c.Err = err c.Err.StatusCode = http.StatusNotFound } else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil { @@ -341,7 +332,7 @@ func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) { return } - if data, err := ReadFile(info.ThumbnailPath); err != nil { + if data, err := app.ReadFile(info.ThumbnailPath); err != nil { c.Err = err c.Err.StatusCode = http.StatusNotFound } else if err := writeFileResponse(info.Name, "", data, w, r); err != nil { @@ -363,7 +354,7 @@ func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) { return } - if data, err := ReadFile(info.PreviewPath); err != nil { + if data, err := app.ReadFile(info.PreviewPath); err != nil { c.Err = err c.Err.StatusCode = http.StatusNotFound } else if err := writeFileResponse(info.Name, "", data, w, r); err != nil { @@ -400,7 +391,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { hash := r.URL.Query().Get("h") if len(hash) > 0 { - correctHash := generatePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt) + correctHash := app.GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt) if hash != correctHash { c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "") @@ -413,7 +404,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { return } - if data, err := ReadFile(info.Path); err != nil { + if data, err := app.ReadFile(info.Path); err != nil { c.Err = err c.Err.StatusCode = http.StatusNotFound } else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil { @@ -482,7 +473,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) { hash := r.URL.Query().Get("h") if len(hash) > 0 { - correctHash := generatePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt) + correctHash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt) if hash != correctHash { c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "") @@ -511,7 +502,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) { return } - if data, err := ReadFile(info.Path); err != nil { + if data, err := app.ReadFile(info.Path); err != nil { c.Err = err c.Err.StatusCode = http.StatusNotFound } else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil { @@ -560,320 +551,5 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { return } - w.Write([]byte(model.StringToJson(generatePublicLink(c.GetSiteURL(), info)))) -} - -func generatePublicLink(siteURL string, info *model.FileInfo) string { - hash := generatePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt) - return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash) -} - -func generatePublicLinkHash(fileId, salt string) string { - hash := sha256.New() - hash.Write([]byte(salt)) - hash.Write([]byte(fileId)) - - return base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) -} - -var fileMigrationLock sync.Mutex - -// Creates and stores FileInfos for a post created before the FileInfos table existed. -func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { - if len(post.Filenames) == 0 { - l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.no_filenames.warn"), post.Id) - return []*model.FileInfo{} - } - - cchan := app.Srv.Store.Channel().Get(post.ChannelId, true) - - // There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those - filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames) - - var channel *model.Channel - if result := <-cchan; result.Err != nil { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.channel.app_error"), post.Id, post.ChannelId, result.Err) - return []*model.FileInfo{} - } else { - channel = result.Data.(*model.Channel) - } - - // Find the team that was used to make this post since its part of the file path that isn't saved in the Filename - var teamId string - if channel.TeamId == "" { - // This post was made in a cross-team DM channel so we need to find where its files were saved - teamId = findTeamIdForFilename(post, filenames[0]) - } else { - teamId = channel.TeamId - } - - // Create FileInfo objects for this post - infos := make([]*model.FileInfo, 0, len(filenames)) - if teamId == "" { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.team_id.error"), post.Id, filenames) - } else { - for _, filename := range filenames { - info := getInfoForFilename(post, teamId, filename) - if info == nil { - continue - } - - infos = append(infos, info) - } - } - - // Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created - fileMigrationLock.Lock() - defer fileMigrationLock.Unlock() - - if result := <-app.Srv.Store.Post().Get(post.Id); result.Err != nil { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_again.app_error"), post.Id, result.Err) - return []*model.FileInfo{} - } else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) { - // Another thread has already created FileInfos for this post, so just return those - if result := <-app.Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_file_infos_again.app_error"), post.Id, result.Err) - return []*model.FileInfo{} - } else { - l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.not_migrating_post.debug"), post.Id) - return result.Data.([]*model.FileInfo) - } - } - - l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.migrating_post.debug"), post.Id) - - savedInfos := make([]*model.FileInfo, 0, len(infos)) - fileIds := make([]string, 0, len(filenames)) - for _, info := range infos { - if result := <-app.Srv.Store.FileInfo().Save(info); result.Err != nil { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_file_info.app_error"), post.Id, info.Id, info.Path, result.Err) - continue - } - - savedInfos = append(savedInfos, info) - fileIds = append(fileIds, info.Id) - } - - // Copy and save the updated post - newPost := &model.Post{} - *newPost = *post - - newPost.Filenames = []string{} - newPost.FileIds = fileIds - - // Update Posts to clear Filenames and set FileIds - if result := <-app.Srv.Store.Post().Update(newPost, post); result.Err != nil { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_post.app_error"), post.Id, newPost.FileIds, post.Filenames, result.Err) - return []*model.FileInfo{} - } else { - return savedInfos - } -} - -func findTeamIdForFilename(post *model.Post, filename string) string { - split := strings.SplitN(filename, "/", 5) - id := split[3] - name, _ := url.QueryUnescape(split[4]) - - // This post is in a direct channel so we need to figure out what team the files are stored under. - if result := <-app.Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.teams.app_error"), post.Id, result.Err) - } else if teams := result.Data.([]*model.Team); len(teams) == 1 { - // The user has only one team so the post must've been sent from it - return teams[0].Id - } else { - for _, team := range teams { - path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name) - if _, err := ReadFile(path); err == nil { - // Found the team that this file was posted from - return team.Id - } - } - } - - return "" -} - -func getInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo { - // Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension} - split := strings.SplitN(filename, "/", 5) - if len(split) < 5 { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.unexpected_filename.error"), post.Id, filename) - return nil - } - - channelId := split[1] - userId := split[2] - oldId := split[3] - name, _ := url.QueryUnescape(split[4]) - - if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") { - l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.mismatched_filename.warn"), post.Id, post.ChannelId, post.UserId, filename) - } - - pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId) - path := pathPrefix + name - - // Open the file and populate the fields of the FileInfo - var info *model.FileInfo - if data, err := ReadFile(path); err != nil { - l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.file_not_found.error"), post.Id, filename, path, err) - return nil - } else { - var err *model.AppError - info, err = model.GetInfoForBytes(name, data) - if err != nil { - l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.info.app_error"), post.Id, filename, err) - } - } - - // Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file - info.Id = model.NewId() - info.CreatorId = post.UserId - info.PostId = post.Id - info.CreateAt = post.CreateAt - info.UpdateAt = post.UpdateAt - info.Path = path - - if info.IsImage() { - nameWithoutExtension := name[:strings.LastIndex(name, ".")] - info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg" - info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg" - } - - return info -} - -func WriteFile(f []byte, path string) *model.AppError { - if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { - endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint - accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId - secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey - secure := *utils.Cfg.FileSettings.AmazonS3SSL - s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure) - if err != nil { - return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error()) - } - bucket := utils.Cfg.FileSettings.AmazonS3Bucket - ext := filepath.Ext(path) - - if model.IsFileExtImage(ext) { - _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), model.GetImageMimeType(ext)) - } else { - _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), "binary/octet-stream") - } - if err != nil { - return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error()) - } - } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { - if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil { - return err - } - } else { - return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "") - } - - return nil -} - -func MoveFile(oldPath, newPath string) *model.AppError { - if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { - endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint - accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId - secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey - secure := *utils.Cfg.FileSettings.AmazonS3SSL - s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure) - if err != nil { - return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error()) - } - bucket := utils.Cfg.FileSettings.AmazonS3Bucket - - var copyConds = s3.NewCopyConditions() - if err = s3Clnt.CopyObject(bucket, newPath, "/"+path.Join(bucket, oldPath), copyConds); err != nil { - return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error()) - } - if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil { - return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error()) - } - } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { - if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+newPath), 0774); err != nil { - return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error()) - } - - if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil { - return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error()) - } - } else { - return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "") - } - - return nil -} - -func writeFileLocally(f []byte, path string) *model.AppError { - if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil { - directory, _ := filepath.Abs(filepath.Dir(path)) - return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error()) - } - - if err := ioutil.WriteFile(path, f, 0644); err != nil { - return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error()) - } - - return nil -} - -func ReadFile(path string) ([]byte, *model.AppError) { - if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { - endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint - accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId - secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey - secure := *utils.Cfg.FileSettings.AmazonS3SSL - s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure) - if err != nil { - return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error()) - } - bucket := utils.Cfg.FileSettings.AmazonS3Bucket - minioObject, err := s3Clnt.GetObject(bucket, path) - defer minioObject.Close() - if err != nil { - return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error()) - } - if f, err := ioutil.ReadAll(minioObject); err != nil { - return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error()) - } else { - return f, nil - } - } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { - if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil { - return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error()) - } else { - return f, nil - } - } else { - return nil, model.NewLocAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "") - } -} - -func openFileWriteStream(path string) (io.Writer, *model.AppError) { - if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { - return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.s3.app_error", nil, "") - } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { - if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil { - return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.creating_dir.app_error", nil, err.Error()) - } - - if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil { - return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.local_server.app_error", nil, err.Error()) - } else { - fileHandle.Chmod(0644) - return fileHandle, nil - } - } - - return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "") -} - -func closeFileWriteStream(file io.Writer) { - file.(*os.File).Close() + w.Write([]byte(model.StringToJson(app.GeneratePublicLink(c.GetSiteURL(), info)))) } diff --git a/api/file_test.go b/api/file_test.go index 56e604fea..ce3e1fab4 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -508,7 +508,7 @@ func TestGetPublicFileOld(t *testing.T) { } func generatePublicLinkOld(siteURL, teamId, channelId, userId, filename string) string { - hash := generatePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt) + hash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt) return fmt.Sprintf("%s%s/public/files/get/%s/%s/%s/%s?h=%s", siteURL, model.API_URL_SUFFIX, teamId, channelId, userId, filename, hash) } @@ -581,29 +581,6 @@ func TestGetPublicLink(t *testing.T) { } } -func TestGeneratePublicLinkHash(t *testing.T) { - filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt" - filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt" - salt1 := model.NewRandomString(32) - salt2 := model.NewRandomString(32) - - hash1 := generatePublicLinkHash(filename1, salt1) - hash2 := generatePublicLinkHash(filename2, salt1) - hash3 := generatePublicLinkHash(filename1, salt2) - - if hash1 != generatePublicLinkHash(filename1, salt1) { - t.Fatal("hash should be equal for the same file name and salt") - } - - if hash1 == hash2 { - t.Fatal("hashes for different files should not be equal") - } - - if hash1 == hash3 { - t.Fatal("hashes for the same file with different salts should not be equal") - } -} - func TestMigrateFilenamesToFileInfos(t *testing.T) { th := Setup().InitBasic() @@ -740,7 +717,7 @@ func TestFindTeamIdForFilename(t *testing.T) { Filenames: []string{fmt.Sprintf("/%s/%s/%s/%s", channel1.Id, user1.Id, fileId1, "test.png")}, })).(*model.Post) - if teamId := findTeamIdForFilename(post1, post1.Filenames[0]); teamId != team1.Id { + if teamId := app.FindTeamIdForFilename(post1, post1.Filenames[0]); teamId != team1.Id { t.Fatal("file should've been found under team1") } @@ -753,7 +730,7 @@ func TestFindTeamIdForFilename(t *testing.T) { })).(*model.Post) Client.SetTeamId(team1.Id) - if teamId := findTeamIdForFilename(post2, post2.Filenames[0]); teamId != team2.Id { + if teamId := app.FindTeamIdForFilename(post2, post2.Filenames[0]); teamId != team2.Id { t.Fatal("file should've been found under team2") } } @@ -795,7 +772,7 @@ func TestGetInfoForFilename(t *testing.T) { Filenames: []string{fmt.Sprintf("/%s/%s/%s/%s", channel1.Id, user1.Id, fileId1, "test.png")}, })).(*model.Post) - if info := getInfoForFilename(post1, team1.Id, post1.Filenames[0]); info == nil { + if info := app.GetInfoForFilename(post1, team1.Id, post1.Filenames[0]); info == nil { t.Fatal("info shouldn't be nil") } else if info.Id == "" { t.Fatal("info.Id shouldn't be empty") diff --git a/api/oauth.go b/api/oauth.go index 80daf7415..b378eb2a3 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -250,32 +250,6 @@ func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) { } } -func RevokeAccessToken(token string) *model.AppError { - - session, _ := app.GetSession(token) - schan := app.Srv.Store.Session().Remove(token) - - if result := <-app.Srv.Store.OAuth().GetAccessData(token); result.Err != nil { - return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "") - } - - tchan := app.Srv.Store.OAuth().RemoveAccessData(token) - - if result := <-tchan; result.Err != nil { - return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "") - } - - if result := <-schan; result.Err != nil { - return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "") - } - - if session != nil { - app.RemoveAllSessionsForUserId(session.UserId) - } - - return nil -} - func GetAuthData(code string) *model.AuthData { if result := <-app.Srv.Store.OAuth().GetAuthData(code); result.Err != nil { l4g.Error(utils.T("api.oauth.get_auth_data.find.error"), code) @@ -311,7 +285,11 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { action := props["action"] switch action { case model.OAUTH_ACTION_SIGNUP: - CreateOAuthUser(c, w, r, service, body, teamId) + if user, err := app.CreateOAuthUser(service, body, teamId); err != nil { + c.Err = err + } else { + doLogin(c, w, r, user, "") + } if c.Err == nil { http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) } @@ -931,7 +909,7 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { accessData := result.Data.([]*model.AccessData) for _, a := range accessData { - if err := RevokeAccessToken(a.Token); err != nil { + if err := app.RevokeAccessToken(a.Token); err != nil { c.Err = err return } diff --git a/api/post.go b/api/post.go index 270ab72ca..bbdce78e8 100644 --- a/api/post.go +++ b/api/post.go @@ -362,13 +362,18 @@ func getPermalinkTmp(c *Context, w http.ResponseWriter, r *http.Request) { } post := list.Posts[list.Order[0]] - // Because we confuse permissions and membership in Mattermost's model, we have to just - // try to join the channel without checking if we already have permission to it. This is - // because system admins have permissions to every channel but are not nessisary a member - // of every channel. If we checked here then system admins would skip joining the channel and - // error when they tried to view it. - if err, _ := JoinChannelById(c, c.Session.UserId, post.ChannelId); err != nil { - // On error just return with permissions error + var channel *model.Channel + var err *model.AppError + if channel, err = app.GetChannel(post.ChannelId); err != nil { + c.Err = err + return + } + + if !HasPermissionToTeamContext(c, channel.TeamId, model.PERMISSION_JOIN_PUBLIC_CHANNELS) { + return + } + + if err = app.JoinChannel(channel, c.Session.UserId); err != nil { c.Err = err return } @@ -611,7 +616,7 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) { if len(post.Filenames) > 0 { // The post has Filenames that need to be replaced with FileInfos - infos = migrateFilenamesToFileInfos(post) + infos = app.MigrateFilenamesToFileInfos(post) } } diff --git a/api/team.go b/api/team.go index a8ed90eb5..6f907a513 100644 --- a/api/team.go +++ b/api/team.go @@ -248,7 +248,10 @@ func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("revoked_all=" + id) if session.IsOAuth { - RevokeAccessToken(session.Token) + if err := app.RevokeAccessToken(session.Token); err != nil { + c.Err = err + return + } } else { if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err diff --git a/api/user.go b/api/user.go index e3cae3704..701badfc9 100644 --- a/api/user.go +++ b/api/user.go @@ -7,16 +7,12 @@ import ( "bytes" b64 "encoding/base64" "fmt" - "hash/fnv" "html/template" "image" - "image/color" - "image/draw" _ "image/gif" _ "image/jpeg" "image/png" "io" - "io/ioutil" "net/http" "net/url" "strconv" @@ -25,7 +21,6 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/disintegration/imaging" - "github.com/golang/freetype" "github.com/gorilla/mux" "github.com/mattermost/platform/app" "github.com/mattermost/platform/einterfaces" @@ -102,94 +97,50 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - hash := r.URL.Query().Get("h") - teamId := "" - var team *model.Team - shouldSendWelcomeEmail := true user.EmailVerified = false - if len(hash) > 0 { - data := r.URL.Query().Get("d") - props := model.MapFromJson(strings.NewReader(data)) - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("createUser", "api.user.create_user.signup_link_invalid.app_error", nil, "") - return - } + shouldSendWelcomeEmail := true - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - c.Err = model.NewLocAppError("createUser", "api.user.create_user.signup_link_expired.app_error", nil, "") - return - } + hash := r.URL.Query().Get("h") + inviteId := r.URL.Query().Get("iid") - teamId = props["id"] + if !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) { + c.Err = model.NewLocAppError("createUser", "api.user.create_user.accepted_domain.app_error", nil, "") + return + } - // try to load the team to make sure it exists - if result := <-app.Srv.Store.Team().Get(teamId); result.Err != nil { - c.Err = result.Err + var ruser *model.User + var err *model.AppError + if len(hash) > 0 { + data := r.URL.Query().Get("d") + ruser, err = app.CreateUserWithHash(user, hash, data) + if err != nil { + c.Err = err return - } else { - team = result.Data.(*model.Team) } - user.Email = props["email"] - user.EmailVerified = true shouldSendWelcomeEmail = false - } - - inviteId := r.URL.Query().Get("iid") - if len(inviteId) > 0 { - if result := <-app.Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { - c.Err = result.Err + } else if len(inviteId) > 0 { + ruser, err = app.CreateUserWithInviteId(user, inviteId) + if err != nil { + c.Err = err return - } else { - team = result.Data.(*model.Team) - teamId = team.Id } - } - - firstAccount := false - if app.SessionCacheLength() == 0 { - if cr := <-app.Srv.Store.User().GetTotalUsersCount(); cr.Err != nil { - c.Err = cr.Err + } else { + if !app.IsFirstUserAccount() && !*utils.Cfg.TeamSettings.EnableOpenServer { + c.Err = model.NewLocAppError("createUser", "api.user.create_user.no_open_server", nil, "email="+user.Email) return - } else { - count := cr.Data.(int64) - if count <= 0 { - firstAccount = true - } } - } - - if !firstAccount && !*utils.Cfg.TeamSettings.EnableOpenServer && len(teamId) == 0 { - c.Err = model.NewLocAppError("createUser", "api.user.create_user.no_open_server", nil, "email="+user.Email) - return - } - - if !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) { - c.Err = model.NewLocAppError("createUser", "api.user.create_user.accepted_domain.app_error", nil, "") - return - } - - ruser, err := app.CreateUser(user) - if err != nil { - c.Err = err - return - } - if len(teamId) > 0 { - err := app.JoinUserToTeam(team, ruser) + ruser, err = app.CreateUser(user) if err != nil { c.Err = err return } - - go addDirectChannels(team.Id, ruser) } if shouldSendWelcomeEmail { - go sendWelcomeEmail(c, ruser.Id, ruser.Email, c.GetSiteURL(), ruser.EmailVerified) + sendWelcomeEmail(c, ruser.Id, ruser.Email, c.GetSiteURL(), ruser.EmailVerified) } w.Write([]byte(ruser.ToJson())) @@ -239,77 +190,6 @@ func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool return shouldVerifyHash } -func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader, teamId string) *model.User { - var user *model.User - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.not_available.app_error", map[string]interface{}{"Service": strings.Title(service)}, "") - return nil - } else { - user = provider.GetUserFromJson(userData) - } - - if user == nil { - c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]interface{}{"Service": service}, "") - return nil - } - - suchan := app.Srv.Store.User().GetByAuth(user.AuthData, service) - euchan := app.Srv.Store.User().GetByEmail(user.Email) - - found := true - count := 0 - for found { - if found = IsUsernameTaken(user.Username); found { - user.Username = user.Username + strconv.Itoa(count) - count += 1 - } - } - - if result := <-suchan; result.Err == nil { - c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_used.app_error", - map[string]interface{}{"Service": service}, "email="+user.Email) - return nil - } - - if result := <-euchan; result.Err == nil { - authService := result.Data.(*model.User).AuthService - if authService == "" { - c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", - map[string]interface{}{"Service": service, "Auth": model.USER_AUTH_SERVICE_EMAIL}, "email="+user.Email) - } else { - c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", - map[string]interface{}{"Service": service, "Auth": authService}, "email="+user.Email) - } - return nil - } - - user.EmailVerified = true - - ruser, err := app.CreateUser(user) - if err != nil { - c.Err = err - return nil - } - - if len(teamId) > 0 { - err = app.JoinUserToTeamById(teamId, user) - if err != nil { - c.Err = err - return nil - } - - go addDirectChannels(teamId, user) - } - - doLogin(c, w, r, ruser, "") - if c.Err != nil { - return nil - } - - return ruser -} - func sendWelcomeEmail(c *Context, userId string, email string, siteURL string, verified bool) { rawUrl, _ := url.Parse(siteURL) @@ -339,43 +219,6 @@ func sendWelcomeEmail(c *Context, userId string, email string, siteURL string, v } } -func addDirectChannels(teamId string, user *model.User) { - var profiles map[string]*model.User - if result := <-app.Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil { - l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error()) - return - } else { - profiles = result.Data.(map[string]*model.User) - } - - var preferences model.Preferences - - for id := range profiles { - if id == user.Id { - continue - } - - profile := profiles[id] - - preference := model.Preference{ - UserId: user.Id, - Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, - Name: profile.Id, - Value: "true", - } - - preferences = append(preferences, preference) - - if len(preferences) >= 10 { - break - } - } - - if result := <-app.Srv.Store.Preference().Save(&preferences); result.Err != nil { - l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error()) - } -} - func SendVerifyEmail(c *Context, userId, userEmail, siteURL string) { link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(userEmail)) @@ -418,21 +261,19 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { if len(id) != 0 { c.LogAuditWithUserId(id, "attempt") - if result := <-app.Srv.Store.User().Get(id); result.Err != nil { + if user, err = app.GetUser(id); err != nil { c.LogAuditWithUserId(id, "failure") - c.Err = result.Err + c.Err = err c.Err.StatusCode = http.StatusBadRequest if einterfaces.GetMetricsInterface() != nil { einterfaces.GetMetricsInterface().IncrementLoginFail() } return - } else { - user = result.Data.(*model.User) } } else { c.LogAudit("attempt") - if user, err = getUserForLogin(loginId, ldapOnly); err != nil { + if user, err = app.GetUserForLogin(loginId, ldapOnly); err != nil { c.LogAudit("failure") c.Err = err if einterfaces.GetMetricsInterface() != nil { @@ -469,37 +310,6 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(user.ToJson())) } -func getUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) { - ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP - - if result := <-app.Srv.Store.User().GetForLogin( - loginId, - *utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap, - *utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap, - ldapAvailable, - ); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" { - // don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work - result.Err.StatusCode = http.StatusBadRequest - return nil, result.Err - } else if result.Err != nil { - if !ldapAvailable { - // failed to find user and no LDAP server to fall back on - result.Err.StatusCode = http.StatusBadRequest - return nil, result.Err - } - - // fall back to LDAP server to see if we can find a user - if ldapUser, ldapErr := einterfaces.GetLdapInterface().GetUser(loginId); ldapErr != nil { - ldapErr.StatusCode = http.StatusBadRequest - return nil, ldapErr - } else { - return ldapUser, nil - } - } else { - return result.Data.(*model.User), nil - } -} - func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader) *model.User { buf := bytes.Buffer{} buf.ReadFrom(userData) @@ -521,20 +331,23 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st } var user *model.User - if result := <-app.Srv.Store.User().GetByAuth(&authData, service); result.Err != nil { - if result.Err.Id == store.MISSING_AUTH_ACCOUNT_ERROR { - return CreateOAuthUser(c, w, r, service, bytes.NewReader(buf.Bytes()), "") + var err *model.AppError + if user, err = app.GetUserByAuth(&authData, service); err != nil { + if err.Id == store.MISSING_AUTH_ACCOUNT_ERROR { + if user, err = app.CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), ""); err != nil { + c.Err = err + return nil + } } - c.Err = result.Err + c.Err = err return nil - } else { - user = result.Data.(*model.User) - doLogin(c, w, r, user, "") - if c.Err != nil { - return nil - } - return user } + + doLogin(c, w, r, user, "") + if c.Err != nil { + return nil + } + return user } // User MUST be authenticated completely before calling Login @@ -558,9 +371,8 @@ func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.Use for _, session := range sessions { if session.DeviceId == deviceId { l4g.Debug(utils.T("api.user.login.revoking.app_error"), session.Id, user.Id) - RevokeSessionById(c, session.Id) - if c.Err != nil { - c.LogError(c.Err) + if err := app.RevokeSessionById(session.Id); err != nil { + c.LogError(err) c.Err = nil } } @@ -634,7 +446,12 @@ func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.Use func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) id := props["id"] - RevokeSessionById(c, id) + + if err := app.RevokeSessionById(id); err != nil { + c.Err = err + return + } + w.Write([]byte(model.MapToJson(props))) } @@ -652,23 +469,11 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { return } - // A special case where we logout of all other sessions with the same Id - if result := <-app.Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil { - c.Err = result.Err + // A special case where we logout of all other sessions with the same device id + if err := app.RevokeSessionsForDeviceId(c.Session.UserId, deviceId, c.Session.Id); err != nil { + c.Err = err c.Err.StatusCode = http.StatusInternalServerError return - } else { - sessions := result.Data.([]*model.Session) - for _, session := range sessions { - if session.DeviceId == deviceId && session.Id != c.Session.Id { - l4g.Debug(utils.T("api.user.login.revoking.app_error"), session.Id, c.Session.UserId) - RevokeSessionById(c, session.Id) - if c.Err != nil { - c.LogError(c.Err) - c.Err = nil - } - } - } } app.RemoveAllSessionsForUserId(c.Session.UserId) @@ -694,34 +499,14 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { http.SetCookie(w, sessionCookie) - if result := <-app.Srv.Store.Session().UpdateDeviceId(c.Session.Id, deviceId, c.Session.ExpiresAt); result.Err != nil { - c.Err = result.Err + if err := app.AttachDeviceId(c.Session.Id, deviceId, c.Session.ExpiresAt); err != nil { + c.Err = err return } w.Write([]byte(model.MapToJson(props))) } -func RevokeSessionById(c *Context, sessionId string) { - if result := <-app.Srv.Store.Session().Get(sessionId); result.Err != nil { - c.Err = result.Err - } else { - session := result.Data.(*model.Session) - c.LogAudit("session_id=" + session.Id) - - if session.IsOAuth { - RevokeAccessToken(session.Token) - } else { - if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil { - c.Err = result.Err - } - } - - RevokeWebrtcToken(session.Id) - app.RemoveAllSessionsForUserId(session.UserId) - } -} - // IF YOU UPDATE THIS PLEASE UPDATE BELOW func RevokeAllSession(c *Context, userId string) { if result := <-app.Srv.Store.Session().GetSessions(userId); result.Err != nil { @@ -733,7 +518,7 @@ func RevokeAllSession(c *Context, userId string) { for _, session := range sessions { c.LogAuditWithUserId(userId, "session_id="+session.Id) if session.IsOAuth { - RevokeAccessToken(session.Token) + app.RevokeAccessToken(session.Token) } else { if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err @@ -741,7 +526,7 @@ func RevokeAllSession(c *Context, userId string) { } } - RevokeWebrtcToken(session.Id) + app.RevokeWebrtcToken(session.Id) } } @@ -758,14 +543,14 @@ func RevokeAllSessionsNoContext(userId string) *model.AppError { for _, session := range sessions { if session.IsOAuth { - RevokeAccessToken(session.Token) + app.RevokeAccessToken(session.Token) } else { if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil { return result.Err } } - RevokeWebrtcToken(session.Id) + app.RevokeWebrtcToken(session.Id) } } @@ -810,23 +595,26 @@ func Logout(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("") c.RemoveSessionCookie(w, r) if c.Session.Id != "" { - RevokeSessionById(c, c.Session.Id) + if err := app.RevokeSessionById(c.Session.Id); err != nil { + c.Err = err + return + } } } func getMe(c *Context, w http.ResponseWriter, r *http.Request) { - if result := <-app.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { - c.Err = result.Err + if user, err := app.GetUser(c.Session.UserId); err != nil { + c.Err = err c.RemoveSessionCookie(w, r) l4g.Error(utils.T("api.user.get_me.getting.error"), c.Session.UserId) return - } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get Me", w, r) { + } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get Me", w, r) { return } else { - result.Data.(*model.User).Sanitize(map[string]bool{}) - w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)) - w.Write([]byte(result.Data.(*model.User).ToJson())) + user.Sanitize(map[string]bool{}) + w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)) + w.Write([]byte(user.ToJson())) return } } @@ -835,59 +623,36 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) { il := model.InitialLoad{} - var cchan store.StoreChannel - - if app.SessionCacheLength() == 0 { - // Below is a special case when intializating a new server - // Lets check to make sure the server is really empty - - cchan = app.Srv.Store.User().GetTotalUsersCount() - } - if len(c.Session.UserId) != 0 { - uchan := app.Srv.Store.User().Get(c.Session.UserId) - pchan := app.Srv.Store.Preference().GetAll(c.Session.UserId) - tchan := app.Srv.Store.Team().GetTeamsByUserId(c.Session.UserId) + var err *model.AppError - il.TeamMembers = c.Session.TeamMembers - - if ru := <-uchan; ru.Err != nil { - c.Err = ru.Err + il.User, err = app.GetUser(c.Session.UserId) + if err != nil { + c.Err = err return - } else { - il.User = ru.Data.(*model.User) - il.User.Sanitize(map[string]bool{}) } + il.User.Sanitize(map[string]bool{}) - if rp := <-pchan; rp.Err != nil { - c.Err = rp.Err - return - } else { - il.Preferences = rp.Data.(model.Preferences) - } + il.Preferences, err = app.GetPreferencesForUser(c.Session.Id) - if rt := <-tchan; rt.Err != nil { - c.Err = rt.Err + il.Teams, err = app.GetTeamsForUser(c.Session.UserId) + if err != nil { + c.Err = err return - } else { - il.Teams = rt.Data.([]*model.Team) + } - for _, team := range il.Teams { - team.Sanitize() - } + for _, team := range il.Teams { + team.Sanitize() } + + il.TeamMembers = c.Session.TeamMembers } - if cchan != nil { - if cr := <-cchan; cr.Err != nil { - c.Err = cr.Err - return - } else { - count := cr.Data.(int64) - if count <= 0 { - il.NoAccounts = true - } - } + if app.SessionCacheLength() == 0 { + // Below is a special case when intializating a new server + // Lets check to make sure the server is really empty + + il.NoAccounts = app.IsFirstUserAccount() } il.ClientCfg = utils.ClientCfg @@ -905,16 +670,19 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["user_id"] - if result := <-app.Srv.Store.User().Get(id); result.Err != nil { - c.Err = result.Err + var user *model.User + var err *model.AppError + + if user, err = app.GetUser(id); err != nil { + c.Err = err return - } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get User", w, r) { + } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get User", w, r) { return } else { - user := sanitizeProfile(c, result.Data.(*model.User)) + sanitizeProfile(c, user) w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)) - w.Write([]byte(result.Data.(*model.User).ToJson())) + w.Write([]byte(user.ToJson())) return } } @@ -923,16 +691,19 @@ func getByUsername(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) username := params["username"] - if result := <-app.Srv.Store.User().GetByUsername(username); result.Err != nil { - c.Err = result.Err + var user *model.User + var err *model.AppError + + if user, err = app.GetUserByUsername(username); err != nil { + c.Err = err return - } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Username", w, r) { + } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Username", w, r) { return } else { - user := sanitizeProfile(c, result.Data.(*model.User)) + sanitizeProfile(c, user) w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)) - w.Write([]byte(result.Data.(*model.User).ToJson())) + w.Write([]byte(user.ToJson())) return } } @@ -941,16 +712,19 @@ func getByEmail(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) email := params["email"] - if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil { - c.Err = result.Err + var user *model.User + var err *model.AppError + + if user, err = app.GetUserByEmail(email); err != nil { + c.Err = err return - } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Email", w, r) { + } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Email", w, r) { return } else { - user := sanitizeProfile(c, result.Data.(*model.User)) + sanitizeProfile(c, user) w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)) - w.Write([]byte(result.Data.(*model.User).ToJson())) + w.Write([]byte(user.ToJson())) return } } @@ -970,17 +744,18 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { return } - etag := (<-app.Srv.Store.User().GetEtagForAllProfiles()).Data.(string) + etag := app.GetUsersEtag() if HandleEtag(etag, "Get Profiles", w, r) { return } - if result := <-app.Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil { - c.Err = result.Err + var profiles map[string]*model.User + var profileErr *model.AppError + + if profiles, profileErr = app.GetUsers(offset, limit); profileErr != nil { + c.Err = profileErr return } else { - profiles := result.Data.(map[string]*model.User) - for k, p := range profiles { profiles[k] = sanitizeProfile(c, p) } @@ -1012,17 +787,18 @@ func getProfilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - etag := (<-app.Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string) + etag := app.GetUsersInTeamEtag(teamId) if HandleEtag(etag, "Get Profiles In Team", w, r) { return } - if result := <-app.Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil { - c.Err = result.Err + var profiles map[string]*model.User + var profileErr *model.AppError + + if profiles, profileErr = app.GetUsersInTeam(teamId, offset, limit); profileErr != nil { + c.Err = profileErr return } else { - profiles := result.Data.(map[string]*model.User) - for k, p := range profiles { profiles[k] = sanitizeProfile(c, p) } @@ -1058,12 +834,13 @@ func getProfilesInChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-app.Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil { - c.Err = result.Err + var profiles map[string]*model.User + var profileErr *model.AppError + + if profiles, err = app.GetUsersInChannel(channelId, offset, limit); profileErr != nil { + c.Err = profileErr return } else { - profiles := result.Data.(map[string]*model.User) - for k, p := range profiles { profiles[k] = sanitizeProfile(c, p) } @@ -1098,12 +875,13 @@ func getProfilesNotInChannel(c *Context, w http.ResponseWriter, r *http.Request) return } - if result := <-app.Srv.Store.User().GetProfilesNotInChannel(c.TeamId, channelId, offset, limit); result.Err != nil { - c.Err = result.Err + var profiles map[string]*model.User + var profileErr *model.AppError + + if profiles, err = app.GetUsersNotInChannel(c.TeamId, channelId, offset, limit); profileErr != nil { + c.Err = profileErr return } else { - profiles := result.Data.(map[string]*model.User) - for k, p := range profiles { profiles[k] = sanitizeProfile(c, p) } @@ -1120,18 +898,10 @@ func getAudits(c *Context, w http.ResponseWriter, r *http.Request) { return } - userChan := app.Srv.Store.User().Get(id) - auditChan := app.Srv.Store.Audit().Get(id, 20) - - if c.Err = (<-userChan).Err; c.Err != nil { - return - } - - if result := <-auditChan; result.Err != nil { - c.Err = result.Err + if audits, err := app.GetAudits(id, 20); err != nil { + c.Err = err return } else { - audits := result.Data.(model.Audits) etag := audits.Etag() if HandleEtag(etag, "Get Audits", w, r) { @@ -1147,128 +917,29 @@ func getAudits(c *Context, w http.ResponseWriter, r *http.Request) { } } -func createProfileImage(username string, userId string) ([]byte, *model.AppError) { - colors := []color.NRGBA{ - {197, 8, 126, 255}, - {227, 207, 18, 255}, - {28, 181, 105, 255}, - {35, 188, 224, 255}, - {116, 49, 196, 255}, - {197, 8, 126, 255}, - {197, 19, 19, 255}, - {250, 134, 6, 255}, - {227, 207, 18, 255}, - {123, 201, 71, 255}, - {28, 181, 105, 255}, - {35, 188, 224, 255}, - {116, 49, 196, 255}, - {197, 8, 126, 255}, - {197, 19, 19, 255}, - {250, 134, 6, 255}, - {227, 207, 18, 255}, - {123, 201, 71, 255}, - {28, 181, 105, 255}, - {35, 188, 224, 255}, - {116, 49, 196, 255}, - {197, 8, 126, 255}, - {197, 19, 19, 255}, - {250, 134, 6, 255}, - {227, 207, 18, 255}, - {123, 201, 71, 255}, - } - - h := fnv.New32a() - h.Write([]byte(userId)) - seed := h.Sum32() - - initial := string(strings.ToUpper(username)[0]) - - fontBytes, err := ioutil.ReadFile(utils.FindDir("fonts") + utils.Cfg.FileSettings.InitialFont) - if err != nil { - return nil, model.NewLocAppError("createProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error()) - } - font, err := freetype.ParseFont(fontBytes) - if err != nil { - return nil, model.NewLocAppError("createProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error()) - } - - width := int(utils.Cfg.FileSettings.ProfileWidth) - height := int(utils.Cfg.FileSettings.ProfileHeight) - color := colors[int64(seed)%int64(len(colors))] - dstImg := image.NewRGBA(image.Rect(0, 0, width, height)) - srcImg := image.White - draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.ZP, draw.Src) - size := float64((width + height) / 4) - - c := freetype.NewContext() - c.SetFont(font) - c.SetFontSize(size) - c.SetClip(dstImg.Bounds()) - c.SetDst(dstImg) - c.SetSrc(srcImg) - - pt := freetype.Pt(width/6, height*2/3) - _, err = c.DrawString(initial, pt) - if err != nil { - return nil, model.NewLocAppError("createProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error()) - } - - buf := new(bytes.Buffer) - - if imgErr := png.Encode(buf, dstImg); imgErr != nil { - return nil, model.NewLocAppError("createProfileImage", "api.user.create_profile_image.encode.app_error", nil, imgErr.Error()) - } else { - return buf.Bytes(), nil - } -} - func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["user_id"] - readFailed := false var etag string - if result := <-app.Srv.Store.User().Get(id); result.Err != nil { - c.Err = result.Err + if user, err := app.GetUser(id); err != nil { + c.Err = err return } else { - var img []byte - etag = strconv.FormatInt(result.Data.(*model.User).LastPictureUpdate, 10) + etag = strconv.FormatInt(user.LastPictureUpdate, 10) if HandleEtag(etag, "Profile Image", w, r) { return } - if len(utils.Cfg.FileSettings.DriverName) == 0 { - var err *model.AppError - if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil { - c.Err = err - return - } - } else { - path := "users/" + id + "/profile.png" - - if data, err := ReadFile(path); err != nil { - readFailed = true - - if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil { - c.Err = err - return - } - - if result.Data.(*model.User).LastPictureUpdate == 0 { - if err := WriteFile(img, path); err != nil { - c.Err = err - return - } - } - - } else { - img = data - } + var img []byte + img, err = app.GetProfileImage(user) + if err != nil { + c.Err = err + return } - if c.Session.UserId == id || readFailed { + if c.Session.UserId == id { w.Header().Set("Cache-Control", "max-age=300, public") // 5 mins } else { w.Header().Set("Cache-Control", "max-age=86400, public") // 24 hrs @@ -1353,7 +1024,7 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { path := "users/" + c.Session.UserId + "/profile.png" - if err := WriteFile(buf.Bytes(), path); err != nil { + if err := app.WriteFile(buf.Bytes(), path); err != nil { c.Err = model.NewLocAppError("uploadProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "") return } @@ -2000,22 +1671,6 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) { } } -// Check if the username is already used by another user. Return false if the username is invalid. -func IsUsernameTaken(name string) bool { - - if !model.IsValidUsername(name) { - return false - } - - if result := <-app.Srv.Store.User().GetByUsername(name); result.Err != nil { - return false - } else { - return true - } - - return false -} - func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) @@ -2336,7 +1991,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { return } - if user, error := getUserForLogin(email, false); error != nil { + if user, error := app.GetUserForLogin(email, false); error != nil { c.Err = error return } else { @@ -2403,13 +2058,13 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") if activate { - if err := ActivateMfa(c.Session.UserId, token); err != nil { + if err := app.ActivateMfa(c.Session.UserId, token); err != nil { c.Err = err return } c.LogAudit("success - activated") } else { - if err := DeactivateMfa(c.Session.UserId); err != nil { + if err := app.DeactivateMfa(c.Session.UserId); err != nil { c.Err = err return } @@ -2432,47 +2087,6 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(rdata))) } -func ActivateMfa(userId, token string) *model.AppError { - mfaInterface := einterfaces.GetMfaInterface() - if mfaInterface == nil { - err := model.NewLocAppError("ActivateMfa", "api.user.update_mfa.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented - return err - } - - var user *model.User - if result := <-app.Srv.Store.User().Get(userId); result.Err != nil { - return result.Err - } else { - user = result.Data.(*model.User) - } - - if len(user.AuthService) > 0 && user.AuthService != model.USER_AUTH_SERVICE_LDAP { - return model.NewLocAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "") - } - - if err := mfaInterface.Activate(user, token); err != nil { - return err - } - - return nil -} - -func DeactivateMfa(userId string) *model.AppError { - mfaInterface := einterfaces.GetMfaInterface() - if mfaInterface == nil { - err := model.NewLocAppError("DeactivateMfa", "api.user.update_mfa.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented - return err - } - - if err := mfaInterface.Deactivate(userId); err != nil { - return err - } - - return nil -} - func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { rdata := map[string]string{} @@ -2592,7 +2206,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { case model.OAUTH_ACTION_SIGNUP: teamId := relayProps["team_id"] if len(teamId) > 0 { - go addDirectChannels(teamId, user) + go app.AddDirectChannels(teamId, user) } break case model.OAUTH_ACTION_EMAIL_TO_SSO: diff --git a/api/user_test.go b/api/user_test.go index fa7f28e6b..3163de078 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -107,25 +107,6 @@ func TestCheckUserDomain(t *testing.T) { } } -func TestIsUsernameTaken(t *testing.T) { - th := Setup().InitBasic() - user := th.BasicUser - taken := IsUsernameTaken(user.Username) - - if !taken { - t.Logf("the username '%v' should be taken", user.Username) - t.FailNow() - } - - newUsername := "randomUsername" - taken = IsUsernameTaken(newUsername) - - if taken { - t.Logf("the username '%v' should not be taken", newUsername) - t.FailNow() - } -} - func TestLogin(t *testing.T) { th := Setup() Client := th.CreateClient() @@ -227,7 +208,10 @@ func TestLogin(t *testing.T) { data := model.MapToJson(props) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) - ruser2, _ := Client.CreateUserFromSignup(&user2, data, hash) + ruser2, err := Client.CreateUserFromSignup(&user2, data, hash) + if err != nil { + t.Fatal(err) + } if _, err := Client.Login(ruser2.Data.(*model.User).Email, user2.Password); err != nil { t.Fatal("From verfied hash") @@ -686,7 +670,7 @@ func TestUserCreateImage(t *testing.T) { th := Setup() Client := th.CreateClient() - b, err := createProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba") + b, err := app.CreateProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba") if err != nil { t.Fatal(err) } diff --git a/api/webrtc.go b/api/webrtc.go index 7f30ffef9..15b7cfa4f 100644 --- a/api/webrtc.go +++ b/api/webrtc.go @@ -115,22 +115,3 @@ func closeBody(r *http.Response) { r.Body.Close() } } - -func RevokeWebrtcToken(sessionId string) { - token := base64.StdEncoding.EncodeToString([]byte(sessionId)) - data := make(map[string]string) - data["janus"] = "remove_token" - data["token"] = token - data["transaction"] = model.NewId() - data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret - - rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data))) - rq.Header.Set("Content-Type", "application/json") - - // we do not care about the response - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, - } - httpClient := &http.Client{Transport: tr} - httpClient.Do(rq) -} diff --git a/api/webrtc_test.go b/api/webrtc_test.go index 953333b09..21049d95d 100644 --- a/api/webrtc_test.go +++ b/api/webrtc_test.go @@ -5,6 +5,7 @@ package api import ( "fmt" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "testing" @@ -39,5 +40,5 @@ func TestWebrtcToken(t *testing.T) { fmt.Println("Turn Password", result["turn_password"]) } - RevokeWebrtcToken(sessionId) + app.RevokeWebrtcToken(sessionId) } diff --git a/app/audit.go b/app/audit.go new file mode 100644 index 000000000..6978e9bc2 --- /dev/null +++ b/app/audit.go @@ -0,0 +1,16 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" +) + +func GetAudits(userId string, limit int) (model.Audits, *model.AppError) { + if result := <-Srv.Store.Audit().Get(userId, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(model.Audits), nil + } +} diff --git a/app/channel.go b/app/channel.go index 1771c856b..9451ca974 100644 --- a/app/channel.go +++ b/app/channel.go @@ -163,6 +163,33 @@ func CreateChannel(channel *model.Channel, addMember bool) (*model.Channel, *mod } } +func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) { + uc := Srv.Store.User().Get(otherUserId) + + if uresult := <-uc; uresult.Err != nil { + return nil, model.NewLocAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, otherUserId) + } + + if result := <-Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil { + if result.Err.Id == store.CHANNEL_EXISTS_ERROR { + return result.Data.(*model.Channel), nil + } else { + return nil, result.Err + } + } else { + channel := result.Data.(*model.Channel) + + InvalidateCacheForUser(userId) + InvalidateCacheForUser(otherUserId) + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil) + message.Add("teammate_id", otherUserId) + Publish(message) + + return channel, nil + } +} + func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) { if channel.DeleteAt > 0 { return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.deleted.app_error", nil, "") @@ -210,7 +237,165 @@ func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelM message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil) message.Add("user_id", user.Id) message.Add("team_id", channel.TeamId) - go Publish(message) + Publish(message) return newMember, nil } + +func AddDirectChannels(teamId string, user *model.User) *model.AppError { + var profiles map[string]*model.User + if result := <-Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil { + return model.NewLocAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]interface{}{"UserId": user.Id, "TeamId": teamId, "Error": result.Err.Error()}, "") + } else { + profiles = result.Data.(map[string]*model.User) + } + + var preferences model.Preferences + + for id := range profiles { + if id == user.Id { + continue + } + + profile := profiles[id] + + preference := model.Preference{ + UserId: user.Id, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Name: profile.Id, + Value: "true", + } + + preferences = append(preferences, preference) + + if len(preferences) >= 10 { + break + } + } + + if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { + return model.NewLocAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]interface{}{"UserId": user.Id, "TeamId": teamId, "Error": result.Err.Error()}, "") + } + + return nil +} + +func PostUpdateChannelHeaderMessage(userId string, channelId string, teamId string, oldChannelHeader, newChannelHeader string) *model.AppError { + uc := Srv.Store.User().Get(userId) + + if uresult := <-uc; uresult.Err != nil { + return model.NewLocAppError("PostUpdateChannelHeaderMessage", "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", nil, uresult.Err.Error()) + } else { + user := uresult.Data.(*model.User) + + var message string + if oldChannelHeader == "" { + message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader) + } else if newChannelHeader == "" { + message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader) + } else { + message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader) + } + + post := &model.Post{ + ChannelId: channelId, + Message: message, + Type: model.POST_HEADER_CHANGE, + UserId: userId, + Props: model.StringInterface{ + "old_header": oldChannelHeader, + "new_header": newChannelHeader, + }, + } + + if _, err := CreatePost(post, teamId, false); err != nil { + return model.NewLocAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, err.Error()) + } + } + + return nil +} + +func PostUpdateChannelDisplayNameMessage(userId string, channelId string, teamId string, oldChannelDisplayName, newChannelDisplayName string) *model.AppError { + uc := Srv.Store.User().Get(userId) + + if uresult := <-uc; uresult.Err != nil { + return model.NewLocAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", nil, uresult.Err.Error()) + } else { + user := uresult.Data.(*model.User) + + message := fmt.Sprintf(utils.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName) + + post := &model.Post{ + ChannelId: channelId, + Message: message, + Type: model.POST_DISPLAYNAME_CHANGE, + UserId: userId, + Props: model.StringInterface{ + "old_displayname": oldChannelDisplayName, + "new_displayname": newChannelDisplayName, + }, + } + + if _, err := CreatePost(post, teamId, false); err != nil { + return model.NewLocAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, err.Error()) + } + } + + return nil +} + +func GetChannel(channelId string) (*model.Channel, *model.AppError) { + if result := <-Srv.Store.Channel().Get(channelId, true); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Channel), nil + } +} + +func GetChannelByName(channelName, teamId string) (*model.Channel, *model.AppError) { + if result := <-Srv.Store.Channel().GetByName(teamId, channelName); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Channel), nil + } +} + +func JoinChannel(channel *model.Channel, userId string) *model.AppError { + userChan := Srv.Store.User().Get(userId) + memberChan := Srv.Store.Channel().GetMember(channel.Id, userId) + + if uresult := <-userChan; uresult.Err != nil { + return uresult.Err + } else if mresult := <-memberChan; mresult.Err == nil && mresult.Data != nil { + // user is already in the channel + return nil + } else { + user := uresult.Data.(*model.User) + + if channel.Type == model.CHANNEL_OPEN { + if _, err := AddUserToChannel(user, channel); err != nil { + return err + } + PostUserAddRemoveMessage(userId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE) + } else { + return model.NewLocAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "") + } + } + + return nil +} + +func PostUserAddRemoveMessage(userId, channelId, teamId, message, postType string) *model.AppError { + post := &model.Post{ + ChannelId: channelId, + Message: message, + Type: postType, + UserId: userId, + } + if _, err := CreatePost(post, teamId, false); err != nil { + return model.NewLocAppError("PostUserAddRemoveMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error()) + } + + return nil +} diff --git a/app/file.go b/app/file.go new file mode 100644 index 000000000..93a286a14 --- /dev/null +++ b/app/file.go @@ -0,0 +1,340 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "sync" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + + s3 "github.com/minio/minio-go" +) + +func ReadFile(path string) ([]byte, *model.AppError) { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { + endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint + accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId + secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey + secure := *utils.Cfg.FileSettings.AmazonS3SSL + s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure) + if err != nil { + return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error()) + } + bucket := utils.Cfg.FileSettings.AmazonS3Bucket + minioObject, err := s3Clnt.GetObject(bucket, path) + defer minioObject.Close() + if err != nil { + return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error()) + } + if f, err := ioutil.ReadAll(minioObject); err != nil { + return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error()) + } else { + return f, nil + } + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil { + return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error()) + } else { + return f, nil + } + } else { + return nil, model.NewLocAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "") + } +} + +func MoveFile(oldPath, newPath string) *model.AppError { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { + endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint + accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId + secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey + secure := *utils.Cfg.FileSettings.AmazonS3SSL + s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure) + if err != nil { + return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error()) + } + bucket := utils.Cfg.FileSettings.AmazonS3Bucket + + var copyConds = s3.NewCopyConditions() + if err = s3Clnt.CopyObject(bucket, newPath, "/"+path.Join(bucket, oldPath), copyConds); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error()) + } + if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error()) + } + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+newPath), 0774); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error()) + } + + if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error()) + } + } else { + return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "") + } + + return nil +} + +func WriteFile(f []byte, path string) *model.AppError { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { + endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint + accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId + secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey + secure := *utils.Cfg.FileSettings.AmazonS3SSL + s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure) + if err != nil { + return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error()) + } + bucket := utils.Cfg.FileSettings.AmazonS3Bucket + ext := filepath.Ext(path) + + if model.IsFileExtImage(ext) { + _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), model.GetImageMimeType(ext)) + } else { + _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), "binary/octet-stream") + } + if err != nil { + return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error()) + } + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil { + return err + } + } else { + return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "") + } + + return nil +} + +func writeFileLocally(f []byte, path string) *model.AppError { + if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil { + directory, _ := filepath.Abs(filepath.Dir(path)) + return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error()) + } + + if err := ioutil.WriteFile(path, f, 0644); err != nil { + return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error()) + } + + return nil +} + +func openFileWriteStream(path string) (io.Writer, *model.AppError) { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { + return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.s3.app_error", nil, "") + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil { + return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.creating_dir.app_error", nil, err.Error()) + } + + if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil { + return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.local_server.app_error", nil, err.Error()) + } else { + fileHandle.Chmod(0644) + return fileHandle, nil + } + } + + return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "") +} + +func closeFileWriteStream(file io.Writer) { + file.(*os.File).Close() +} + +func GetInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo { + // Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension} + split := strings.SplitN(filename, "/", 5) + if len(split) < 5 { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.unexpected_filename.error"), post.Id, filename) + return nil + } + + channelId := split[1] + userId := split[2] + oldId := split[3] + name, _ := url.QueryUnescape(split[4]) + + if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") { + l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.mismatched_filename.warn"), post.Id, post.ChannelId, post.UserId, filename) + } + + pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId) + path := pathPrefix + name + + // Open the file and populate the fields of the FileInfo + var info *model.FileInfo + if data, err := ReadFile(path); err != nil { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.file_not_found.error"), post.Id, filename, path, err) + return nil + } else { + var err *model.AppError + info, err = model.GetInfoForBytes(name, data) + if err != nil { + l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.info.app_error"), post.Id, filename, err) + } + } + + // Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file + info.Id = model.NewId() + info.CreatorId = post.UserId + info.PostId = post.Id + info.CreateAt = post.CreateAt + info.UpdateAt = post.UpdateAt + info.Path = path + + if info.IsImage() { + nameWithoutExtension := name[:strings.LastIndex(name, ".")] + info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg" + info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg" + } + + return info +} + +func FindTeamIdForFilename(post *model.Post, filename string) string { + split := strings.SplitN(filename, "/", 5) + id := split[3] + name, _ := url.QueryUnescape(split[4]) + + // This post is in a direct channel so we need to figure out what team the files are stored under. + if result := <-Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.teams.app_error"), post.Id, result.Err) + } else if teams := result.Data.([]*model.Team); len(teams) == 1 { + // The user has only one team so the post must've been sent from it + return teams[0].Id + } else { + for _, team := range teams { + path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name) + if _, err := ReadFile(path); err == nil { + // Found the team that this file was posted from + return team.Id + } + } + } + + return "" +} + +var fileMigrationLock sync.Mutex + +// Creates and stores FileInfos for a post created before the FileInfos table existed. +func MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { + if len(post.Filenames) == 0 { + l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.no_filenames.warn"), post.Id) + return []*model.FileInfo{} + } + + cchan := Srv.Store.Channel().Get(post.ChannelId, true) + + // There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those + filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames) + + var channel *model.Channel + if result := <-cchan; result.Err != nil { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.channel.app_error"), post.Id, post.ChannelId, result.Err) + return []*model.FileInfo{} + } else { + channel = result.Data.(*model.Channel) + } + + // Find the team that was used to make this post since its part of the file path that isn't saved in the Filename + var teamId string + if channel.TeamId == "" { + // This post was made in a cross-team DM channel so we need to find where its files were saved + teamId = FindTeamIdForFilename(post, filenames[0]) + } else { + teamId = channel.TeamId + } + + // Create FileInfo objects for this post + infos := make([]*model.FileInfo, 0, len(filenames)) + if teamId == "" { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.team_id.error"), post.Id, filenames) + } else { + for _, filename := range filenames { + info := GetInfoForFilename(post, teamId, filename) + if info == nil { + continue + } + + infos = append(infos, info) + } + } + + // Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created + fileMigrationLock.Lock() + defer fileMigrationLock.Unlock() + + if result := <-Srv.Store.Post().Get(post.Id); result.Err != nil { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_again.app_error"), post.Id, result.Err) + return []*model.FileInfo{} + } else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) { + // Another thread has already created FileInfos for this post, so just return those + if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_file_infos_again.app_error"), post.Id, result.Err) + return []*model.FileInfo{} + } else { + l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.not_migrating_post.debug"), post.Id) + return result.Data.([]*model.FileInfo) + } + } + + l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.migrating_post.debug"), post.Id) + + savedInfos := make([]*model.FileInfo, 0, len(infos)) + fileIds := make([]string, 0, len(filenames)) + for _, info := range infos { + if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_file_info.app_error"), post.Id, info.Id, info.Path, result.Err) + continue + } + + savedInfos = append(savedInfos, info) + fileIds = append(fileIds, info.Id) + } + + // Copy and save the updated post + newPost := &model.Post{} + *newPost = *post + + newPost.Filenames = []string{} + newPost.FileIds = fileIds + + // Update Posts to clear Filenames and set FileIds + if result := <-Srv.Store.Post().Update(newPost, post); result.Err != nil { + l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_post.app_error"), post.Id, newPost.FileIds, post.Filenames, result.Err) + return []*model.FileInfo{} + } else { + return savedInfos + } +} + +func GeneratePublicLink(siteURL string, info *model.FileInfo) string { + hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt) + return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash) +} + +func GeneratePublicLinkHash(fileId, salt string) string { + hash := sha256.New() + hash.Write([]byte(salt)) + hash.Write([]byte(fileId)) + + return base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) +} diff --git a/app/file_test.go b/app/file_test.go new file mode 100644 index 000000000..9df03315e --- /dev/null +++ b/app/file_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/platform/model" +) + +func TestGeneratePublicLinkHash(t *testing.T) { + filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt" + filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt" + salt1 := model.NewRandomString(32) + salt2 := model.NewRandomString(32) + + hash1 := GeneratePublicLinkHash(filename1, salt1) + hash2 := GeneratePublicLinkHash(filename2, salt1) + hash3 := GeneratePublicLinkHash(filename1, salt2) + + if hash1 != GeneratePublicLinkHash(filename1, salt1) { + t.Fatal("hash should be equal for the same file name and salt") + } + + if hash1 == hash2 { + t.Fatal("hashes for different files should not be equal") + } + + if hash1 == hash3 { + t.Fatal("hashes for the same file with different salts should not be equal") + } +} diff --git a/app/notification.go b/app/notification.go index d5e3c7b13..fc1d44f06 100644 --- a/app/notification.go +++ b/app/notification.go @@ -25,192 +25,189 @@ import ( ) func SendNotifications(post *model.Post, team *model.Team, channel *model.Channel) ([]string, *model.AppError) { - mentionedUsersList := make([]string, 0) - var fchan store.StoreChannel - var senderUsername string + pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) + fchan := Srv.Store.FileInfo().GetForPost(post.Id) - if post.IsSystemMessage() { - senderUsername = utils.T("system.message.name") + var profileMap map[string]*model.User + if result := <-pchan; result.Err != nil { + return nil, result.Err } else { - pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) - fchan = Srv.Store.FileInfo().GetForPost(post.Id) + profileMap = result.Data.(map[string]*model.User) + } - var profileMap map[string]*model.User - if result := <-pchan; result.Err != nil { - return nil, result.Err + // If the user who made the post isn't in the channel, don't send a notification + if _, ok := profileMap[post.UserId]; !ok { + l4g.Debug(utils.T("api.post.send_notifications.user_id.debug"), post.Id, channel.Id, post.UserId) + return []string{}, nil + } + + mentionedUserIds := make(map[string]bool) + allActivityPushUserIds := []string{} + hereNotification := false + channelNotification := false + allNotification := false + updateMentionChans := []store.StoreChannel{} + + if channel.Type == model.CHANNEL_DIRECT { + var otherUserId string + if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { + otherUserId = userIds[1] } else { - profileMap = result.Data.(map[string]*model.User) + otherUserId = userIds[0] } - // If the user who made the post isn't in the channel don't send a notification - if _, ok := profileMap[post.UserId]; !ok { - l4g.Debug(utils.T("api.post.send_notifications.user_id.debug"), post.Id, channel.Id, post.UserId) - return []string{}, nil + mentionedUserIds[otherUserId] = true + if post.Props["from_webhook"] == "true" { + mentionedUserIds[post.UserId] = true } + } else { + keywords := GetMentionKeywordsInChannel(profileMap) - mentionedUserIds := make(map[string]bool) - allActivityPushUserIds := []string{} - hereNotification := false - channelNotification := false - allNotification := false - updateMentionChans := []store.StoreChannel{} + var potentialOtherMentions []string + mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords) - if channel.Type == model.CHANNEL_DIRECT { - var otherUserId string - if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { - otherUserId = userIds[1] + // get users that have comment thread mentions enabled + if len(post.RootId) > 0 { + if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { + return nil, result.Err } else { - otherUserId = userIds[0] - } - - mentionedUserIds[otherUserId] = true - if post.Props["from_webhook"] == "true" { - mentionedUserIds[post.UserId] = true - } - } else { - keywords := GetMentionKeywordsInChannel(profileMap) - - var potentialOtherMentions []string - mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords) + list := result.Data.(*model.PostList) - // get users that have comment thread mentions enabled - if len(post.RootId) > 0 { - if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { - return nil, result.Err - } else { - list := result.Data.(*model.PostList) - - for _, threadPost := range list.Posts { - if profile, ok := profileMap[threadPost.UserId]; ok { - if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { - mentionedUserIds[threadPost.UserId] = true - } - } + for _, threadPost := range list.Posts { + profile := profileMap[threadPost.UserId] + if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { + mentionedUserIds[threadPost.UserId] = true } } } + } - // prevent the user from mentioning themselves - if post.Props["from_webhook"] != "true" { - delete(mentionedUserIds, post.UserId) - } + // prevent the user from mentioning themselves + if post.Props["from_webhook"] != "true" { + delete(mentionedUserIds, post.UserId) + } - if len(potentialOtherMentions) > 0 { - if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { - outOfChannelMentions := result.Data.(map[string]*model.User) - go sendOutOfChannelMentions(post, team.Id, outOfChannelMentions) - } + if len(potentialOtherMentions) > 0 { + if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { + outOfChannelMentions := result.Data.(map[string]*model.User) + go sendOutOfChannelMentions(post, team.Id, outOfChannelMentions) } + } - // find which users in the channel are set up to always receive mobile notifications - for _, profile := range profileMap { - if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && - (post.UserId != profile.Id || post.Props["from_webhook"] == "true") { - allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) - } + // find which users in the channel are set up to always receive mobile notifications + for _, profile := range profileMap { + if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && + (post.UserId != profile.Id || post.Props["from_webhook"] == "true") && + !post.IsSystemMessage() { + allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) } } + } - mentionedUsersList = make([]string, 0, len(mentionedUserIds)) - for id := range mentionedUserIds { - mentionedUsersList = append(mentionedUsersList, id) - updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) - } + mentionedUsersList := make([]string, 0, len(mentionedUserIds)) + for id := range mentionedUserIds { + mentionedUsersList = append(mentionedUsersList, id) + updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) + } - var sender *model.User - senderName := make(map[string]string) - for _, id := range mentionedUsersList { - senderName[id] = "" - if profile, ok := profileMap[post.UserId]; ok { - if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { - senderName[id] = value.(string) + var sender *model.User + senderName := make(map[string]string) + for _, id := range mentionedUsersList { + senderName[id] = "" + if post.IsSystemMessage() { + senderName[id] = utils.T("system.message.name") + } else if profile, ok := profileMap[post.UserId]; ok { + if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { + senderName[id] = value.(string) + } else { + // Get the Display name preference from the receiver + if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil { + // Show default sender's name if user doesn't set display settings. + senderName[id] = profile.Username } else { - //Get the Display name preference from the receiver - if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil { - // Show default sender's name if user doesn't set display settings. - senderName[id] = profile.Username - } else { - senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value) - } + senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value) } - sender = profile } + sender = profile } + } - if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { - senderUsername = value.(string) - } else { - senderUsername = profileMap[post.UserId].Username - } - - if utils.Cfg.EmailSettings.SendEmailNotifications { - for _, id := range mentionedUsersList { - userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" + var senderUsername string + if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { + senderUsername = value.(string) + } else { + senderUsername = profileMap[post.UserId].Username + } - var status *model.Status - var err *model.AppError - if status, err = GetStatus(id); err != nil { - status = &model.Status{ - UserId: id, - Status: model.STATUS_OFFLINE, - Manual: false, - LastActivityAt: 0, - ActiveChannel: "", - } + if utils.Cfg.EmailSettings.SendEmailNotifications { + for _, id := range mentionedUsersList { + userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" + + var status *model.Status + var err *model.AppError + if status, err = GetStatus(id); err != nil { + status = &model.Status{ + UserId: id, + Status: model.STATUS_OFFLINE, + Manual: false, + LastActivityAt: 0, + ActiveChannel: "", } + } - if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 { - if err := sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender); err != nil { - l4g.Error(err.Error()) - } - } + if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 { + sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender) } } + } - T := utils.GetUserTranslations(profileMap[post.UserId].Locale) - - // If the channel has more than 1K users then @here is disabled - if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { - hereNotification = false - SendEphemeralPost( - team.Id, - post.UserId, - &model.Post{ - ChannelId: post.ChannelId, - Message: T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), - CreateAt: post.CreateAt + 1, - }, - ) - } - - // If the channel has more than 1K users then @channel is disabled - if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { - SendEphemeralPost( - team.Id, - post.UserId, - &model.Post{ - ChannelId: post.ChannelId, - Message: T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), - CreateAt: post.CreateAt + 1, - }, - ) - } - - // If the channel has more than 1K users then @all is disabled - if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { - SendEphemeralPost( - team.Id, - post.UserId, - &model.Post{ - ChannelId: post.ChannelId, - Message: T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), - CreateAt: post.CreateAt + 1, - }, - ) - } - - if hereNotification { - statuses := GetAllStatuses() + T := utils.GetUserTranslations(profileMap[post.UserId].Locale) + + // If the channel has more than 1K users then @here is disabled + if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + hereNotification = false + SendEphemeralPost( + team.Id, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), + CreateAt: post.CreateAt + 1, + }, + ) + } + + // If the channel has more than 1K users then @channel is disabled + if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + SendEphemeralPost( + team.Id, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), + CreateAt: post.CreateAt + 1, + }, + ) + } + + // If the channel has more than 1K users then @all is disabled + if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { + SendEphemeralPost( + team.Id, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), + CreateAt: post.CreateAt + 1, + }, + ) + } + + if hereNotification { + if result := <-Srv.Store.Status().GetOnline(); result.Err != nil { + return nil, result.Err + } else { + statuses := result.Data.([]*model.Status) for _, status := range statuses { if status.UserId == post.UserId { continue @@ -225,29 +222,43 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe } } } + } - // Make sure all mention updates are complete to prevent race - // Probably better to batch these DB updates in the future - // MUST be completed before push notifications send - for _, uchan := range updateMentionChans { - if result := <-uchan; result.Err != nil { - l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) - } + // Make sure all mention updates are complete to prevent race + // Probably better to batch these DB updates in the future + // MUST be completed before push notifications send + for _, uchan := range updateMentionChans { + if result := <-uchan; result.Err != nil { + l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) } + } - sendPushNotifications := false - if *utils.Cfg.EmailSettings.SendPushNotifications { - pushServer := *utils.Cfg.EmailSettings.PushNotificationServer - if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { - l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) - sendPushNotifications = false - } else { - sendPushNotifications = true + sendPushNotifications := false + if *utils.Cfg.EmailSettings.SendPushNotifications { + pushServer := *utils.Cfg.EmailSettings.PushNotificationServer + if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { + l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) + sendPushNotifications = false + } else { + sendPushNotifications = true + } + } + + if sendPushNotifications { + for _, id := range mentionedUsersList { + var status *model.Status + var err *model.AppError + if status, err = GetStatus(id); err != nil { + status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} + } + + if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { + sendPushNotification(post, profileMap[id], channel, senderName[id], true) } } - if sendPushNotifications { - for _, id := range mentionedUsersList { + for _, id := range allActivityPushUserIds { + if _, ok := mentionedUserIds[id]; !ok { var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { @@ -255,25 +266,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe } if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { - if err := sendPushNotification(post, profileMap[id], channel, senderName[id], true); err != nil { - l4g.Error(err.Error()) - } - } - } - - for _, id := range allActivityPushUserIds { - if _, ok := mentionedUserIds[id]; !ok { - var status *model.Status - var err *model.AppError - if status, err = GetStatus(id); err != nil { - status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} - } - - if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { - if err := sendPushNotification(post, profileMap[id], channel, senderName[id], false); err != nil { - l4g.Error(err.Error()) - } - } + sendPushNotification(post, profileMap[id], channel, senderName[id], false) } } } @@ -287,7 +280,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe message.Add("sender_name", senderUsername) message.Add("team_id", team.Id) - if len(post.FileIds) != 0 && fchan != nil { + if len(post.FileIds) != 0 { message.Add("otherFile", "true") var infos []*model.FileInfo @@ -314,7 +307,6 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe } func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError { - if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id { // this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil { @@ -327,18 +319,15 @@ func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Ch for i := range teams { if teams[i].Id == team.Id { found = true - team = teams[i] break } } - if !found { - if len(teams) > 0 { - team = teams[0] - } else { - // in case the user hasn't joined any teams we send them to the select_team page - team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName} - } + if !found && len(teams) > 0 { + team = teams[0] + } else { + // in case the user hasn't joined any teams we send them to the select_team page + team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName} } } } @@ -511,8 +500,9 @@ func sendPushNotification(post *model.Post, user *model.User, channel *model.Cha tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) if err := sendToPushProxy(tmpMessage); err != nil { - l4g.Error(err.Error) + return err } + if einterfaces.GetMetricsInterface() != nil { einterfaces.GetMetricsInterface().IncrementPostSentPush() } @@ -539,11 +529,12 @@ func ClearPushNotification(userId string, channelId string) *model.AppError { } l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId) + for _, session := range sessions { tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) if err := sendToPushProxy(tmpMessage); err != nil { - l4g.Error(err.Error) + return err } } @@ -551,6 +542,7 @@ func ClearPushNotification(userId string, channelId string) *model.AppError { } func sendToPushProxy(msg model.PushNotification) *model.AppError { + msg.ServerId = utils.CfgDiagnosticId tr := &http.Transport{ diff --git a/app/oauth.go b/app/oauth.go new file mode 100644 index 000000000..862897b24 --- /dev/null +++ b/app/oauth.go @@ -0,0 +1,34 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" +) + +func RevokeAccessToken(token string) *model.AppError { + + session, _ := GetSession(token) + schan := Srv.Store.Session().Remove(token) + + if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil { + return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "") + } + + tchan := Srv.Store.OAuth().RemoveAccessData(token) + + if result := <-tchan; result.Err != nil { + return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "") + } + + if result := <-schan; result.Err != nil { + return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "") + } + + if session != nil { + RemoveAllSessionsForUserId(session.UserId) + } + + return nil +} diff --git a/app/preference.go b/app/preference.go new file mode 100644 index 000000000..4e492c4a8 --- /dev/null +++ b/app/preference.go @@ -0,0 +1,16 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" +) + +func GetPreferencesForUser(userId string) (model.Preferences, *model.AppError) { + if result := <-Srv.Store.Preference().GetAll(userId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(model.Preferences), nil + } +} diff --git a/app/session.go b/app/session.go index 29c961e81..3bb167891 100644 --- a/app/session.go +++ b/app/session.go @@ -92,3 +92,55 @@ func InvalidateAllCaches() { func SessionCacheLength() int { return sessionCache.Len() } + +func RevokeSessionsForDeviceId(userId string, deviceId string, currentSessionId string) *model.AppError { + if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil { + return result.Err + } else { + sessions := result.Data.([]*model.Session) + for _, session := range sessions { + if session.DeviceId == deviceId && session.Id != currentSessionId { + l4g.Debug(utils.T("api.user.login.revoking.app_error"), session.Id, userId) + if err := RevokeSession(session); err != nil { + // Soft error so we still remove the other sessions + l4g.Error(err.Error()) + } + } + } + } + + return nil +} + +func RevokeSessionById(sessionId string) *model.AppError { + if result := <-Srv.Store.Session().Get(sessionId); result.Err != nil { + return result.Err + } else { + return RevokeSession(result.Data.(*model.Session)) + } +} + +func RevokeSession(session *model.Session) *model.AppError { + if session.IsOAuth { + if err := RevokeAccessToken(session.Token); err != nil { + return err + } + } else { + if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + return result.Err + } + } + + RevokeWebrtcToken(session.Id) + RemoveAllSessionsForUserId(session.UserId) + + return nil +} + +func AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError { + if result := <-Srv.Store.Session().UpdateDeviceId(sessionId, deviceId, expiresAt); result.Err != nil { + return result.Err + } + + return nil +} diff --git a/app/team.go b/app/team.go index 98b6894a5..495e0773f 100644 --- a/app/team.go +++ b/app/team.go @@ -80,3 +80,11 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { return nil } + +func GetTeamsForUser(userId string) ([]*model.Team, *model.AppError) { + if result := <-Srv.Store.Team().GetTeamsByUserId(userId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.Team), nil + } +} diff --git a/app/user.go b/app/user.go index 5acd9dcaa..909c8cca9 100644 --- a/app/user.go +++ b/app/user.go @@ -4,11 +4,105 @@ package app import ( + "bytes" + "fmt" + "hash/fnv" + "image" + "image/color" + "image/draw" + _ "image/gif" + _ "image/jpeg" + "image/png" + "io" + "io/ioutil" + "net/http" + "strconv" + "strings" + l4g "github.com/alecthomas/log4go" + "github.com/golang/freetype" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) +func CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) { + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_invalid.app_error", nil, "") + } + + if t, err := strconv.ParseInt(props["time"], 10, 64); err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_expired.app_error", nil, "") + } + + teamId := props["id"] + + var team *model.Team + if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + return nil, result.Err + } else { + team = result.Data.(*model.Team) + } + + user.Email = props["email"] + user.EmailVerified = true + + var ruser *model.User + var err *model.AppError + if ruser, err = CreateUser(user); err != nil { + return nil, err + } + + if err := JoinUserToTeam(team, ruser); err != nil { + return nil, err + } + + AddDirectChannels(team.Id, ruser) + + return ruser, nil +} + +func CreateUserWithInviteId(user *model.User, inviteId string) (*model.User, *model.AppError) { + var team *model.Team + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + return nil, result.Err + } else { + team = result.Data.(*model.Team) + } + + var ruser *model.User + var err *model.AppError + if ruser, err = CreateUser(user); err != nil { + return nil, err + } + + if err := JoinUserToTeam(team, ruser); err != nil { + return nil, err + } + + AddDirectChannels(team.Id, ruser) + + return ruser, nil +} + +func IsFirstUserAccount() bool { + if SessionCacheLength() == 0 { + if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil { + l4g.Error(cr.Err) + return false + } else { + count := cr.Data.(int64) + if count <= 0 { + return true + } + } + } + + return false +} + func CreateUser(user *model.User) (*model.User, *model.AppError) { user.Roles = model.ROLE_SYSTEM_USER.Id @@ -58,3 +152,330 @@ func CreateUser(user *model.User) (*model.User, *model.AppError) { return ruser, nil } } + +func CreateOAuthUser(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) { + var user *model.User + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.not_available.app_error", map[string]interface{}{"Service": strings.Title(service)}, "") + } else { + user = provider.GetUserFromJson(userData) + } + + if user == nil { + return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]interface{}{"Service": service}, "") + } + + suchan := Srv.Store.User().GetByAuth(user.AuthData, service) + euchan := Srv.Store.User().GetByEmail(user.Email) + + found := true + count := 0 + for found { + if found = IsUsernameTaken(user.Username); found { + user.Username = user.Username + strconv.Itoa(count) + count += 1 + } + } + + if result := <-suchan; result.Err == nil { + return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_used.app_error", map[string]interface{}{"Service": service}, "email="+user.Email) + } + + if result := <-euchan; result.Err == nil { + authService := result.Data.(*model.User).AuthService + if authService == "" { + return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", + map[string]interface{}{"Service": service, "Auth": model.USER_AUTH_SERVICE_EMAIL}, "email="+user.Email) + } else { + return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", + map[string]interface{}{"Service": service, "Auth": authService}, "email="+user.Email) + } + } + + user.EmailVerified = true + + ruser, err := CreateUser(user) + if err != nil { + return nil, err + } + + if len(teamId) > 0 { + err = JoinUserToTeamById(teamId, user) + if err != nil { + return nil, err + } + + err = AddDirectChannels(teamId, user) + if err != nil { + l4g.Error(err.Error()) + } + } + + return ruser, nil +} + +// Check if the username is already used by another user. Return false if the username is invalid. +func IsUsernameTaken(name string) bool { + + if !model.IsValidUsername(name) { + return false + } + + if result := <-Srv.Store.User().GetByUsername(name); result.Err != nil { + return false + } else { + return true + } + + return false +} + +func GetUser(userId string) (*model.User, *model.AppError) { + if result := <-Srv.Store.User().Get(userId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.User), nil + } +} + +func GetUserByUsername(username string) (*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.User), nil + } +} + +func GetUserByEmail(email string) (*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.User), nil + } +} + +func GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetByAuth(authData, authService); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.User), nil + } +} + +func GetUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) { + ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP + + if result := <-Srv.Store.User().GetForLogin( + loginId, + *utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap, + *utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap, + ldapAvailable, + ); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" { + // don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work + result.Err.StatusCode = http.StatusBadRequest + return nil, result.Err + } else if result.Err != nil { + if !ldapAvailable { + // failed to find user and no LDAP server to fall back on + result.Err.StatusCode = http.StatusBadRequest + return nil, result.Err + } + + // fall back to LDAP server to see if we can find a user + if ldapUser, ldapErr := einterfaces.GetLdapInterface().GetUser(loginId); ldapErr != nil { + ldapErr.StatusCode = http.StatusBadRequest + return nil, ldapErr + } else { + return ldapUser, nil + } + } else { + return result.Data.(*model.User), nil + } +} + +func GetUsers(offset int, limit int) (map[string]*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(map[string]*model.User), nil + } +} + +func GetUsersEtag() string { + return (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string) +} + +func GetUsersInTeam(teamId string, offset int, limit int) (map[string]*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(map[string]*model.User), nil + } +} + +func GetUsersInTeamEtag(teamId string) string { + return (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string) +} + +func GetUsersInChannel(channelId string, offset int, limit int) (map[string]*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(map[string]*model.User), nil + } +} + +func GetUsersNotInChannel(teamId string, channelId string, offset int, limit int) (map[string]*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(map[string]*model.User), nil + } +} + +func ActivateMfa(userId, token string) *model.AppError { + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + err := model.NewLocAppError("ActivateMfa", "api.user.update_mfa.not_available.app_error", nil, "") + err.StatusCode = http.StatusNotImplemented + return err + } + + var user *model.User + if result := <-Srv.Store.User().Get(userId); result.Err != nil { + return result.Err + } else { + user = result.Data.(*model.User) + } + + if len(user.AuthService) > 0 && user.AuthService != model.USER_AUTH_SERVICE_LDAP { + return model.NewLocAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "") + } + + if err := mfaInterface.Activate(user, token); err != nil { + return err + } + + return nil +} + +func DeactivateMfa(userId string) *model.AppError { + mfaInterface := einterfaces.GetMfaInterface() + if mfaInterface == nil { + err := model.NewLocAppError("DeactivateMfa", "api.user.update_mfa.not_available.app_error", nil, "") + err.StatusCode = http.StatusNotImplemented + return err + } + + if err := mfaInterface.Deactivate(userId); err != nil { + return err + } + + return nil +} + +func CreateProfileImage(username string, userId string) ([]byte, *model.AppError) { + colors := []color.NRGBA{ + {197, 8, 126, 255}, + {227, 207, 18, 255}, + {28, 181, 105, 255}, + {35, 188, 224, 255}, + {116, 49, 196, 255}, + {197, 8, 126, 255}, + {197, 19, 19, 255}, + {250, 134, 6, 255}, + {227, 207, 18, 255}, + {123, 201, 71, 255}, + {28, 181, 105, 255}, + {35, 188, 224, 255}, + {116, 49, 196, 255}, + {197, 8, 126, 255}, + {197, 19, 19, 255}, + {250, 134, 6, 255}, + {227, 207, 18, 255}, + {123, 201, 71, 255}, + {28, 181, 105, 255}, + {35, 188, 224, 255}, + {116, 49, 196, 255}, + {197, 8, 126, 255}, + {197, 19, 19, 255}, + {250, 134, 6, 255}, + {227, 207, 18, 255}, + {123, 201, 71, 255}, + } + + h := fnv.New32a() + h.Write([]byte(userId)) + seed := h.Sum32() + + initial := string(strings.ToUpper(username)[0]) + + fontBytes, err := ioutil.ReadFile(utils.FindDir("fonts") + utils.Cfg.FileSettings.InitialFont) + if err != nil { + return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error()) + } + font, err := freetype.ParseFont(fontBytes) + if err != nil { + return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error()) + } + + width := int(utils.Cfg.FileSettings.ProfileWidth) + height := int(utils.Cfg.FileSettings.ProfileHeight) + color := colors[int64(seed)%int64(len(colors))] + dstImg := image.NewRGBA(image.Rect(0, 0, width, height)) + srcImg := image.White + draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.ZP, draw.Src) + size := float64((width + height) / 4) + + c := freetype.NewContext() + c.SetFont(font) + c.SetFontSize(size) + c.SetClip(dstImg.Bounds()) + c.SetDst(dstImg) + c.SetSrc(srcImg) + + pt := freetype.Pt(width/6, height*2/3) + _, err = c.DrawString(initial, pt) + if err != nil { + return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error()) + } + + buf := new(bytes.Buffer) + + if imgErr := png.Encode(buf, dstImg); imgErr != nil { + return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.encode.app_error", nil, imgErr.Error()) + } else { + return buf.Bytes(), nil + } +} + +func GetProfileImage(user *model.User) ([]byte, *model.AppError) { + var img []byte + + if len(utils.Cfg.FileSettings.DriverName) == 0 { + var err *model.AppError + if img, err = CreateProfileImage(user.Username, user.Id); err != nil { + return nil, err + } + } else { + path := "users/" + user.Id + "/profile.png" + + if data, err := ReadFile(path); err != nil { + if img, err = CreateProfileImage(user.Username, user.Id); err != nil { + return nil, err + } + + if user.LastPictureUpdate == 0 { + if err := WriteFile(img, path); err != nil { + return nil, err + } + } + + } else { + img = data + } + } + + return img, nil +} diff --git a/app/user_test.go b/app/user_test.go new file mode 100644 index 000000000..ce2249ca0 --- /dev/null +++ b/app/user_test.go @@ -0,0 +1,27 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" +) + +func TestIsUsernameTaken(t *testing.T) { + th := Setup().InitBasic() + user := th.BasicUser + taken := IsUsernameTaken(user.Username) + + if !taken { + t.Logf("the username '%v' should be taken", user.Username) + t.FailNow() + } + + newUsername := "randomUsername" + taken = IsUsernameTaken(newUsername) + + if taken { + t.Logf("the username '%v' should not be taken", newUsername) + t.FailNow() + } +} diff --git a/app/webhook.go b/app/webhook.go index dfd59349f..70ba1d07a 100644 --- a/app/webhook.go +++ b/app/webhook.go @@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "net/http" + "regexp" "strings" l4g "github.com/alecthomas/log4go" @@ -118,6 +119,10 @@ func handleWebhookEvents(post *model.Post, team *model.Team, channel *model.Chan } func CreateWebhookPost(userId, teamId, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) { + // parse links into Markdown format + linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") + post := &model.Post{UserId: userId, ChannelId: channelId, Message: text, Type: postType} post.AddProp("from_webhook", "true") @@ -135,12 +140,41 @@ func CreateWebhookPost(userId, teamId, channelId, text, overrideUsername, overri } } - post.Message = parseSlackLinksToMarkdown(post.Message) - if len(props) > 0 { for key, val := range props { if key == "attachments" { - parseSlackAttachment(post, val) + if list, success := val.([]interface{}); success { + // parse attachment links into Markdown format + for i, aInt := range list { + attachment := aInt.(map[string]interface{}) + if aText, ok := attachment["text"].(string); ok { + aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") + attachment["text"] = aText + list[i] = attachment + } + if aText, ok := attachment["pretext"].(string); ok { + aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})") + attachment["pretext"] = aText + list[i] = attachment + } + if fVal, ok := attachment["fields"]; ok { + if fields, ok := fVal.([]interface{}); ok { + // parse attachment field links into Markdown format + for j, fInt := range fields { + field := fInt.(map[string]interface{}) + if fValue, ok := field["value"].(string); ok { + fValue = linkWithTextRegex.ReplaceAllString(fValue, "[${2}](${1})") + field["value"] = fValue + fields[j] = field + } + } + attachment["fields"] = fields + list[i] = attachment + } + } + } + post.AddProp(key, list) + } } else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" { post.AddProp(key, val) } diff --git a/app/webtrc.go b/app/webtrc.go new file mode 100644 index 000000000..b526c96a6 --- /dev/null +++ b/app/webtrc.go @@ -0,0 +1,33 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "crypto/tls" + "encoding/base64" + "net/http" + "strings" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func RevokeWebrtcToken(sessionId string) { + token := base64.StdEncoding.EncodeToString([]byte(sessionId)) + data := make(map[string]string) + data["janus"] = "remove_token" + data["token"] = token + data["transaction"] = model.NewId() + data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret + + rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data))) + rq.Header.Set("Content-Type", "application/json") + + // we do not care about the response + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + httpClient := &http.Client{Transport: tr} + httpClient.Do(rq) +} diff --git a/cmd/platform/oldcommands.go b/cmd/platform/oldcommands.go index 39560af7d..faf086cb7 100644 --- a/cmd/platform/oldcommands.go +++ b/cmd/platform/oldcommands.go @@ -781,7 +781,7 @@ func cmdResetMfa() { } } - if err := api.DeactivateMfa(user.Id); err != nil { + if err := app.DeactivateMfa(user.Id); err != nil { l4g.Error("%v", err) flushLogAndExit(1) } diff --git a/cmd/platform/user.go b/cmd/platform/user.go index aabc6afcf..4fa4cd36e 100644 --- a/cmd/platform/user.go +++ b/cmd/platform/user.go @@ -298,7 +298,7 @@ func resetUserMfaCmdF(cmd *cobra.Command, args []string) error { return errors.New("Unable to find user '" + args[i] + "'") } - if err := api.DeactivateMfa(user.Id); err != nil { + if err := app.DeactivateMfa(user.Id); err != nil { return err } } diff --git a/i18n/en.json b/i18n/en.json index 84ebd2748..f4b752dc1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -301,19 +301,19 @@ }, { "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", - "translation": "Failed to post displayname update message %v" + "translation": "Failed to post displayname update message" }, { "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", - "translation": "Failed to retrieve user while trying to save update channel displayname message %v" + "translation": "Failed to retrieve user while trying to save update channel displayname message" }, { "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", "translation": "%s updated the channel display name from: %s to: %s" }, { - "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error", - "translation": "Failed to post join/leave message %v" + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Failed to post update channel header message" }, { "id": "api.channel.post_update_channel_header_message_and_forget.removed", @@ -333,7 +333,7 @@ }, { "id": "api.channel.post_user_add_remove_message_and_forget.error", - "translation": "Failed to post join/leave message %v" + "translation": "Failed to post join/leave message" }, { "id": "api.channel.remove.default.app_error", @@ -2245,7 +2245,7 @@ }, { "id": "api.user.add_direct_channels_and_forget.failed.error", - "translation": "Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v" + "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}" }, { "id": "api.user.authorize_oauth_user.bad_response.app_error", |