From 0e2b321e6f5ab5983bc3428aa455dac7012c36ee Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Fri, 13 Jan 2017 15:17:50 -0500 Subject: Refactor and migrate more functions out of api into app package (#5063) --- app/audit.go | 16 ++ app/channel.go | 187 ++++++++++++++++++++++- app/file.go | 340 ++++++++++++++++++++++++++++++++++++++++++ app/file_test.go | 33 ++++ app/notification.go | 394 ++++++++++++++++++++++++------------------------ app/oauth.go | 34 +++++ app/preference.go | 16 ++ app/session.go | 52 +++++++ app/team.go | 8 + app/user.go | 421 ++++++++++++++++++++++++++++++++++++++++++++++++++++ app/user_test.go | 27 ++++ app/webhook.go | 40 ++++- app/webtrc.go | 33 ++++ 13 files changed, 1396 insertions(+), 205 deletions(-) create mode 100644 app/audit.go create mode 100644 app/file.go create mode 100644 app/file_test.go create mode 100644 app/oauth.go create mode 100644 app/preference.go create mode 100644 app/user_test.go create mode 100644 app/webtrc.go (limited to 'app') 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) +} -- cgit v1.2.3-1-g7c22