diff options
Diffstat (limited to 'api')
51 files changed, 9820 insertions, 0 deletions
diff --git a/api/api.go b/api/api.go new file mode 100644 index 000000000..70e1b64ae --- /dev/null +++ b/api/api.go @@ -0,0 +1,61 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "bytes" + l4g "code.google.com/p/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "html/template" + "net/http" +) + +var ServerTemplates *template.Template + +type ServerTemplatePage Page + +func NewServerTemplatePage(templateName, teamUrl string) *ServerTemplatePage { + props := make(map[string]string) + props["AnalyticsUrl"] = utils.Cfg.ServiceSettings.AnalyticsUrl + return &ServerTemplatePage{TemplateName: templateName, SiteName: utils.Cfg.ServiceSettings.SiteName, FeedbackEmail: utils.Cfg.EmailSettings.FeedbackEmail, TeamUrl: teamUrl, Props: props} +} + +func (me *ServerTemplatePage) Render() string { + var text bytes.Buffer + if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil { + l4g.Error("Error rendering template %v err=%v", me.TemplateName, err) + } + + return text.String() +} + +func InitApi() { + r := Srv.Router.PathPrefix("/api/v1").Subrouter() + InitUser(r) + InitTeam(r) + InitChannel(r) + InitPost(r) + InitWebSocket(r) + InitFile(r) + InitCommand(r) + + templatesDir := utils.FindDir("api/templates") + l4g.Debug("Parsing server templates at %v", templatesDir) + var err error + if ServerTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error("Failed to parse server templates %v", err) + } +} + +func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool { + if et := r.Header.Get(model.HEADER_ETAG_CLIENT); len(etag) > 0 { + if et == etag { + w.WriteHeader(http.StatusNotModified) + return true + } + } + + return false +} diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 000000000..ffa951f6d --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,42 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +var Client *model.Client + +func Setup() { + if Srv == nil { + utils.LoadConfig("config.json") + NewServer() + StartServer() + InitApi() + Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1") + } +} + +func SetupBenchmark() (*model.Team, *model.User, *model.Channel) { + Setup() + + team := &model.Team{Name: "Benchmark Team", Domain: "z-z-" + model.NewId() + "a", Email: "benchmark@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + user := &model.User{TeamId: team.Id, Email: model.NewId() + "benchmark@test.com", FullName: "Mr. Benchmarker", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + Client.LoginByEmail(team.Domain, user.Email, "pwd") + channel := &model.Channel{DisplayName: "Benchmark Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + return team, user, channel +} + +func TearDown() { + if Srv != nil { + StopServer() + } +} diff --git a/api/auto_channels.go b/api/auto_channels.go new file mode 100644 index 000000000..b72e5d538 --- /dev/null +++ b/api/auto_channels.go @@ -0,0 +1,70 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +type AutoChannelCreator struct { + client *model.Client + teamID string + Fuzzy bool + DisplayNameLen utils.Range + DisplayNameCharset string + NameLen utils.Range + NameCharset string + ChannelType string +} + +func NewAutoChannelCreator(client *model.Client, teamID string) *AutoChannelCreator { + return &AutoChannelCreator{ + client: client, + teamID: teamID, + Fuzzy: false, + DisplayNameLen: CHANNEL_DISPLAY_NAME_LEN, + DisplayNameCharset: utils.ALPHANUMERIC, + NameLen: CHANNEL_NAME_LEN, + NameCharset: utils.LOWERCASE, + ChannelType: CHANNEL_TYPE, + } +} + +func (cfg *AutoChannelCreator) createRandomChannel() (*model.Channel, bool) { + var displayName string + if cfg.Fuzzy { + displayName = utils.FuzzName() + } else { + displayName = utils.RandomName(cfg.NameLen, cfg.NameCharset) + } + name := utils.RandomName(cfg.NameLen, cfg.NameCharset) + + channel := &model.Channel{ + TeamId: cfg.teamID, + DisplayName: displayName, + Name: name, + Type: cfg.ChannelType} + + result, err := cfg.client.CreateChannel(channel) + if err != nil { + return nil, false + } + return result.Data.(*model.Channel), true +} + +func (cfg *AutoChannelCreator) CreateTestChannels(num utils.Range) ([]*model.Channel, bool) { + numChannels := utils.RandIntFromRange(num) + channels := make([]*model.Channel, numChannels) + + for i := 0; i < numChannels; i++ { + var err bool + channels[i], err = cfg.createRandomChannel() + if err != true { + return channels, false + } + } + + return channels, true +} diff --git a/api/auto_constants.go b/api/auto_constants.go new file mode 100644 index 000000000..7af90a5f1 --- /dev/null +++ b/api/auto_constants.go @@ -0,0 +1,36 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +const ( + USER_PASSWORD = "passwd" + CHANNEL_TYPE = model.CHANNEL_OPEN + FUZZ_USER_EMAIL_PREFIX_LEN = 10 + BTEST_TEAM_NAME = "TestTeam" + BTEST_TEAM_DOMAIN_NAME = "z-z-testdomaina" + BTEST_TEAM_EMAIL = "test@nowhere.com" + BTEST_TEAM_TYPE = model.TEAM_OPEN + BTEST_USER_NAME = "Mr. Testing Tester" + BTEST_USER_EMAIL = "success+ttester@simulator.amazonses.com" + BTEST_USER_PASSWORD = "passwd" +) + +var ( + TEAM_NAME_LEN = utils.Range{10, 20} + TEAM_DOMAIN_NAME_LEN = utils.Range{10, 20} + TEAM_EMAIL_LEN = utils.Range{15, 30} + USER_NAME_LEN = utils.Range{5, 20} + USER_EMAIL_LEN = utils.Range{15, 30} + CHANNEL_DISPLAY_NAME_LEN = utils.Range{10, 20} + CHANNEL_NAME_LEN = utils.Range{5, 20} + POST_MESSAGE_LEN = utils.Range{100, 400} + POST_HASHTAGS_NUM = utils.Range{5, 10} + POST_MENTIONS_NUM = utils.Range{0, 3} + TEST_IMAGE_FILENAMES = []string{"test.png", "salamander.jpg", "toothless.gif"} +) diff --git a/api/auto_enviroment.go b/api/auto_enviroment.go new file mode 100644 index 000000000..dd663533c --- /dev/null +++ b/api/auto_enviroment.go @@ -0,0 +1,97 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "math/rand" + "time" +) + +type TestEnviroment struct { + Teams []*model.Team + Enviroments []TeamEnviroment +} + +func CreateTestEnviromentWithTeams(client *model.Client, rangeTeams utils.Range, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TestEnviroment, bool) { + rand.Seed(time.Now().UTC().UnixNano()) + + teamCreator := NewAutoTeamCreator(client) + teamCreator.Fuzzy = fuzzy + teams, err := teamCreator.CreateTestTeams(rangeTeams) + if err != true { + return TestEnviroment{}, false + } + + enviroment := TestEnviroment{teams, make([]TeamEnviroment, len(teams))} + + for i, team := range teams { + userCreator := NewAutoUserCreator(client, team.Id) + userCreator.Fuzzy = fuzzy + randomUser, err := userCreator.createRandomUser() + if err != true { + return TestEnviroment{}, false + } + client.LoginById(randomUser.Id, USER_PASSWORD) + teamEnviroment, err := CreateTestEnviromentInTeam(client, team.Id, rangeChannels, rangeUsers, rangePosts, fuzzy) + if err != true { + return TestEnviroment{}, false + } + enviroment.Enviroments[i] = teamEnviroment + } + + return enviroment, true +} + +func CreateTestEnviromentInTeam(client *model.Client, teamID string, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnviroment, bool) { + rand.Seed(time.Now().UTC().UnixNano()) + + // We need to create at least one user + if rangeUsers.Begin <= 0 { + rangeUsers.Begin = 1 + } + + userCreator := NewAutoUserCreator(client, teamID) + userCreator.Fuzzy = fuzzy + users, err := userCreator.CreateTestUsers(rangeUsers) + if err != true { + return TeamEnviroment{}, false + } + usernames := make([]string, len(users)) + for i, user := range users { + usernames[i] = user.Username + } + + channelCreator := NewAutoChannelCreator(client, teamID) + channelCreator.Fuzzy = fuzzy + channels, err := channelCreator.CreateTestChannels(rangeChannels) + + // Have every user join every channel + for _, user := range users { + for _, channel := range channels { + client.LoginById(user.Id, USER_PASSWORD) + client.JoinChannel(channel.Id) + } + } + + if err != true { + return TeamEnviroment{}, false + } + numPosts := utils.RandIntFromRange(rangePosts) + numImages := utils.RandIntFromRange(rangePosts) / 4 + for j := 0; j < numPosts; j++ { + user := users[utils.RandIntFromRange(utils.Range{0, len(users) - 1})] + client.LoginById(user.Id, USER_PASSWORD) + for i, channel := range channels { + postCreator := NewAutoPostCreator(client, channel.Id) + postCreator.HasImage = i < numImages + postCreator.Users = usernames + postCreator.Fuzzy = fuzzy + postCreator.CreateRandomPost() + } + } + + return TeamEnviroment{users, channels}, true +} diff --git a/api/auto_posts.go b/api/auto_posts.go new file mode 100644 index 000000000..a014d22ae --- /dev/null +++ b/api/auto_posts.go @@ -0,0 +1,126 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "bytes" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "io" + "mime/multipart" + "os" +) + +type AutoPostCreator struct { + client *model.Client + channelid string + Fuzzy bool + TextLength utils.Range + HasImage bool + ImageFilenames []string + Users []string + Mentions utils.Range + Tags utils.Range +} + +// Automatic poster used for testing +func NewAutoPostCreator(client *model.Client, channelid string) *AutoPostCreator { + return &AutoPostCreator{ + client: client, + channelid: channelid, + Fuzzy: false, + TextLength: utils.Range{100, 200}, + HasImage: false, + ImageFilenames: TEST_IMAGE_FILENAMES, + Users: []string{}, + Mentions: utils.Range{0, 5}, + Tags: utils.Range{0, 7}, + } +} + +func (cfg *AutoPostCreator) UploadTestFile() ([]string, bool) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + filename := cfg.ImageFilenames[utils.RandIntFromRange(utils.Range{0, len(cfg.ImageFilenames) - 1})] + + part, err := writer.CreateFormFile("files", filename) + if err != nil { + return nil, false + } + + path := utils.FindDir("web/static/images") + file, err := os.Open(path + "/" + filename) + defer file.Close() + + _, err = io.Copy(part, file) + if err != nil { + return nil, false + } + + field, err := writer.CreateFormField("channel_id") + if err != nil { + return nil, false + } + + _, err = field.Write([]byte(cfg.channelid)) + if err != nil { + return nil, false + } + + err = writer.Close() + if err != nil { + return nil, false + } + + resp, appErr := cfg.client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) + if appErr != nil { + return nil, false + } + + return resp.Data.(*model.FileUploadResponse).Filenames, true +} + +func (cfg *AutoPostCreator) CreateRandomPost() (*model.Post, bool) { + var filenames []string + if cfg.HasImage { + var err1 bool + filenames, err1 = cfg.UploadTestFile() + if err1 == false { + return nil, false + } + } + + var postText string + if cfg.Fuzzy { + postText = utils.FuzzPost() + } else { + postText = utils.RandomText(cfg.TextLength, cfg.Tags, cfg.Mentions, cfg.Users) + } + + post := &model.Post{ + ChannelId: cfg.channelid, + Message: postText, + Filenames: filenames} + result, err2 := cfg.client.CreatePost(post) + if err2 != nil { + return nil, false + } + return result.Data.(*model.Post), true +} + +func (cfg *AutoPostCreator) CreateTestPosts(rangePosts utils.Range) ([]*model.Post, bool) { + numPosts := utils.RandIntFromRange(rangePosts) + posts := make([]*model.Post, numPosts) + + for i := 0; i < numPosts; i++ { + var err bool + posts[i], err = cfg.CreateRandomPost() + if err != true { + return posts, false + } + } + + return posts, true +} diff --git a/api/auto_teams.go b/api/auto_teams.go new file mode 100644 index 000000000..2fe826774 --- /dev/null +++ b/api/auto_teams.go @@ -0,0 +1,81 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +type TeamEnviroment struct { + Users []*model.User + Channels []*model.Channel +} + +type AutoTeamCreator struct { + client *model.Client + Fuzzy bool + NameLength utils.Range + NameCharset string + DomainLength utils.Range + DomainCharset string + EmailLength utils.Range + EmailCharset string +} + +func NewAutoTeamCreator(client *model.Client) *AutoTeamCreator { + return &AutoTeamCreator{ + client: client, + Fuzzy: false, + NameLength: TEAM_NAME_LEN, + NameCharset: utils.LOWERCASE, + DomainLength: TEAM_DOMAIN_NAME_LEN, + DomainCharset: utils.LOWERCASE, + EmailLength: TEAM_EMAIL_LEN, + EmailCharset: utils.LOWERCASE, + } +} + +func (cfg *AutoTeamCreator) createRandomTeam() (*model.Team, bool) { + var teamEmail string + var teamName string + var teamDomain string + if cfg.Fuzzy { + teamEmail = utils.FuzzEmail() + teamName = utils.FuzzName() + teamDomain = utils.FuzzName() + } else { + teamEmail = utils.RandomEmail(cfg.EmailLength, cfg.EmailCharset) + teamName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + teamDomain = utils.RandomName(cfg.NameLength, cfg.NameCharset) + model.NewId() + } + team := &model.Team{ + Name: teamName, + Domain: teamDomain, + Email: teamEmail, + Type: model.TEAM_OPEN, + } + + result, err := cfg.client.CreateTeam(team) + if err != nil { + return nil, false + } + createdTeam := result.Data.(*model.Team) + return createdTeam, true +} + +func (cfg *AutoTeamCreator) CreateTestTeams(num utils.Range) ([]*model.Team, bool) { + numTeams := utils.RandIntFromRange(num) + teams := make([]*model.Team, numTeams) + + for i := 0; i < numTeams; i++ { + var err bool + teams[i], err = cfg.createRandomTeam() + if err != true { + return teams, false + } + } + + return teams, true +} diff --git a/api/auto_users.go b/api/auto_users.go new file mode 100644 index 000000000..1874ffbec --- /dev/null +++ b/api/auto_users.go @@ -0,0 +1,92 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +type AutoUserCreator struct { + client *model.Client + teamID string + EmailLength utils.Range + EmailCharset string + NameLength utils.Range + NameCharset string + Fuzzy bool +} + +func NewAutoUserCreator(client *model.Client, teamID string) *AutoUserCreator { + return &AutoUserCreator{ + client: client, + teamID: teamID, + EmailLength: USER_EMAIL_LEN, + EmailCharset: utils.LOWERCASE, + NameLength: USER_NAME_LEN, + NameCharset: utils.LOWERCASE, + Fuzzy: false, + } +} + +// Basic test team and user so you always know one +func CreateBasicUser(client *model.Client) *model.AppError { + result, _ := client.FindTeamByDomain(BTEST_TEAM_DOMAIN_NAME, true) + if result.Data.(bool) == false { + newteam := &model.Team{Name: BTEST_TEAM_NAME, Domain: BTEST_TEAM_DOMAIN_NAME, Email: BTEST_TEAM_EMAIL, Type: BTEST_TEAM_TYPE} + result, err := client.CreateTeam(newteam) + if err != nil { + return err + } + basicteam := result.Data.(*model.Team) + newuser := &model.User{TeamId: basicteam.Id, Email: BTEST_USER_EMAIL, FullName: BTEST_USER_NAME, Password: BTEST_USER_PASSWORD} + result, err = client.CreateUser(newuser, "") + if err != nil { + return err + } + Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id) + } + return nil +} + +func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) { + var userEmail string + var userName string + if cfg.Fuzzy { + userEmail = utils.RandString(FUZZ_USER_EMAIL_PREFIX_LEN, utils.LOWERCASE) + "-" + utils.FuzzEmail() + userName = utils.FuzzName() + } else { + userEmail = utils.RandomEmail(cfg.EmailLength, cfg.EmailCharset) + userName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + } + + user := &model.User{ + TeamId: cfg.teamID, + Email: userEmail, + FullName: userName, + Password: USER_PASSWORD} + + result, err := cfg.client.CreateUser(user, "") + if err != nil { + return nil, false + } + // We need to cheat to verify the user's email + Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id) + return result.Data.(*model.User), true +} + +func (cfg *AutoUserCreator) CreateTestUsers(num utils.Range) ([]*model.User, bool) { + numUsers := utils.RandIntFromRange(num) + users := make([]*model.User, numUsers) + + for i := 0; i < numUsers; i++ { + var err bool + users[i], err = cfg.createRandomUser() + if err != true { + return users, false + } + } + + return users, true +} diff --git a/api/channel.go b/api/channel.go new file mode 100644 index 000000000..d3f6ca2de --- /dev/null +++ b/api/channel.go @@ -0,0 +1,713 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "fmt" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "net/http" + "strings" +) + +func InitChannel(r *mux.Router) { + l4g.Debug("Initializing channel api routes") + + sr := r.PathPrefix("/channels").Subrouter() + sr.Handle("/", ApiUserRequiredActivity(getChannels, false)).Methods("GET") + sr.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET") + sr.Handle("/create", ApiUserRequired(createChannel)).Methods("POST") + sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") + sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST") + sr.Handle("/update_desc", ApiUserRequired(updateChannelDesc)).Methods("POST") + sr.Handle("/update_notify_level", ApiUserRequired(updateNotifyLevel)).Methods("POST") + sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET") + sr.Handle("/{id:[A-Za-z0-9]+}/join", ApiUserRequired(joinChannel)).Methods("POST") + sr.Handle("/{id:[A-Za-z0-9]+}/leave", ApiUserRequired(leaveChannel)).Methods("POST") + sr.Handle("/{id:[A-Za-z0-9]+}/delete", ApiUserRequired(deleteChannel)).Methods("POST") + sr.Handle("/{id:[A-Za-z0-9]+}/add", ApiUserRequired(addChannelMember)).Methods("POST") + sr.Handle("/{id:[A-Za-z0-9]+}/remove", ApiUserRequired(removeChannelMember)).Methods("POST") + sr.Handle("/{id:[A-Za-z0-9]+}/update_last_viewed_at", ApiUserRequired(updateLastViewedAt)).Methods("POST") + +} + +func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { + + channel := model.ChannelFromJson(r.Body) + + if channel == nil { + c.SetInvalidParam("createChannel", "channel") + return + } + + if !c.HasPermissionsToTeam(channel.TeamId, "createChannel") { + return + } + + if channel.Type == model.CHANNEL_DIRECT { + c.Err = model.NewAppError("createDirectChannel", "Must use createDirectChannel api service for direct message channel creation", "") + return + } + + if strings.Index(channel.Name, "__") > 0 { + c.Err = model.NewAppError("createDirectChannel", "Invalid character '__' in channel name for non-direct channel", "") + return + } + + if sc, err := CreateChannel(c, channel, r.URL.Path, true); err != nil { + c.Err = err + return + } else { + w.Write([]byte(sc.ToJson())) + } +} + +func CreateChannel(c *Context, channel *model.Channel, path string, addMember bool) (*model.Channel, *model.AppError) { + if result := <-Srv.Store.Channel().Save(channel); result.Err != nil { + return nil, result.Err + } else { + sc := result.Data.(*model.Channel) + + if addMember { + cm := &model.ChannelMember{ChannelId: sc.Id, UserId: c.Session.UserId, + Roles: model.CHANNEL_ROLE_ADMIN, NotifyLevel: model.CHANNEL_NOTIFY_ALL} + + if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { + return nil, cmresult.Err + } + } + + c.LogAudit("name=" + channel.Name) + + return sc, nil + } +} + +func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) { + + data := model.MapFromJson(r.Body) + + userId := data["user_id"] + if len(userId) != 26 { + c.SetInvalidParam("createDirectChannel", "user_id") + return + } + + if !c.HasPermissionsToTeam(c.Session.TeamId, "createDirectChannel") { + return + } + + if sc, err := CreateDirectChannel(c, userId, r.URL.Path); err != nil { + c.Err = err + return + } else { + w.Write([]byte(sc.ToJson())) + } +} + +func CreateDirectChannel(c *Context, otherUserId string, path string) (*model.Channel, *model.AppError) { + if len(otherUserId) != 26 { + return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId) + } + + uc := Srv.Store.User().Get(otherUserId) + + channel := new(model.Channel) + + channel.DisplayName = "" + if otherUserId > c.Session.UserId { + channel.Name = c.Session.UserId + "__" + otherUserId + } else { + channel.Name = otherUserId + "__" + c.Session.UserId + } + + channel.TeamId = c.Session.TeamId + channel.Description = "" + channel.Type = model.CHANNEL_DIRECT + + if uresult := <-uc; uresult.Err != nil { + return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId) + } + + if sc, err := CreateChannel(c, channel, path, true); err != nil { + return nil, err + } else { + cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId, + Roles: "", NotifyLevel: model.CHANNEL_NOTIFY_ALL} + + if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { + return nil, cmresult.Err + } + + return sc, nil + } +} + +func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { + + channel := model.ChannelFromJson(r.Body) + + if channel == nil { + c.SetInvalidParam("updateChannel", "channel") + return + } + + sc := Srv.Store.Channel().Get(channel.Id) + cmc := Srv.Store.Channel().GetMember(channel.Id, c.Session.UserId) + + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return + } else if cmcresult := <-cmc; cmcresult.Err != nil { + c.Err = cmcresult.Err + return + } else { + oldChannel := cresult.Data.(*model.Channel) + channelMember := cmcresult.Data.(model.ChannelMember) + if !c.HasPermissionsToTeam(oldChannel.TeamId, "updateChannel") { + return + } + + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if oldChannel.DeleteAt > 0 { + c.Err = model.NewAppError("updateChannel", "The channel has been archived or deleted", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if oldChannel.Name == model.DEFAULT_CHANNEL { + c.Err = model.NewAppError("updateChannel", "Cannot update the default channel "+model.DEFAULT_CHANNEL, "") + c.Err.StatusCode = http.StatusForbidden + return + } + + oldChannel.Description = channel.Description + + if len(channel.DisplayName) > 0 { + oldChannel.DisplayName = channel.DisplayName + } + + if len(channel.Name) > 0 { + oldChannel.Name = channel.Name + } + + if len(channel.Type) > 0 { + oldChannel.Type = channel.Type + } + + if ucresult := <-Srv.Store.Channel().Update(oldChannel); ucresult.Err != nil { + c.Err = ucresult.Err + return + } else { + c.LogAudit("name=" + channel.Name) + w.Write([]byte(oldChannel.ToJson())) + } + } +} + +func updateChannelDesc(c *Context, w http.ResponseWriter, r *http.Request) { + + props := model.MapFromJson(r.Body) + channelId := props["channel_id"] + if len(channelId) != 26 { + c.SetInvalidParam("updateChannelDesc", "channel_id") + return + } + + channelDesc := props["channel_description"] + if len(channelDesc) > 1024 { + c.SetInvalidParam("updateChannelDesc", "channel_description") + return + } + + sc := Srv.Store.Channel().Get(channelId) + cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId) + + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return + } else if cmcresult := <-cmc; cmcresult.Err != nil { + c.Err = cmcresult.Err + return + } else { + channel := cresult.Data.(*model.Channel) + // Don't need to do anything channel member, just wanted to confirm it exists + + if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelDesc") { + return + } + + channel.Description = channelDesc + + if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil { + c.Err = ucresult.Err + return + } else { + c.LogAudit("name=" + channel.Name) + w.Write([]byte(channel.ToJson())) + } + } +} + +func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { + + // user is already in the newtork + + if result := <-Srv.Store.Channel().GetChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { + if result.Err.Message == "No channels were found" { + // lets make sure the user is valid + if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + c.Err = result.Err + c.RemoveSessionCookie(w) + l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) + return + } + } + c.Err = result.Err + return + } else if HandleEtag(result.Data.(*model.ChannelList).Etag(), w, r) { + return + } else { + data := result.Data.(*model.ChannelList) + w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag()) + w.Write([]byte(data.ToJson())) + } +} + +func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) { + + // user is already in the newtork + + if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { + c.Err = result.Err + return + } else if HandleEtag(result.Data.(*model.ChannelList).Etag(), w, r) { + return + } else { + data := result.Data.(*model.ChannelList) + w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag()) + w.Write([]byte(data.ToJson())) + } +} + +func joinChannel(c *Context, w http.ResponseWriter, r *http.Request) { + + params := mux.Vars(r) + channelId := params["id"] + + JoinChannel(c, channelId, r.URL.Path) + + if c.Err != nil { + return + } + + result := make(map[string]string) + result["id"] = channelId + w.Write([]byte(model.MapToJson(result))) +} + +func JoinChannel(c *Context, channelId string, path string) { + + sc := Srv.Store.Channel().Get(channelId) + uc := Srv.Store.User().Get(c.Session.UserId) + + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return + } else if uresult := <-uc; uresult.Err != nil { + c.Err = uresult.Err + return + } else { + channel := cresult.Data.(*model.Channel) + user := uresult.Data.(*model.User) + + if !c.HasPermissionsToTeam(channel.TeamId, "joinChannel") { + return + } + + if channel.DeleteAt > 0 { + c.Err = model.NewAppError("joinChannel", "The channel has been archived or deleted", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if channel.Type == model.CHANNEL_OPEN { + cm := &model.ChannelMember{ChannelId: channel.Id, UserId: c.Session.UserId, NotifyLevel: model.CHANNEL_NOTIFY_ALL} + + if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { + c.Err = cmresult.Err + return + } + + post := &model.Post{ChannelId: channel.Id, Message: fmt.Sprintf( + `User %v has joined this channel.`, + user.Username)} + if _, err := CreatePost(c, post, false); err != nil { + l4g.Error("Failed to post join message %v", err) + c.Err = model.NewAppError("joinChannel", "Failed to send join request", "") + return + } + } else { + c.Err = model.NewAppError("joinChannel", "You do not have the appropriate permissions", "") + c.Err.StatusCode = http.StatusForbidden + return + } + } +} + +func leaveChannel(c *Context, w http.ResponseWriter, r *http.Request) { + + params := mux.Vars(r) + id := params["id"] + + sc := Srv.Store.Channel().Get(id) + uc := Srv.Store.User().Get(c.Session.UserId) + + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return + } else if uresult := <-uc; uresult.Err != nil { + c.Err = cresult.Err + return + } else { + channel := cresult.Data.(*model.Channel) + user := uresult.Data.(*model.User) + + if !c.HasPermissionsToTeam(channel.TeamId, "leaveChannel") { + return + } + + if channel.DeleteAt > 0 { + c.Err = model.NewAppError("leaveChannel", "The channel has been archived or deleted", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if channel.Type == model.CHANNEL_DIRECT { + c.Err = model.NewAppError("leaveChannel", "Cannot leave a direct message channel", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if channel.Name == model.DEFAULT_CHANNEL { + c.Err = model.NewAppError("leaveChannel", "Cannot leave the default channel "+model.DEFAULT_CHANNEL, "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if cmresult := <-Srv.Store.Channel().RemoveMember(channel.Id, c.Session.UserId); cmresult.Err != nil { + c.Err = cmresult.Err + return + } + + post := &model.Post{ChannelId: channel.Id, Message: fmt.Sprintf( + `%v has left the channel.`, + user.Username)} + if _, err := CreatePost(c, post, false); err != nil { + l4g.Error("Failed to post leave message %v", err) + c.Err = model.NewAppError("leaveChannel", "Failed to send leave message", "") + return + } + + result := make(map[string]string) + result["id"] = channel.Id + w.Write([]byte(model.MapToJson(result))) + } +} + +func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { + + params := mux.Vars(r) + id := params["id"] + + sc := Srv.Store.Channel().Get(id) + scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) + uc := Srv.Store.User().Get(c.Session.UserId) + + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return + } else if uresult := <-uc; uresult.Err != nil { + c.Err = cresult.Err + return + } else if scmresult := <-scm; scmresult.Err != nil { + c.Err = scmresult.Err + return + } else { + channel := cresult.Data.(*model.Channel) + user := uresult.Data.(*model.User) + channelMember := scmresult.Data.(model.ChannelMember) + + if !c.HasPermissionsToTeam(channel.TeamId, "deleteChannel") { + return + } + + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + c.Err = model.NewAppError("deleteChannel", "You do not have the appropriate permissions", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if channel.DeleteAt > 0 { + c.Err = model.NewAppError("deleteChannel", "The channel has been archived or deleted", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if channel.Name == model.DEFAULT_CHANNEL { + c.Err = model.NewAppError("deleteChannel", "Cannot delete the default channel "+model.DEFAULT_CHANNEL, "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if dresult := <-Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); dresult.Err != nil { + c.Err = dresult.Err + return + } + + c.LogAudit("name=" + channel.Name) + + post := &model.Post{ChannelId: channel.Id, Message: fmt.Sprintf( + `%v has archived the channel.`, + user.Username)} + if _, err := CreatePost(c, post, false); err != nil { + l4g.Error("Failed to post archive message %v", err) + c.Err = model.NewAppError("deleteChannel", "Failed to send archive message", "") + return + } + + result := make(map[string]string) + result["id"] = channel.Id + w.Write([]byte(model.MapToJson(result))) + } +} + +func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + Srv.Store.Channel().UpdateLastViewedAt(id, c.Session.UserId) + + message := model.NewMessage(c.Session.TeamId, id, c.Session.UserId, model.ACTION_VIEWED) + message.Add("channel_id", id) + + store.PublishAndForget(message) + + result := make(map[string]string) + result["id"] = id + w.Write([]byte(model.MapToJson(result))) +} + +func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { + + params := mux.Vars(r) + id := params["id"] + + sc := Srv.Store.Channel().Get(id) + scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) + ecm := Srv.Store.Channel().GetExtraMembers(id, 20) + + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return + } else if cmresult := <-scm; cmresult.Err != nil { + c.Err = cmresult.Err + return + } else if ecmresult := <-ecm; ecmresult.Err != nil { + c.Err = ecmresult.Err + return + } else { + channel := cresult.Data.(*model.Channel) + member := cmresult.Data.(model.ChannelMember) + extraMembers := ecmresult.Data.([]model.ExtraMember) + + if !c.HasPermissionsToTeam(channel.TeamId, "getChannelExtraInfo") { + return + } + + if !c.HasPermissionsToUser(member.UserId, "getChannelExtraInfo") { + return + } + + if channel.DeleteAt > 0 { + c.Err = model.NewAppError("getChannelExtraInfo", "The channel has been archived or deleted", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + data := model.ChannelExtra{Id: channel.Id, Members: extraMembers} + w.Header().Set("Expires", "-1") + w.Write([]byte(data.ToJson())) + } +} + +func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + data := model.MapFromJson(r.Body) + userId := data["user_id"] + + if len(userId) != 26 { + c.SetInvalidParam("addChannelMember", "user_id") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + sc := Srv.Store.Channel().Get(id) + ouc := Srv.Store.User().Get(c.Session.UserId) + nuc := Srv.Store.User().Get(userId) + + // Only need to be a member of the channel to add a new member + if !c.HasPermissionsToChannel(cchan, "addChannelMember") { + return + } + + if nresult := <-nuc; nresult.Err != nil { + c.Err = model.NewAppError("addChannelMember", "Failed to find user to be added", "") + return + } else if cresult := <-sc; cresult.Err != nil { + c.Err = model.NewAppError("addChannelMember", "Failed to find channel", "") + return + } else { + channel := cresult.Data.(*model.Channel) + nUser := nresult.Data.(*model.User) + + if channel.DeleteAt > 0 { + c.Err = model.NewAppError("updateChannel", "The channel has been archived or deleted", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if oresult := <-ouc; oresult.Err != nil { + c.Err = model.NewAppError("addChannelMember", "Failed to find user doing the adding", "") + return + } else { + oUser := oresult.Data.(*model.User) + + cm := &model.ChannelMember{ChannelId: channel.Id, UserId: userId, NotifyLevel: model.CHANNEL_NOTIFY_ALL} + + if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { + l4g.Error("Failed to add member user_id=%v channel_id=%v err=%v", userId, id, cmresult.Err) + c.Err = model.NewAppError("addChannelMember", "Failed to add user to channel", "") + return + } + + post := &model.Post{ChannelId: id, Message: fmt.Sprintf( + `%v added to the channel by %v`, + nUser.Username, oUser.Username)} + if _, err := CreatePost(c, post, false); err != nil { + l4g.Error("Failed to post add message %v", err) + c.Err = model.NewAppError("addChannelMember", "Failed to add member to channel", "") + return + } + + c.LogAudit("name=" + channel.Name + " user_id=" + userId) + + <-Srv.Store.Channel().UpdateLastViewedAt(id, oUser.Id) + w.Write([]byte(cm.ToJson())) + } + } +} + +func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + data := model.MapFromJson(r.Body) + userId := data["user_id"] + + if len(userId) != 26 { + c.SetInvalidParam("addChannelMember", "user_id") + return + } + + sc := Srv.Store.Channel().Get(id) + cmc := Srv.Store.Channel().GetMember(id, c.Session.UserId) + + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return + } else if cmcresult := <-cmc; cmcresult.Err != nil { + c.Err = cmcresult.Err + return + } else { + channel := cresult.Data.(*model.Channel) + channelMember := cmcresult.Data.(model.ChannelMember) + + if !c.HasPermissionsToTeam(channel.TeamId, "removeChannelMember") { + return + } + + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions ", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if channel.DeleteAt > 0 { + c.Err = model.NewAppError("updateChannel", "The channel has been archived or deleted", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if cmresult := <-Srv.Store.Channel().RemoveMember(id, userId); cmresult.Err != nil { + c.Err = cmresult.Err + return + } + + c.LogAudit("name=" + channel.Name + " user_id=" + userId) + + result := make(map[string]string) + result["channel_id"] = channel.Id + result["removed_user_id"] = userId + w.Write([]byte(model.MapToJson(result))) + } + +} + +func updateNotifyLevel(c *Context, w http.ResponseWriter, r *http.Request) { + data := model.MapFromJson(r.Body) + userId := data["user_id"] + if len(userId) != 26 { + c.SetInvalidParam("updateNotifyLevel", "user_id") + return + } + + channelId := data["channel_id"] + if len(channelId) != 26 { + c.SetInvalidParam("updateNotifyLevel", "channel_id") + return + } + + notifyLevel := data["notify_level"] + if len(notifyLevel) == 0 || !model.IsChannelNotifyLevelValid(notifyLevel) { + c.SetInvalidParam("updateNotifyLevel", "notify_level") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + + if !c.HasPermissionsToUser(userId, "updateNotifyLevel") { + return + } + + if !c.HasPermissionsToChannel(cchan, "updateNotifyLevel") { + return + } + + if result := <-Srv.Store.Channel().UpdateNotifyLevel(channelId, userId, notifyLevel); result.Err != nil { + c.Err = result.Err + return + } + + w.Write([]byte(model.MapToJson(data))) +} diff --git a/api/channel_benchmark_test.go b/api/channel_benchmark_test.go new file mode 100644 index 000000000..bb00da138 --- /dev/null +++ b/api/channel_benchmark_test.go @@ -0,0 +1,287 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "testing" +) + +const ( + NUM_CHANNELS = 140 +) + +func BenchmarkCreateChannel(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + ) + team, _, _ := SetupBenchmark() + + channelCreator := NewAutoChannelCreator(Client, team.Id) + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + } +} + +func BenchmarkCreateDirectChannel(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + ) + team, _, _ := SetupBenchmark() + + userCreator := NewAutoUserCreator(Client, team.Id) + users, err := userCreator.CreateTestUsers(NUM_CHANNELS_RANGE) + if err == false { + b.Fatal("Could not create users") + } + + data := make([]map[string]string, len(users)) + + for i := range data { + newmap := map[string]string{ + "user_id": users[i].Id, + } + data[i] = newmap + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < NUM_CHANNELS; j++ { + Client.CreateDirectChannel(data[j]) + } + } +} + +func BenchmarkUpdateChannel(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + CHANNEL_DESCRIPTION_LEN = 50 + ) + team, _, _ := SetupBenchmark() + + channelCreator := NewAutoChannelCreator(Client, team.Id) + channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + if valid == false { + b.Fatal("Unable to create test channels") + } + + for i := range channels { + channels[i].Description = utils.RandString(CHANNEL_DESCRIPTION_LEN, utils.ALPHANUMERIC) + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := range channels { + if _, err := Client.UpdateChannel(channels[j]); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkGetChannels(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + ) + team, _, _ := SetupBenchmark() + + channelCreator := NewAutoChannelCreator(Client, team.Id) + _, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + if valid == false { + b.Fatal("Unable to create test channels") + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + Client.Must(Client.GetChannels("")) + } +} + +func BenchmarkGetMoreChannels(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + ) + team, _, _ := SetupBenchmark() + + channelCreator := NewAutoChannelCreator(Client, team.Id) + _, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + if valid == false { + b.Fatal("Unable to create test channels") + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + Client.Must(Client.GetMoreChannels("")) + } +} + +func BenchmarkJoinChannel(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + ) + team, _, _ := SetupBenchmark() + + channelCreator := NewAutoChannelCreator(Client, team.Id) + channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + if valid == false { + b.Fatal("Unable to create test channels") + } + + // Secondary test user to join channels created by primary test user + user := &model.User{TeamId: team.Id, Email: model.NewId() + "random@test.com", FullName: "That Guy", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := range channels { + Client.Must(Client.JoinChannel(channels[j].Id)) + } + } +} + +func BenchmarkDeleteChannel(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + ) + team, _, _ := SetupBenchmark() + + channelCreator := NewAutoChannelCreator(Client, team.Id) + channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + if valid == false { + b.Fatal("Unable to create test channels") + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := range channels { + Client.Must(Client.DeleteChannel(channels[j].Id)) + } + } +} + +func BenchmarkGetChannelExtraInfo(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + ) + team, _, _ := SetupBenchmark() + + channelCreator := NewAutoChannelCreator(Client, team.Id) + channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + if valid == false { + b.Fatal("Unable to create test channels") + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := range channels { + Client.Must(Client.GetChannelExtraInfo(channels[j].Id)) + } + } +} + +func BenchmarkAddChannelMember(b *testing.B) { + var ( + NUM_USERS = 100 + NUM_USERS_RANGE = utils.Range{NUM_USERS, NUM_USERS} + ) + team, _, _ := SetupBenchmark() + + channel := &model.Channel{DisplayName: "Test Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + userCreator := NewAutoUserCreator(Client, team.Id) + users, valid := userCreator.CreateTestUsers(NUM_USERS_RANGE) + if valid == false { + b.Fatal("Unable to create test users") + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := range users { + if _, err := Client.AddChannelMember(channel.Id, users[j].Id); err != nil { + b.Fatal(err) + } + } + } +} + +// Is this benchmark failing? Raise your file ulimit! 2048 worked for me. +func BenchmarkRemoveChannelMember(b *testing.B) { + var ( + NUM_USERS = 140 + NUM_USERS_RANGE = utils.Range{NUM_USERS, NUM_USERS} + ) + team, _, _ := SetupBenchmark() + + channel := &model.Channel{DisplayName: "Test Channel", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + userCreator := NewAutoUserCreator(Client, team.Id) + users, valid := userCreator.CreateTestUsers(NUM_USERS_RANGE) + if valid == false { + b.Fatal("Unable to create test users") + } + + for i := range users { + if _, err := Client.AddChannelMember(channel.Id, users[i].Id); err != nil { + b.Fatal(err) + } + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := range users { + if _, err := Client.RemoveChannelMember(channel.Id, users[j].Id); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkUpdateNotifyLevel(b *testing.B) { + var ( + NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} + ) + team, user, _ := SetupBenchmark() + + channelCreator := NewAutoChannelCreator(Client, team.Id) + channels, valid := channelCreator.CreateTestChannels(NUM_CHANNELS_RANGE) + if valid == false { + b.Fatal("Unable to create test channels") + } + + data := make([]map[string]string, len(channels)) + + for i := range data { + newmap := map[string]string{ + "channel_id": channels[i].Id, + "user_id": user.Id, + "notify_level": model.CHANNEL_NOTIFY_MENTION, + } + data[i] = newmap + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := range channels { + Client.Must(Client.UpdateNotifyLevel(data[j])) + } + } +} diff --git a/api/channel_test.go b/api/channel_test.go new file mode 100644 index 000000000..e8aaf4e3f --- /dev/null +++ b/api/channel_test.go @@ -0,0 +1,787 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "net/http" + "testing" +) + +func TestCreateChannel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + team2 := &model.Team{Name: "Name Team 2", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + channel := model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + rchannel, err := Client.CreateChannel(&channel) + if err != nil { + t.Fatal(err) + } + + if rchannel.Data.(*model.Channel).Name != channel.Name { + t.Fatal("full name didn't match") + } + + rget := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList) + if rget.Channels[0].Name != channel.Name { + t.Fatal("full name didn't match") + } + + if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err == nil { + t.Fatal("Cannot create an existing") + } + + rchannel.Data.(*model.Channel).Id = "" + if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err != nil { + if err.Message != "A channel with that name already exists" { + t.Fatal(err) + } + } + + if _, err := Client.DoPost("/channels/create", "garbage"); err == nil { + t.Fatal("should have been an error") + } + + channel = model.Channel{DisplayName: "Channel on Different Team", Name: "aaaa" + model.NewId() + "abbb", Type: model.CHANNEL_OPEN, TeamId: team2.Id} + + if _, err := Client.CreateChannel(&channel); err.StatusCode != http.StatusForbidden { + t.Fatal(err) + } + + channel = model.Channel{DisplayName: "Test API Name", Name: model.NewId() + "__" + model.NewId(), Type: model.CHANNEL_OPEN, TeamId: team.Id} + + if _, err := Client.CreateChannel(&channel); err == nil { + t.Fatal("Should have errored out on invalid '__' character") + } + + channel = model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_DIRECT, TeamId: team.Id} + + if _, err := Client.CreateChannel(&channel); err == nil { + t.Fatal("Should have errored out on direct channel type") + } +} + +func TestCreateDirectChannel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + data := make(map[string]string) + data["user_id"] = user2.Id + + rchannel, err := Client.CreateDirectChannel(data) + if err != nil { + t.Fatal(err) + } + + channelName := "" + if user2.Id > user.Id { + channelName = user.Id + "__" + user2.Id + } else { + channelName = user2.Id + "__" + user.Id + } + + if rchannel.Data.(*model.Channel).Name != channelName { + t.Fatal("channel name didn't match") + } + + if rchannel.Data.(*model.Channel).Type != model.CHANNEL_DIRECT { + t.Fatal("channel type was not direct") + } + + if _, err := Client.CreateDirectChannel(data); err == nil { + t.Fatal("channel already exists and should have failed") + } + + data["user_id"] = "junk" + if _, err := Client.CreateDirectChannel(data); err == nil { + t.Fatal("should have failed with bad user id") + } + + data["user_id"] = "12345678901234567890123456" + if _, err := Client.CreateDirectChannel(data); err == nil { + t.Fatal("should have failed with non-existent user") + } + +} + +func TestUpdateChannel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + userTeamAdmin := &model.User{TeamId: team.Id, Email: team.Email, FullName: "Corey Hulen", Password: "pwd"} + userTeamAdmin = Client.Must(Client.CreateUser(userTeamAdmin, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userTeamAdmin.Id) + + userChannelAdmin := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + userChannelAdmin = Client.Must(Client.CreateUser(userChannelAdmin, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userChannelAdmin.Id) + + userStd := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + userStd = Client.Must(Client.CreateUser(userStd, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userStd.Id) + userStd.Roles = "" + + Client.LoginByEmail(team.Domain, userChannelAdmin.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + Client.AddChannelMember(channel1.Id, userTeamAdmin.Id) + + desc := "a" + model.NewId() + "a" + upChannel1 := &model.Channel{Id: channel1.Id, Description: desc} + upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel) + + if upChannel1.Description != desc { + t.Fatal("Channel admin failed to update desc") + } + + if upChannel1.DisplayName != channel1.DisplayName { + t.Fatal("Channel admin failed to skip displayName") + } + + Client.LoginByEmail(team.Domain, userTeamAdmin.Email, "pwd") + + desc = "b" + model.NewId() + "b" + upChannel1 = &model.Channel{Id: channel1.Id, Description: desc} + upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel) + + if upChannel1.Description != desc { + t.Fatal("Team admin failed to update desc") + } + + if upChannel1.DisplayName != channel1.DisplayName { + t.Fatal("Team admin failed to skip displayName") + } + + rget := Client.Must(Client.GetChannels("")) + data := rget.Data.(*model.ChannelList) + for _, c := range data.Channels { + if c.Name == model.DEFAULT_CHANNEL { + c.Description = "new desc" + if _, err := Client.UpdateChannel(c); err == nil { + t.Fatal("should have errored on updating default channel") + } + break + } + } + + Client.LoginByEmail(team.Domain, userStd.Email, "pwd") + + if _, err := Client.UpdateChannel(upChannel1); err == nil { + t.Fatal("Standard User should have failed to update") + } +} + +func TestUpdateChannelDesc(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + data := make(map[string]string) + data["channel_id"] = channel1.Id + data["channel_description"] = "new desc" + + var upChannel1 *model.Channel + if result, err := Client.UpdateChannelDesc(data); err != nil { + t.Fatal(err) + } else { + upChannel1 = result.Data.(*model.Channel) + } + + if upChannel1.Description != data["channel_description"] { + t.Fatal("Failed to update desc") + } + + data["channel_id"] = "junk" + if _, err := Client.UpdateChannelDesc(data); err == nil { + t.Fatal("should have errored on junk channel id") + } + + data["channel_id"] = "12345678901234567890123456" + if _, err := Client.UpdateChannelDesc(data); err == nil { + t.Fatal("should have errored on non-existent channel id") + } + + data["channel_id"] = channel1.Id + data["channel_description"] = "" + for i := 0; i < 1050; i++ { + data["channel_description"] += "a" + } + if _, err := Client.UpdateChannelDesc(data); err == nil { + t.Fatal("should have errored on bad channel desc") + } + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + data["channel_id"] = channel1.Id + data["channel_description"] = "new desc" + if _, err := Client.UpdateChannelDesc(data); err == nil { + t.Fatal("should have errored non-channel member trying to update desc") + } +} + +func TestGetChannel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + rget := Client.Must(Client.GetChannels("")) + data := rget.Data.(*model.ChannelList) + + if data.Channels[0].DisplayName != channel1.DisplayName { + t.Fatal("full name didn't match") + } + + if data.Channels[1].DisplayName != channel2.DisplayName { + t.Fatal("full name didn't match") + } + + // test etag caching + if cache_result, err := Client.GetChannels(rget.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.ChannelList) != nil { + t.Log(cache_result.Data) + t.Fatal("cache should be empty") + } + + if _, err := Client.UpdateLastViewedAt(channel2.Id); err != nil { + t.Fatal(err) + } +} + +func TestGetMoreChannel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + rget := Client.Must(Client.GetMoreChannels("")) + data := rget.Data.(*model.ChannelList) + + if data.Channels[0].DisplayName != channel1.DisplayName { + t.Fatal("full name didn't match") + } + + if data.Channels[1].DisplayName != channel2.DisplayName { + t.Fatal("full name didn't match") + } + + // test etag caching + if cache_result, err := Client.GetMoreChannels(rget.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.ChannelList) != nil { + t.Log(cache_result.Data) + t.Fatal("cache should be empty") + } +} + +func TestJoinChannel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel3 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} + channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + Client.Must(Client.JoinChannel(channel1.Id)) + + if _, err := Client.JoinChannel(channel3.Id); err == nil { + t.Fatal("shouldn't be able to join secret group") + } + + data := make(map[string]string) + data["user_id"] = user1.Id + rchannel := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel) + + user3 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + + Client.LoginByEmail(team.Domain, user3.Email, "pwd") + + if _, err := Client.JoinChannel(rchannel.Id); err == nil { + t.Fatal("shoudn't be able to join direct channel") + } +} + +func TestLeaveChannel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel3 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} + channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + Client.Must(Client.JoinChannel(channel1.Id)) + + // No error if you leave a channel you cannot see + Client.Must(Client.LeaveChannel(channel3.Id)) + + data := make(map[string]string) + data["user_id"] = user1.Id + rchannel := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel) + + if _, err := Client.LeaveChannel(rchannel.Id); err == nil { + t.Fatal("should have errored, cannot leave direct channel") + } + + rget := Client.Must(Client.GetChannels("")) + cdata := rget.Data.(*model.ChannelList) + for _, c := range cdata.Channels { + if c.Name == model.DEFAULT_CHANNEL { + if _, err := Client.LeaveChannel(c.Id); err == nil { + t.Fatal("should have errored on leaving default channel") + } + break + } + } +} + +func TestDeleteChannel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + userTeamAdmin := &model.User{TeamId: team.Id, Email: team.Email, FullName: "Corey Hulen", Password: "pwd"} + userTeamAdmin = Client.Must(Client.CreateUser(userTeamAdmin, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userTeamAdmin.Id) + + userChannelAdmin := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + userChannelAdmin = Client.Must(Client.CreateUser(userChannelAdmin, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userChannelAdmin.Id) + + Client.LoginByEmail(team.Domain, userChannelAdmin.Email, "pwd") + + channelMadeByCA := &model.Channel{DisplayName: "C Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channelMadeByCA = Client.Must(Client.CreateChannel(channelMadeByCA)).Data.(*model.Channel) + + Client.AddChannelMember(channelMadeByCA.Id, userTeamAdmin.Id) + + Client.LoginByEmail(team.Domain, userTeamAdmin.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "B Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + if _, err := Client.DeleteChannel(channel1.Id); err != nil { + t.Fatal(err) + } + + if _, err := Client.DeleteChannel(channelMadeByCA.Id); err != nil { + t.Fatal("Team admin failed to delete Channel Admin's channel") + } + + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + if _, err := Client.CreatePost(post1); err == nil { + t.Fatal("should have failed to post to deleted channel") + } + + userStd := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + userStd = Client.Must(Client.CreateUser(userStd, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userStd.Id) + + Client.LoginByEmail(team.Domain, userStd.Email, "pwd") + + if _, err := Client.JoinChannel(channel1.Id); err == nil { + t.Fatal("should have failed to join deleted channel") + } + + Client.Must(Client.JoinChannel(channel2.Id)) + + if _, err := Client.DeleteChannel(channel2.Id); err == nil { + t.Fatal("should have failed to delete channel you're not an admin of") + } + + rget := Client.Must(Client.GetChannels("")) + cdata := rget.Data.(*model.ChannelList) + for _, c := range cdata.Channels { + if c.Name == model.DEFAULT_CHANNEL { + if _, err := Client.DeleteChannel(c.Id); err == nil { + t.Fatal("should have errored on deleting default channel") + } + break + } + } +} + +func TestGetChannelExtraInfo(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + rget := Client.Must(Client.GetChannelExtraInfo(channel1.Id)).Data.(*model.ChannelExtra) + if rget.Id != channel1.Id { + t.Fatal("couldnt't get extra info") + } +} + +func TestAddChannelMember(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + if _, err := Client.AddChannelMember(channel1.Id, user2.Id); err != nil { + t.Fatal(err) + } + + if _, err := Client.AddChannelMember(channel1.Id, "dsgsdg"); err == nil { + t.Fatal("Should have errored, bad user id") + } + + if _, err := Client.AddChannelMember(channel1.Id, "12345678901234567890123456"); err == nil { + t.Fatal("Should have errored, bad user id") + } + + if _, err := Client.AddChannelMember(channel1.Id, user2.Id); err == nil { + t.Fatal("Should have errored, user already a member") + } + + if _, err := Client.AddChannelMember("sgdsgsdg", user2.Id); err == nil { + t.Fatal("Should have errored, bad channel id") + } + + channel2 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + if _, err := Client.AddChannelMember(channel2.Id, user2.Id); err == nil { + t.Fatal("Should have errored, user not in channel") + } + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + Client.Must(Client.DeleteChannel(channel2.Id)) + + if _, err := Client.AddChannelMember(channel2.Id, user2.Id); err == nil { + t.Fatal("Should have errored, channel deleted") + } + +} + +func TestRemoveChannelMember(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + userTeamAdmin := &model.User{TeamId: team.Id, Email: team.Email, FullName: "Corey Hulen", Password: "pwd"} + userTeamAdmin = Client.Must(Client.CreateUser(userTeamAdmin, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userTeamAdmin.Id) + + userChannelAdmin := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + userChannelAdmin = Client.Must(Client.CreateUser(userChannelAdmin, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userChannelAdmin.Id) + + Client.LoginByEmail(team.Domain, userChannelAdmin.Email, "pwd") + + channelMadeByCA := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channelMadeByCA = Client.Must(Client.CreateChannel(channelMadeByCA)).Data.(*model.Channel) + + Client.Must(Client.AddChannelMember(channelMadeByCA.Id, userTeamAdmin.Id)) + + Client.LoginByEmail(team.Domain, userTeamAdmin.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + userStd := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + userStd = Client.Must(Client.CreateUser(userStd, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(userStd.Id) + + Client.Must(Client.AddChannelMember(channel1.Id, userStd.Id)) + + Client.Must(Client.AddChannelMember(channelMadeByCA.Id, userStd.Id)) + + if _, err := Client.RemoveChannelMember(channel1.Id, "dsgsdg"); err == nil { + t.Fatal("Should have errored, bad user id") + } + + if _, err := Client.RemoveChannelMember("sgdsgsdg", userStd.Id); err == nil { + t.Fatal("Should have errored, bad channel id") + } + + if _, err := Client.RemoveChannelMember(channel1.Id, userStd.Id); err != nil { + t.Fatal(err) + } + + if _, err := Client.RemoveChannelMember(channelMadeByCA.Id, userStd.Id); err != nil { + t.Fatal("Team Admin failed to remove member from Channel Admin's channel") + } + + channel2 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + Client.LoginByEmail(team.Domain, userStd.Email, "pwd") + + if _, err := Client.RemoveChannelMember(channel2.Id, userStd.Id); err == nil { + t.Fatal("Should have errored, user not channel admin") + } + + Client.LoginByEmail(team.Domain, userTeamAdmin.Email, "pwd") + Client.Must(Client.AddChannelMember(channel2.Id, userStd.Id)) + + Client.Must(Client.DeleteChannel(channel2.Id)) + + if _, err := Client.RemoveChannelMember(channel2.Id, userStd.Id); err == nil { + t.Fatal("Should have errored, channel deleted") + } + +} + +func TestUpdateNotifyLevel(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + data := make(map[string]string) + data["channel_id"] = channel1.Id + data["user_id"] = user.Id + data["notify_level"] = model.CHANNEL_NOTIFY_MENTION + + if _, err := Client.UpdateNotifyLevel(data); err != nil { + t.Fatal(err) + } + + rget := Client.Must(Client.GetChannels("")) + rdata := rget.Data.(*model.ChannelList) + if len(rdata.Members) == 0 || rdata.Members[channel1.Id].NotifyLevel != data["notify_level"] { + t.Fatal("NotifyLevel did not update properly") + } + + data["user_id"] = "junk" + if _, err := Client.UpdateNotifyLevel(data); err == nil { + t.Fatal("Should have errored - bad user id") + } + + data["user_id"] = "12345678901234567890123456" + if _, err := Client.UpdateNotifyLevel(data); err == nil { + t.Fatal("Should have errored - bad user id") + } + + data["user_id"] = user.Id + data["channel_id"] = "junk" + if _, err := Client.UpdateNotifyLevel(data); err == nil { + t.Fatal("Should have errored - bad channel id") + } + + data["channel_id"] = "12345678901234567890123456" + if _, err := Client.UpdateNotifyLevel(data); err == nil { + t.Fatal("Should have errored - bad channel id") + } + + data["channel_id"] = channel1.Id + data["notify_level"] = "" + if _, err := Client.UpdateNotifyLevel(data); err == nil { + t.Fatal("Should have errored - empty notify level") + } + + data["notify_level"] = "junk" + if _, err := Client.UpdateNotifyLevel(data); err == nil { + t.Fatal("Should have errored - bad notify level") + } + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + data["channel_id"] = channel1.Id + data["user_id"] = user2.Id + data["notify_level"] = model.CHANNEL_NOTIFY_MENTION + if _, err := Client.UpdateNotifyLevel(data); err == nil { + t.Fatal("Should have errored - user not in channel") + } +} + +func TestFuzzyChannel(t *testing.T) { + Setup(); + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + // Strings that should pass as acceptable channel names + var fuzzyStringsPass = []string { + "*", "?", ".", "}{][)(><", "{}[]()<>", + + "qahwah ( قهوة)", + "שָׁלוֹם עֲלֵיכֶם", + "Ramen チャーシュー chāshū", + "言而无信", + "Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒", + "& < &qu", + + "' or '1'='1' -- ", + "' or '1'='1' ({ ", + "' or '1'='1' /* ", + "1;DROP TABLE users", + + "<b><i><u><strong><em>", + + "sue@thatmightbe", + "sue@thatmightbe.", + "sue@thatmightbe.c", + "sue@thatmightbe.co", + "su+san@thatmightbe.com", + "a@b.中国", + "1@2.am", + "a@b.co.uk", + "a@b.cancerresearch", + "local@[127.0.0.1]", + } + + for i := 0; i < len(fuzzyStringsPass); i++ { + channel := model.Channel{DisplayName: fuzzyStringsPass[i], Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + + _, err := Client.CreateChannel(&channel) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/api/command.go b/api/command.go new file mode 100644 index 000000000..9efc79b49 --- /dev/null +++ b/api/command.go @@ -0,0 +1,470 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/http" + "strconv" + "strings" +) + +type commandHandler func(c *Context, command *model.Command) bool + +var commands = []commandHandler{ + logoutCommand, + joinCommand, + loadTestCommand, + echoCommand, +} + +func InitCommand(r *mux.Router) { + l4g.Debug("Initializing command api routes") + r.Handle("/command", ApiUserRequired(command)).Methods("POST") + hub.Start() +} + +func command(c *Context, w http.ResponseWriter, r *http.Request) { + + props := model.MapFromJson(r.Body) + + command := &model.Command{ + Command: strings.TrimSpace(props["command"]), + ChannelId: strings.TrimSpace(props["channelId"]), + Suggest: props["suggest"] == "true", + Suggestions: make([]*model.SuggestCommand, 0, 128), + } + + checkCommand(c, command) + + if c.Err != nil { + return + } else { + w.Write([]byte(command.ToJson())) + } +} + +func checkCommand(c *Context, command *model.Command) bool { + + if len(command.Command) == 0 || strings.Index(command.Command, "/") != 0 { + c.Err = model.NewAppError("checkCommand", "Command must start with /", "") + return false + } + + if len(command.ChannelId) > 0 { + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId) + + if !c.HasPermissionsToChannel(cchan, "checkCommand") { + return true + } + } + + for _, v := range commands { + if v(c, command) { + return true + } else if c.Err != nil { + return true + } + } + + return false +} + +func logoutCommand(c *Context, command *model.Command) bool { + + cmd := "/logout" + + if strings.Index(command.Command, cmd) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Logout"}) + + if !command.Suggest { + command.GotoLocation = "/logout" + command.Response = model.RESP_EXECUTED + return true + } + + } else if strings.Index(cmd, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Logout"}) + } + + return false +} + +func echoCommand(c *Context, command *model.Command) bool { + + cmd := "/echo" + + if strings.Index(command.Command, cmd) == 0 { + parts := strings.SplitN(command.Command, " ", 3) + + channelName := "" + if len(parts) >= 2 { + channelName = parts[1] + } + + message := "" + if len(parts) >= 3 { + message = parts[2] + } + + if result := <-Srv.Store.Channel().GetChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { + c.Err = result.Err + return false + } else { + channels := result.Data.(*model.ChannelList) + + for _, v := range channels.Channels { + if v.Type == model.CHANNEL_DIRECT { + continue + } + + if v.Name == channelName && !command.Suggest { + post := &model.Post{} + post.ChannelId = v.Id + post.Message = message + + if _, err := CreateValetPost(c, post); err != nil { + c.Err = err + return false + } + + command.Response = model.RESP_EXECUTED + return true + } + + if len(channelName) == 0 || (strings.Index(v.Name, channelName) == 0 && len(parts) < 3) { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd + " " + v.Name, Description: "Echo a message using Valet in a channel"}) + } + } + } + + } else if strings.Index(cmd, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Echo a message using Valet in a channel"}) + } + + return false +} + +func joinCommand(c *Context, command *model.Command) bool { + + // looks for "/join channel-name" + cmd := "/join" + + if strings.Index(command.Command, cmd) == 0 { + + parts := strings.Split(command.Command, " ") + + startsWith := "" + + if len(parts) == 2 { + startsWith = parts[1] + } + + if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { + c.Err = result.Err + return false + } else { + channels := result.Data.(*model.ChannelList) + + for _, v := range channels.Channels { + + if v.Name == startsWith && !command.Suggest { + + if v.Type == model.CHANNEL_DIRECT { + return false + } + + JoinChannel(c, v.Id, "/command") + + if c.Err != nil { + return false + } + + command.GotoLocation = "/channels/" + v.Name + command.Response = model.RESP_EXECUTED + return true + } + + if len(startsWith) == 0 || strings.Index(v.Name, startsWith) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd + " " + v.Name, Description: "Join the open channel"}) + } + } + } + } else if strings.Index(cmd, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Join an open channel"}) + } + + return false +} + +func loadTestCommand(c *Context, command *model.Command) bool { + cmd := "/loadtest" + + // This command is only available when AllowTesting is true + if !utils.Cfg.ServiceSettings.AllowTesting { + return false + } + + if strings.Index(command.Command, cmd) == 0 { + if loadTestSetupCommand(c, command) { + return true + } + if loadTestUsersCommand(c, command) { + return true + } + if loadTestChannelsCommand(c, command) { + return true + } + if loadTestPostsCommand(c, command) { + return true + } + } else if strings.Index(cmd, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Debug Load Testing"}) + } + + return false +} + +func parseRange(command string, cmd string) (utils.Range, bool) { + tokens := strings.Fields(strings.TrimPrefix(command, cmd)) + var begin int + var end int + var err1 error + var err2 error + switch { + case len(tokens) == 1: + begin, err1 = strconv.Atoi(tokens[0]) + end = begin + if err1 != nil { + return utils.Range{0, 0}, false + } + case len(tokens) >= 2: + begin, err1 = strconv.Atoi(tokens[0]) + end, err2 = strconv.Atoi(tokens[1]) + if err1 != nil || err2 != nil { + return utils.Range{0, 0}, false + } + default: + return utils.Range{0, 0}, false + } + return utils.Range{begin, end}, true +} + +func contains(items []string, token string) bool { + for _, elem := range items { + if elem == token { + return true + } + } + return false +} + +func loadTestSetupCommand(c *Context, command *model.Command) bool { + cmd := "/loadtest setup" + + if strings.Index(command.Command, cmd) == 0 && !command.Suggest { + tokens := strings.Fields(strings.TrimPrefix(command.Command, cmd)) + doTeams := contains(tokens, "teams") + doFuzz := contains(tokens, "fuzz") + + numArgs := 0 + if doTeams { + numArgs++ + } + if doFuzz { + numArgs++ + } + + var numTeams int + var numChannels int + var numUsers int + var numPosts int + + // Defaults + numTeams = 10 + numChannels = 10 + numUsers = 10 + numPosts = 10 + + if doTeams { + if (len(tokens) - numArgs) >= 4 { + numTeams, _ = strconv.Atoi(tokens[numArgs+0]) + numChannels, _ = strconv.Atoi(tokens[numArgs+1]) + numUsers, _ = strconv.Atoi(tokens[numArgs+2]) + numPosts, _ = strconv.Atoi(tokens[numArgs+3]) + } + } else { + if (len(tokens) - numArgs) >= 3 { + numChannels, _ = strconv.Atoi(tokens[numArgs+0]) + numUsers, _ = strconv.Atoi(tokens[numArgs+1]) + numPosts, _ = strconv.Atoi(tokens[numArgs+2]) + } + } + client := model.NewClient(c.TeamUrl + "/api/v1") + + if doTeams { + if err := CreateBasicUser(client); err != nil { + l4g.Error("Failed to create testing enviroment") + return true + } + client.LoginByEmail(BTEST_TEAM_DOMAIN_NAME, BTEST_USER_EMAIL, BTEST_USER_PASSWORD) + enviroment, err := CreateTestEnviromentWithTeams( + client, + utils.Range{numTeams, numTeams}, + utils.Range{numChannels, numChannels}, + utils.Range{numUsers, numUsers}, + utils.Range{numPosts, numPosts}, + doFuzz) + if err != true { + l4g.Error("Failed to create testing enviroment") + return true + } else { + l4g.Info("Testing enviroment created") + for i := 0; i < len(enviroment.Teams); i++ { + l4g.Info("Team Created: " + enviroment.Teams[i].Domain) + l4g.Info("\t User to login: " + enviroment.Enviroments[i].Users[0].Email + ", " + USER_PASSWORD) + } + } + } else { + client.MockSession(c.Session.Id) + CreateTestEnviromentInTeam( + client, + c.Session.TeamId, + utils.Range{numChannels, numChannels}, + utils.Range{numUsers, numUsers}, + utils.Range{numPosts, numPosts}, + doFuzz) + } + return true + } else if strings.Index(cmd, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{ + Suggestion: cmd, + Description: "Creates a testing enviroment in current team. [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>"}) + } + + return false +} + +func loadTestUsersCommand(c *Context, command *model.Command) bool { + cmd1 := "/loadtest users" + cmd2 := "/loadtest users fuzz" + + if strings.Index(command.Command, cmd1) == 0 && !command.Suggest { + cmd := cmd1 + doFuzz := false + if strings.Index(command.Command, cmd2) == 0 { + doFuzz = true + cmd = cmd2 + } + usersr, err := parseRange(command.Command, cmd) + if err == false { + usersr = utils.Range{10, 15} + } + client := model.NewClient(c.TeamUrl + "/api/v1") + userCreator := NewAutoUserCreator(client, c.Session.TeamId) + userCreator.Fuzzy = doFuzz + userCreator.CreateTestUsers(usersr) + return true + } else if strings.Index(cmd1, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd1, Description: "Add a specified number of random users to current team <Min Users> <Max Users>"}) + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add a specified number of random users with fuzz text to current team <Min Users> <Max Users>"}) + } else if strings.Index(cmd2, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add a specified number of random users with fuzz text to current team <Min Users> <Max Users>"}) + } + + return false +} + +func loadTestChannelsCommand(c *Context, command *model.Command) bool { + cmd1 := "/loadtest channels" + cmd2 := "/loadtest channels fuzz" + + if strings.Index(command.Command, cmd1) == 0 && !command.Suggest { + cmd := cmd1 + doFuzz := false + if strings.Index(command.Command, cmd2) == 0 { + doFuzz = true + cmd = cmd2 + } + channelsr, err := parseRange(command.Command, cmd) + if err == false { + channelsr = utils.Range{20, 30} + } + client := model.NewClient(c.TeamUrl + "/api/v1") + client.MockSession(c.Session.Id) + channelCreator := NewAutoChannelCreator(client, c.Session.TeamId) + channelCreator.Fuzzy = doFuzz + channelCreator.CreateTestChannels(channelsr) + return true + } else if strings.Index(cmd1, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd1, Description: "Add a specified number of random channels to current team <MinChannels> <MaxChannels>"}) + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add a specified number of random channels with fuzz text to current team <Min Channels> <Max Channels>"}) + } else if strings.Index(cmd2, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add a specified number of random channels with fuzz text to current team <Min Channels> <Max Channels>"}) + } + + return false +} + +func loadTestPostsCommand(c *Context, command *model.Command) bool { + cmd1 := "/loadtest posts" + cmd2 := "/loadtest posts fuzz" + + if strings.Index(command.Command, cmd1) == 0 && !command.Suggest { + cmd := cmd1 + doFuzz := false + if strings.Index(command.Command, cmd2) == 0 { + cmd = cmd2 + doFuzz = true + } + + postsr, err := parseRange(command.Command, cmd) + if err == false { + postsr = utils.Range{20, 30} + } + + tokens := strings.Fields(strings.TrimPrefix(command.Command, cmd)) + rimages := utils.Range{0, 0} + if len(tokens) >= 3 { + if numImages, err := strconv.Atoi(tokens[2]); err == nil { + rimages = utils.Range{numImages, numImages} + } + } + + var usernames []string + if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err == nil { + profileUsers := result.Data.(map[string]*model.User) + usernames = make([]string, len(profileUsers)) + i := 0 + for _, userprof := range profileUsers { + usernames[i] = userprof.Username + i++ + } + } + + client := model.NewClient(c.TeamUrl + "/api/v1") + client.MockSession(c.Session.Id) + testPoster := NewAutoPostCreator(client, command.ChannelId) + testPoster.Fuzzy = doFuzz + testPoster.Users = usernames + + numImages := utils.RandIntFromRange(rimages) + numPosts := utils.RandIntFromRange(postsr) + for i := 0; i < numPosts; i++ { + testPoster.HasImage = (i < numImages) + testPoster.CreateRandomPost() + } + return true + } else if strings.Index(cmd1, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd1, Description: "Add some random posts to current channel <Min Posts> <Max Posts> <Min Images> <Max Images>"}) + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add some random posts with fuzz text to current channel <Min Posts> <Max Posts> <Min Images> <Max Images>"}) + } else if strings.Index(cmd2, command.Command) == 0 { + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add some random posts with fuzz text to current channel <Min Posts> <Max Posts> <Min Images> <Max Images>"}) + } + + return false +} diff --git a/api/command_test.go b/api/command_test.go new file mode 100644 index 000000000..d3b0da455 --- /dev/null +++ b/api/command_test.go @@ -0,0 +1,146 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +func TestSuggestRootCommands(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + if _, err := Client.Command("", "", true); err == nil { + t.Fatal("Should fail") + } + + rs1 := Client.Must(Client.Command("", "/", true)).Data.(*model.Command) + + hasLogout := false + for _, v := range rs1.Suggestions { + if v.Suggestion == "/logout" { + hasLogout = true + } + } + + if !hasLogout { + t.Log(rs1.Suggestions) + t.Fatal("should have logout cmd") + } + + rs2 := Client.Must(Client.Command("", "/log", true)).Data.(*model.Command) + + if rs2.Suggestions[0].Suggestion != "/logout" { + t.Fatal("should have logout cmd") + } + + rs3 := Client.Must(Client.Command("", "/joi", true)).Data.(*model.Command) + + if rs3.Suggestions[0].Suggestion != "/join" { + t.Fatal("should have join cmd") + } +} + +func TestLogoutCommands(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + rs1 := Client.Must(Client.Command("", "/logout", false)).Data.(*model.Command) + if rs1.GotoLocation != "/logout" { + t.Fatal("failed to logout") + } +} + +func TestJoinCommands(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + Client.Must(Client.LeaveChannel(channel1.Id)) + + channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + Client.Must(Client.LeaveChannel(channel2.Id)) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + data := make(map[string]string) + data["user_id"] = user2.Id + channel3 := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel) + + rs1 := Client.Must(Client.Command("", "/join aa", true)).Data.(*model.Command) + if rs1.Suggestions[0].Suggestion != "/join "+channel1.Name { + t.Fatal("should have join cmd") + } + + rs2 := Client.Must(Client.Command("", "/join bb", true)).Data.(*model.Command) + if rs2.Suggestions[0].Suggestion != "/join "+channel2.Name { + t.Fatal("should have join cmd") + } + + rs3 := Client.Must(Client.Command("", "/join", true)).Data.(*model.Command) + if len(rs3.Suggestions) != 2 { + t.Fatal("should have 2 join cmd") + } + + rs4 := Client.Must(Client.Command("", "/join ", true)).Data.(*model.Command) + if len(rs4.Suggestions) != 2 { + t.Fatal("should have 2 join cmd") + } + + rs5 := Client.Must(Client.Command("", "/join "+channel2.Name, false)).Data.(*model.Command) + if rs5.GotoLocation != "/channels/"+channel2.Name { + t.Fatal("failed to join channel") + } + + rs6 := Client.Must(Client.Command("", "/join "+channel3.Name, false)).Data.(*model.Command) + if rs6.GotoLocation == "/channels/"+channel3.Name { + t.Fatal("should not have joined direct message channel") + } + + c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList) + + if len(c1.Channels) != 3 { // 3 because of town-square and direct + t.Fatal("didn't join channel") + } + + found := false + for _, c := range c1.Channels { + if c.Name == channel2.Name { + found = true + break + } + } + if !found { + t.Fatal("didn't join channel") + } +} diff --git a/api/context.go b/api/context.go new file mode 100644 index 000000000..16105c8af --- /dev/null +++ b/api/context.go @@ -0,0 +1,375 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "net" + "net/http" + "net/url" + "strings" +) + +var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) + +type Context struct { + Session model.Session + RequestId string + IpAddress string + TeamUrl string + Path string + Err *model.AppError +} + +type Page struct { + TemplateName string + Title string + SiteName string + FeedbackEmail string + TeamUrl string + Props map[string]string +} + +func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{h, false, false, true, false} +} + +func AppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{h, false, false, false, false} +} + +func ApiUserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{h, true, false, true, true} +} + +func ApiUserRequiredActivity(h func(*Context, http.ResponseWriter, *http.Request), isUserActivity bool) http.Handler { + return &handler{h, true, false, true, isUserActivity} +} + +func UserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{h, true, false, false, false} +} + +func ApiAdminSystemRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{h, true, true, true, false} +} + +type handler struct { + handleFunc func(*Context, http.ResponseWriter, *http.Request) + requireUser bool + requireSystemAdmin bool + isApi bool + isUserActivity bool +} + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + + l4g.Debug("%v", r.URL.Path) + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = GetIpAddress(r) + c.Path = r.URL.Path + + protocol := "http" + + // if the request came from the ELB then assume this is produciton + // and redirect all http requests to https + if utils.Cfg.ServiceSettings.UseSSL { + forwardProto := r.Header.Get(model.HEADER_FORWARDED_PROTO) + if forwardProto == "http" { + l4g.Info("redirecting http request to https for %v", r.URL.Path) + http.Redirect(w, r, "https://"+r.Host, http.StatusTemporaryRedirect) + } else { + protocol = "https" + } + } + + c.TeamUrl = protocol + "://" + r.Host + + w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) + w.Header().Set(model.HEADER_VERSION_ID, utils.Cfg.ServiceSettings.Version) + + sessionId := "" + + // attempt to parse the session token from the header + if ah := r.Header.Get(model.HEADER_AUTH); ah != "" { + if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" { + sessionId = ah[7:] + } + } + + // attempt to parse the session token from the cookie + if sessionId == "" { + if cookie, err := r.Cookie(model.SESSION_TOKEN); err == nil { + sessionId = cookie.Value + } + } + + if sessionId != "" { + + var session *model.Session + if ts, ok := sessionCache.Get(sessionId); ok { + session = ts.(*model.Session) + } + + if session == nil { + if sessionResult := <-Srv.Store.Session().Get(sessionId); sessionResult.Err != nil { + c.LogError(model.NewAppError("ServeHTTP", "Invalid session", "id="+sessionId+", err="+sessionResult.Err.DetailedError)) + } else { + session = sessionResult.Data.(*model.Session) + } + } + + if session == nil || session.IsExpired() { + c.RemoveSessionCookie(w) + c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "id="+sessionId) + c.Err.StatusCode = http.StatusUnauthorized + } else { + c.Session = *session + } + } + + if c.Err == nil && h.requireUser { + c.UserRequired() + } + + if c.Err == nil && h.requireSystemAdmin { + c.SystemAdminRequired() + } + + if c.Err == nil && h.isUserActivity && sessionId != "" && len(c.Session.UserId) > 0 { + go func() { + if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, sessionId, model.GetMillis())).Err; err != nil { + l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", c.Session.UserId, sessionId, err) + } + }() + } + + if c.Err == nil { + h.handleFunc(c, w, r) + } + + if c.Err != nil { + c.Err.RequestId = c.RequestId + c.LogError(c.Err) + c.Err.Where = r.URL.Path + + if h.isApi { + w.WriteHeader(c.Err.StatusCode) + w.Write([]byte(c.Err.ToJson())) + } else { + if c.Err.StatusCode == http.StatusUnauthorized { + http.Redirect(w, r, "/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) + } else { + RenderWebError(c.Err, w, r) + } + } + } +} + +func (c *Context) LogAudit(extraInfo string) { + audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.AltId} + Srv.Store.Audit().Save(audit) +} + +func (c *Context) LogAuditWithUserId(userId, extraInfo string) { + + if len(c.Session.UserId) > 0 { + extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.Session.UserId) + } + + audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.AltId} + Srv.Store.Audit().Save(audit) +} + +func (c *Context) LogError(err *model.AppError) { + l4g.Error("%v:%v code=%v rid=%v uid=%v ip=%v %v [details: %v]", c.Path, err.Where, err.StatusCode, + c.RequestId, c.Session.UserId, c.IpAddress, err.Message, err.DetailedError) +} + +func (c *Context) UserRequired() { + if len(c.Session.UserId) == 0 { + c.Err = model.NewAppError("", "Invalid or expired session, please login again.", "UserRequired") + c.Err.StatusCode = http.StatusUnauthorized + return + } +} + +func (c *Context) SystemAdminRequired() { + if len(c.Session.UserId) == 0 { + c.Err = model.NewAppError("", "Invalid or expired session, please login again.", "SystemAdminRequired") + c.Err.StatusCode = http.StatusUnauthorized + return + } else if !c.IsSystemAdmin() { + c.Err = model.NewAppError("", "You do not have the appropriate permissions", "AdminRequired") + c.Err.StatusCode = http.StatusForbidden + return + } +} + +func (c *Context) HasPermissionsToUser(userId string, where string) bool { + + // You are the user + if c.Session.UserId == userId { + return true + } + + // You're a mattermost system admin and you're on the VPN + if c.IsSystemAdmin() { + return true + } + + c.Err = model.NewAppError(where, "You do not have the appropriate permissions", "userId="+userId) + c.Err.StatusCode = http.StatusForbidden + return false +} + +func (c *Context) HasPermissionsToTeam(teamId string, where string) bool { + if c.Session.TeamId == teamId { + return true + } + + // You're a mattermost system admin and you're on the VPN + if c.IsSystemAdmin() { + return true + } + + c.Err = model.NewAppError(where, "You do not have the appropriate permissions", "userId="+c.Session.UserId+", teamId="+teamId) + c.Err.StatusCode = http.StatusForbidden + return false +} + +func (c *Context) HasPermissionsToChannel(sc store.StoreChannel, where string) bool { + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err + return false + } else if cresult.Data.(int64) != 1 { + c.Err = model.NewAppError(where, "You do not have the appropriate permissions", "userId="+c.Session.UserId) + c.Err.StatusCode = http.StatusForbidden + return false + } + + return true +} + +func (c *Context) IsSystemAdmin() bool { + if strings.Contains(c.Session.Roles, model.ROLE_SYSTEM_ADMIN) && IsPrivateIpAddress(c.IpAddress) { + return true + } + return false +} + +func (c *Context) RemoveSessionCookie(w http.ResponseWriter) { + + sessionCache.Remove(c.Session.Id) + + cookie := &http.Cookie{ + Name: model.SESSION_TOKEN, + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + } + + http.SetCookie(w, cookie) +} + +func (c *Context) SetInvalidParam(where string, name string) { + c.Err = model.NewAppError(where, "Invalid "+name+" parameter", "") + c.Err.StatusCode = http.StatusBadRequest +} + +func (c *Context) SetUnknownError(where string, details string) { + c.Err = model.NewAppError(where, "An unknown error has occured. Please contact support.", details) +} + +func GetIpAddress(r *http.Request) string { + address := r.Header.Get(model.HEADER_FORWARDED) + if len(address) == 0 { + address, _, _ = net.SplitHostPort(r.RemoteAddr) + } + + return address +} + +func IsTestDomain(r *http.Request) bool { + + if strings.Index(r.Host, "localhost") == 0 { + return true + } + + if strings.Index(r.Host, "test") == 0 { + return true + } + + if strings.Index(r.Host, "127.0.") == 0 { + return true + } + + if strings.Index(r.Host, "192.168.") == 0 { + return true + } + + if strings.Index(r.Host, "10.") == 0 { + return true + } + + if strings.Index(r.Host, "176.") == 0 { + return true + } + + return false +} + +func IsBetaDomain(r *http.Request) bool { + + if strings.Index(r.Host, "beta") == 0 { + return true + } + + if strings.Index(r.Host, "ci") == 0 { + return true + } + + return false +} + +var privateIpAddress = []*net.IPNet{ + &net.IPNet{IP: net.IPv4(10, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)}, + &net.IPNet{IP: net.IPv4(176, 16, 0, 1), Mask: net.IPv4Mask(255, 255, 0, 0)}, + &net.IPNet{IP: net.IPv4(192, 168, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 0)}, + &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 252)}, +} + +func IsPrivateIpAddress(ipAddress string) bool { + + for _, pips := range privateIpAddress { + if pips.Contains(net.ParseIP(ipAddress)) { + return true + } + } + + return false +} + +func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { + m := make(map[string]string) + m["Message"] = err.Message + m["Details"] = err.DetailedError + m["SiteName"] = utils.Cfg.ServiceSettings.SiteName + + w.WriteHeader(err.StatusCode) + ServerTemplates.ExecuteTemplate(w, "error.html", m) +} + +func Handle404(w http.ResponseWriter, r *http.Request) { + err := model.NewAppError("Handle404", "Sorry, we could not find the page.", "") + err.StatusCode = http.StatusNotFound + l4g.Error("%v: code=404 ip=%v", r.URL.Path, GetIpAddress(r)) + RenderWebError(err, w, r) +} diff --git a/api/context_test.go b/api/context_test.go new file mode 100644 index 000000000..56ccce1ee --- /dev/null +++ b/api/context_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "testing" +) + +var ipAddressTests = []struct { + address string + expected bool +}{ + {"126.255.255.255", false}, + {"127.0.0.1", true}, + {"127.0.0.4", false}, + {"9.255.255.255", false}, + {"10.0.0.1", true}, + {"11.0.0.1", false}, + {"176.15.155.255", false}, + {"176.16.0.1", true}, + {"176.31.0.1", false}, + {"192.167.255.255", false}, + {"192.168.0.1", true}, + {"192.169.0.1", false}, +} + +func TestIpAddress(t *testing.T) { + for _, v := range ipAddressTests { + if IsPrivateIpAddress(v.address) != v.expected { + t.Errorf("expect %v as %v", v.address, v.expected) + } + } +} + +func TestContext(t *testing.T) { + context := Context{} + + context.IpAddress = "127.0.0.1" + context.Session.UserId = "5" + + if !context.HasPermissionsToUser("5", "") { + t.Fatal("should have permissions") + } + + if context.HasPermissionsToUser("6", "") { + t.Fatal("shouldn't have permissions") + } + + context.Session.Roles = model.ROLE_SYSTEM_ADMIN + if !context.HasPermissionsToUser("6", "") { + t.Fatal("should have permissions") + } + + context.IpAddress = "125.0.0.1" + if context.HasPermissionsToUser("6", "") { + t.Fatal("shouldn't have permissions") + } +} diff --git a/api/file.go b/api/file.go new file mode 100644 index 000000000..c7c3b7b3e --- /dev/null +++ b/api/file.go @@ -0,0 +1,375 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "bytes" + l4g "code.google.com/p/log4go" + "fmt" + "github.com/goamz/goamz/aws" + "github.com/goamz/goamz/s3" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "github.com/nfnt/resize" + "image" + _ "image/gif" + "image/jpeg" + "io" + "net/http" + "net/url" + "path/filepath" + "strconv" + "strings" + "time" +) + +func InitFile(r *mux.Router) { + l4g.Debug("Initializing post api routes") + + sr := r.PathPrefix("/files").Subrouter() + sr.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST") + sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+\\.[A-Za-z0-9]{3,}}", ApiAppHandler(getFile)).Methods("GET") + sr.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST") +} + +func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.IsS3Configured() { + c.Err = model.NewAppError("uploadFile", "Unable to upload file. Amazon S3 not configured. ", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + err := r.ParseMultipartForm(model.MAX_FILE_SIZE) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + m := r.MultipartForm + + props := m.Value + + if len(props["channel_id"]) == 0 { + c.SetInvalidParam("uploadFile", "channel_id") + return + } + channelId := props["channel_id"][0] + if len(channelId) == 0 { + c.SetInvalidParam("uploadFile", "channel_id") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + + files := m.File["files"] + + resStruct := &model.FileUploadResponse{ + Filenames: []string{}} + + imageNameList := []string{} + imageDataList := [][]byte{} + + if !c.HasPermissionsToChannel(cchan, "uploadFile") { + return + } + + for i, _ := range files { + file, err := files[i].Open() + defer file.Close() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + buf := bytes.NewBuffer(nil) + io.Copy(buf, file) + + ext := filepath.Ext(files[i].Filename) + + uid := model.NewId() + + path := "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + c.Session.UserId + "/" + uid + "/" + files[i].Filename + + if model.IsFileExtImage(ext) { + options := s3.Options{} + err = bucket.Put(path, buf.Bytes(), model.GetImageMimeType(ext), s3.Private, options) + imageNameList = append(imageNameList, uid+"/"+files[i].Filename) + imageDataList = append(imageDataList, buf.Bytes()) + } else { + options := s3.Options{} + err = bucket.Put(path, buf.Bytes(), "binary/octet-stream", s3.Private, options) + } + + if err != nil { + c.Err = model.NewAppError("uploadFile", "Unable to upload file. ", err.Error()) + return + } + + fileUrl := c.TeamUrl + "/api/v1/files/get/" + channelId + "/" + c.Session.UserId + "/" + uid + "/" + files[i].Filename + resStruct.Filenames = append(resStruct.Filenames, fileUrl) + } + + fireAndForgetHandleImages(imageNameList, imageDataList, c.Session.TeamId, channelId, c.Session.UserId) + + w.Write([]byte(resStruct.ToJson())) +} + +func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, channelId, userId string) { + + go func() { + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + dest := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + + for i, filename := range filenames { + name := filename[:strings.LastIndex(filename, ".")] + go func() { + // Decode image bytes into Image object + img, _, err := image.Decode(bytes.NewReader(fileData[i])) + if err != nil { + l4g.Error("Unable to decode image channelId=%v userId=%v filename=%v err=%v", channelId, userId, filename, err) + return + } + + // Decode image config + imgConfig, _, err := image.DecodeConfig(bytes.NewReader(fileData[i])) + if err != nil { + l4g.Error("Unable to decode image config channelId=%v userId=%v filename=%v err=%v", channelId, userId, filename, err) + return + } + + // Create thumbnail + go func() { + var thumbnail image.Image + if imgConfig.Width > int(utils.Cfg.ImageSettings.ThumbnailWidth) { + thumbnail = resize.Resize(utils.Cfg.ImageSettings.ThumbnailWidth, utils.Cfg.ImageSettings.ThumbnailHeight, img, resize.NearestNeighbor) + } else { + thumbnail = img + } + + buf := new(bytes.Buffer) + err = jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90}) + if err != nil { + l4g.Error("Unable to encode image as jpeg channelId=%v userId=%v filename=%v err=%v", channelId, userId, filename, err) + return + } + + // Upload thumbnail to S3 + options := s3.Options{} + err = bucket.Put(dest+name+"_thumb.jpg", buf.Bytes(), "image/jpeg", s3.Private, options) + if err != nil { + l4g.Error("Unable to upload thumbnail to S3 channelId=%v userId=%v filename=%v err=%v", channelId, userId, filename, err) + return + } + }() + + // Create preview + go func() { + var preview image.Image + if imgConfig.Width > int(utils.Cfg.ImageSettings.PreviewWidth) { + preview = resize.Resize(utils.Cfg.ImageSettings.PreviewWidth, utils.Cfg.ImageSettings.PreviewHeight, img, resize.NearestNeighbor) + } else { + preview = img + } + + buf := new(bytes.Buffer) + err = jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}) + + //err = png.Encode(buf, preview) + if err != nil { + l4g.Error("Unable to encode image as preview jpg channelId=%v userId=%v filename=%v err=%v", channelId, userId, filename, err) + return + } + + // Upload preview to S3 + options := s3.Options{} + err = bucket.Put(dest+name+"_preview.jpg", buf.Bytes(), "image/jpeg", s3.Private, options) + if err != nil { + l4g.Error("Unable to upload preview to S3 channelId=%v userId=%v filename=%v err=%v", channelId, userId, filename, err) + return + } + }() + }() + } + }() +} + +type ImageGetResult struct { + Error error + ImageData []byte +} + +func getFile(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.IsS3Configured() { + c.Err = model.NewAppError("getFile", "Unable to get file. Amazon S3 not configured. ", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + params := mux.Vars(r) + + channelId := params["channel_id"] + if len(channelId) != 26 { + c.SetInvalidParam("getFile", "channel_id") + return + } + + userId := params["user_id"] + if len(userId) != 26 { + c.SetInvalidParam("getFile", "user_id") + return + } + + filename := params["filename"] + if len(filename) == 0 { + c.SetInvalidParam("getFile", "filename") + return + } + + hash := r.URL.Query().Get("h") + data := r.URL.Query().Get("d") + teamId := r.URL.Query().Get("t") + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + path := "" + if len(teamId) == 26 { + path = "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + filename + } else { + path = "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + userId + "/" + filename + } + + fileData := make(chan []byte) + asyncGetFile(bucket, path, fileData) + + if len(hash) > 0 && len(data) > 0 && len(teamId) == 26 { + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) { + c.Err = model.NewAppError("getFile", "The public link does not appear to be valid", "") + return + } + props := model.MapFromJson(strings.NewReader(data)) + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60*24*7 { // one week + c.Err = model.NewAppError("getFile", "The public link has expired", "") + return + } + } else if !c.HasPermissionsToChannel(cchan, "getFile") { + return + } + + f := <-fileData + + if f == nil { + var f2 []byte + tries := 0 + for { + time.Sleep(3000 * time.Millisecond) + tries++ + + asyncGetFile(bucket, path, fileData) + f2 = <-fileData + + if f2 != nil { + w.Header().Set("Cache-Control", "max-age=2592000, public") + w.Header().Set("Content-Length", strconv.Itoa(len(f2))) + w.Write(f2) + return + } else if tries >= 2 { + break + } + } + + c.Err = model.NewAppError("getFile", "Could not find file.", "url extenstion: "+path) + c.Err.StatusCode = http.StatusNotFound + return + } + + w.Header().Set("Cache-Control", "max-age=2592000, public") + w.Header().Set("Content-Length", strconv.Itoa(len(f))) + w.Write(f) +} + +func asyncGetFile(bucket *s3.Bucket, path string, fileData chan []byte) { + go func() { + data, getErr := bucket.Get(path) + if getErr != nil { + fileData <- nil + } else { + fileData <- data + } + }() +} + +func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.TeamSettings.AllowPublicLink { + c.Err = model.NewAppError("getPublicLink", "Public links have been disabled", "") + c.Err.StatusCode = http.StatusForbidden + } + + if !utils.IsS3Configured() { + c.Err = model.NewAppError("getPublicLink", "Unable to get link. Amazon S3 not configured. ", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + props := model.MapFromJson(r.Body) + + filename := props["filename"] + if len(filename) == 0 { + c.SetInvalidParam("getPublicLink", "filename") + return + } + + matches := model.PartialUrlRegex.FindAllStringSubmatch(filename, -1) + if len(matches) == 0 || len(matches[0]) < 5 { + c.SetInvalidParam("getPublicLink", "filename") + return + } + + getType := matches[0][1] + channelId := matches[0][2] + userId := matches[0][3] + filename = matches[0][4] + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + + newProps := make(map[string]string) + newProps["filename"] = filename + newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) + + data := model.MapToJson(newProps) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) + + url := fmt.Sprintf("%s/api/v1/files/%s/%s/%s/%s?d=%s&h=%s&t=%s", c.TeamUrl, getType, channelId, userId, filename, url.QueryEscape(data), url.QueryEscape(hash), c.Session.TeamId) + + if !c.HasPermissionsToChannel(cchan, "getPublicLink") { + return + } + + rData := make(map[string]string) + rData["public_link"] = url + + w.Write([]byte(model.MapToJson(rData))) +} diff --git a/api/file_benchmark_test.go b/api/file_benchmark_test.go new file mode 100644 index 000000000..251ff7793 --- /dev/null +++ b/api/file_benchmark_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "fmt" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/url" + "testing" + "time" +) + +func BenchmarkUploadFile(b *testing.B) { + _, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + testPoster.UploadTestFile() + } +} + +func BenchmarkGetFile(b *testing.B) { + team, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + filenames, err := testPoster.UploadTestFile() + if err == false { + b.Fatal("Unable to upload file for benchmark") + } + + newProps := make(map[string]string) + newProps["filename"] = filenames[0] + newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) + + data := model.MapToJson(newProps) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) + + // wait a bit for files to ready + time.Sleep(5 * time.Second) + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&h="+url.QueryEscape(hash)+"&t="+team.Id, true); downErr != nil { + b.Fatal(downErr) + } + } +} + +func BenchmarkGetPublicLink(b *testing.B) { + _, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + filenames, err := testPoster.UploadTestFile() + if err == false { + b.Fatal("Unable to upload file for benchmark") + } + + data := make(map[string]string) + data["filename"] = filenames[0] + + // wait a bit for files to ready + time.Sleep(5 * time.Second) + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, downErr := Client.GetPublicLink(data); downErr != nil { + b.Fatal(downErr) + } + } +} diff --git a/api/file_test.go b/api/file_test.go new file mode 100644 index 000000000..6fd231165 --- /dev/null +++ b/api/file_test.go @@ -0,0 +1,379 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "bytes" + "fmt" + "github.com/goamz/goamz/aws" + "github.com/goamz/goamz/s3" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "io" + "mime/multipart" + "net/http" + "net/url" + "os" + "strings" + "testing" + "time" +) + +func TestUploadFile(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("files", "test.png") + if err != nil { + t.Fatal(err) + } + + path := utils.FindDir("web/static/images") + file, err := os.Open(path + "/test.png") + defer file.Close() + + _, err = io.Copy(part, file) + if err != nil { + t.Fatal(err) + } + + field, err := writer.CreateFormField("channel_id") + if err != nil { + t.Fatal(err) + } + + _, err = field.Write([]byte(channel1.Id)) + if err != nil { + t.Fatal(err) + } + + err = writer.Close() + if err != nil { + t.Fatal(err) + } + + resp, appErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) + if utils.IsS3Configured() { + if appErr != nil { + t.Fatal(appErr) + } + + filenames := resp.Data.(*model.FileUploadResponse).Filenames + + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + fileId := strings.Split(filenames[0], ".")[0] + + // wait a bit for files to ready + time.Sleep(5 * time.Second) + + err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filenames[0]) + if err != nil { + t.Fatal(err) + } + + err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg") + if err != nil { + t.Fatal(err) + } + + err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.png") + if err != nil { + t.Fatal(err) + } + } else { + if appErr == nil { + t.Fatal("S3 not configured, should have failed") + } + } +} + +func TestGetFile(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + if utils.IsS3Configured() { + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("files", "test.png") + if err != nil { + t.Fatal(err) + } + + path := utils.FindDir("web/static/images") + file, err := os.Open(path + "/test.png") + if err != nil { + t.Fatal(err) + } + defer file.Close() + + _, err = io.Copy(part, file) + if err != nil { + t.Fatal(err) + } + + field, err := writer.CreateFormField("channel_id") + if err != nil { + t.Fatal(err) + } + + _, err = field.Write([]byte(channel1.Id)) + if err != nil { + t.Fatal(err) + } + + err = writer.Close() + if err != nil { + t.Fatal(err) + } + + resp, upErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) + if upErr != nil { + t.Fatal(upErr) + } + + filenames := resp.Data.(*model.FileUploadResponse).Filenames + + // wait a bit for files to ready + time.Sleep(5 * time.Second) + + if _, downErr := Client.GetFile(filenames[0], true); downErr != nil { + t.Fatal("file get failed") + } + + team2 := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user2 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + newProps := make(map[string]string) + newProps["filename"] = filenames[0] + newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) + + data := model.MapToJson(newProps) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.PublicLinkSalt)) + + Client.LoginByEmail(team2.Domain, user2.Email, "pwd") + + if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&h="+url.QueryEscape(hash)+"&t="+team.Id, true); downErr != nil { + t.Fatal(downErr) + } + + if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&h="+url.QueryEscape(hash), true); downErr == nil { + t.Fatal("Should have errored - missing team id") + } + + if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&h="+url.QueryEscape(hash)+"&t=junk", true); downErr == nil { + t.Fatal("Should have errored - bad team id") + } + + if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&h="+url.QueryEscape(hash)+"&t=12345678901234567890123456", true); downErr == nil { + t.Fatal("Should have errored - bad team id") + } + + if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&t="+team.Id, true); downErr == nil { + t.Fatal("Should have errored - missing hash") + } + + if _, downErr := Client.GetFile(filenames[0]+"?d="+url.QueryEscape(data)+"&h=junk&t="+team.Id, true); downErr == nil { + t.Fatal("Should have errored - bad hash") + } + + if _, downErr := Client.GetFile(filenames[0]+"?h="+url.QueryEscape(hash)+"&t="+team.Id, true); downErr == nil { + t.Fatal("Should have errored - missing data") + } + + if _, downErr := Client.GetFile(filenames[0]+"?d=junk&h="+url.QueryEscape(hash)+"&t="+team.Id, true); downErr == nil { + t.Fatal("Should have errored - bad data") + } + + if _, downErr := Client.GetFile(filenames[0], true); downErr == nil { + t.Fatal("Should have errored - user not logged in and link not public") + } + + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + fileId := strings.Split(filenames[0], ".")[0] + + err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + filenames[0]) + if err != nil { + t.Fatal(err) + } + + err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_thumb.jpg") + if err != nil { + t.Fatal(err) + } + + err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + user1.Id + "/" + fileId + "_preview.png") + if err != nil { + t.Fatal(err) + } + } else { + if _, downErr := Client.GetFile("/files/get/yxebdmbz5pgupx7q6ez88rw11a/n3btzxu9hbnapqk36iwaxkjxhc/junk.jpg", false); downErr.StatusCode != http.StatusNotImplemented { + t.Fatal("Status code should have been 501 - Not Implemented") + } + } +} + +func TestGetPublicLink(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + if utils.IsS3Configured() { + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("files", "test.png") + if err != nil { + t.Fatal(err) + } + + path := utils.FindDir("web/static/images") + file, err := os.Open(path + "/test.png") + if err != nil { + t.Fatal(err) + } + defer file.Close() + + _, err = io.Copy(part, file) + if err != nil { + t.Fatal(err) + } + + field, err := writer.CreateFormField("channel_id") + if err != nil { + t.Fatal(err) + } + + _, err = field.Write([]byte(channel1.Id)) + if err != nil { + t.Fatal(err) + } + + err = writer.Close() + if err != nil { + t.Fatal(err) + } + + resp, upErr := Client.UploadFile("/files/upload", body.Bytes(), writer.FormDataContentType()) + if upErr != nil { + t.Fatal(upErr) + } + + filenames := resp.Data.(*model.FileUploadResponse).Filenames + + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", Filenames: filenames} + + rpost1, postErr := Client.CreatePost(post1) + if postErr != nil { + t.Fatal(postErr) + } + + if rpost1.Data.(*model.Post).Filenames[0] != filenames[0] { + t.Fatal("filenames don't match") + } + + // wait a bit for files to ready + time.Sleep(5 * time.Second) + + data := make(map[string]string) + data["filename"] = filenames[0] + + if _, err := Client.GetPublicLink(data); err != nil { + t.Fatal(err) + } + + data["filename"] = "junk" + + if _, err := Client.GetPublicLink(data); err == nil { + t.Fatal("Should have errored - bad file path") + } + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + data["filename"] = filenames[0] + if _, err := Client.GetPublicLink(data); err == nil { + t.Fatal("should have errored, user not member of channel") + } + + // perform clean-up on s3 + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + fileId := strings.Split(filenames[0], ".")[0] + + if err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + rpost1.Data.(*model.Post).UserId + "/" + filenames[0]); err != nil { + t.Fatal(err) + } + + if err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + rpost1.Data.(*model.Post).UserId + "/" + fileId + "_thumb.jpg"); err != nil { + t.Fatal(err) + } + + if err = bucket.Del("teams/" + team.Id + "/channels/" + channel1.Id + "/users/" + rpost1.Data.(*model.Post).UserId + "/" + fileId + "_preview.png"); err != nil { + t.Fatal(err) + } + } else { + data := make(map[string]string) + if _, err := Client.GetPublicLink(data); err.StatusCode != http.StatusNotImplemented { + t.Fatal("Status code should have been 501 - Not Implemented") + } + } +} diff --git a/api/post.go b/api/post.go new file mode 100644 index 000000000..25a68304d --- /dev/null +++ b/api/post.go @@ -0,0 +1,692 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "fmt" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "net/http" + "strconv" + "strings" + "time" +) + +func InitPost(r *mux.Router) { + l4g.Debug("Initializing post api routes") + + r.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("GET") + + sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter() + sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST") + sr.Handle("/valet_create", ApiUserRequired(createValetPost)).Methods("POST") + sr.Handle("/update", ApiUserRequired(updatePost)).Methods("POST") + sr.Handle("/posts/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET") + sr.Handle("/post/{post_id:[A-Za-z0-9]+}", ApiUserRequired(getPost)).Methods("GET") + sr.Handle("/post/{post_id:[A-Za-z0-9]+}/delete", ApiUserRequired(deletePost)).Methods("POST") +} + +func createPost(c *Context, w http.ResponseWriter, r *http.Request) { + post := model.PostFromJson(r.Body) + if post == nil { + c.SetInvalidParam("createPost", "post") + return + } + + // Create and save post object to channel + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId) + + if !c.HasPermissionsToChannel(cchan, "createPost") { + return + } + + if rp, err := CreatePost(c, post, true); err != nil { + c.Err = err + + if strings.Contains(c.Err.Message, "parameter") { + c.Err.StatusCode = http.StatusBadRequest + } + + return + } else { + w.Write([]byte(rp.ToJson())) + } +} + +func createValetPost(c *Context, w http.ResponseWriter, r *http.Request) { + post := model.PostFromJson(r.Body) + if post == nil { + c.SetInvalidParam("createValetPost", "post") + return + } + + // Any one with access to the team can post as valet to any open channel + cchan := Srv.Store.Channel().CheckOpenChannelPermissions(c.Session.TeamId, post.ChannelId) + + if !c.HasPermissionsToChannel(cchan, "createValetPost") { + return + } + + if rp, err := CreateValetPost(c, post); err != nil { + c.Err = err + + if strings.Contains(c.Err.Message, "parameter") { + c.Err.StatusCode = http.StatusBadRequest + } + + return + } else { + w.Write([]byte(rp.ToJson())) + } +} + +func CreateValetPost(c *Context, post *model.Post) (*model.Post, *model.AppError) { + post.Hashtags, _ = model.ParseHashtags(post.Message) + + post.Filenames = []string{} // no files allowed in valet posts yet + + if result := <-Srv.Store.User().GetByUsername(c.Session.TeamId, "valet"); result.Err != nil { + // if the bot doesn't exist, create it + if tresult := <-Srv.Store.Team().Get(c.Session.TeamId); tresult.Err != nil { + return nil, tresult.Err + } else { + post.UserId = (CreateValet(c, tresult.Data.(*model.Team))).Id + } + } else { + post.UserId = result.Data.(*model.User).Id + } + + var rpost *model.Post + if result := <-Srv.Store.Post().Save(post); result.Err != nil { + return nil, result.Err + } else { + rpost = result.Data.(*model.Post) + } + + fireAndForgetNotifications(rpost, c.Session.TeamId, c.TeamUrl) + + return rpost, nil +} + +func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.Post, *model.AppError) { + var pchan store.StoreChannel + if len(post.RootId) > 0 { + pchan = Srv.Store.Post().Get(post.RootId) + } + + // Verify the parent/child relationships are correct + if pchan != nil { + if presult := <-pchan; presult.Err != nil { + return nil, model.NewAppError("createPost", "Invalid RootId parameter", "") + } else { + list := presult.Data.(*model.PostList) + if len(list.Posts) == 0 || !list.IsChannelId(post.ChannelId) { + return nil, model.NewAppError("createPost", "Invalid ChannelId for RootId parameter", "") + } + + if post.ParentId == "" { + post.ParentId = post.RootId + } + + if post.RootId != post.ParentId { + parent := list.Posts[post.ParentId] + if parent == nil { + return nil, model.NewAppError("createPost", "Invalid ParentId parameter", "") + } + } + } + } + + post.Hashtags, _ = model.ParseHashtags(post.Message) + + post.UserId = c.Session.UserId + + if len(post.Filenames) > 0 { + doRemove := false + for i := len(post.Filenames) - 1; i >= 0; i-- { + path := post.Filenames[i] + + doRemove = false + if model.UrlRegex.MatchString(path) { + continue + } else if model.PartialUrlRegex.MatchString(path) { + matches := model.PartialUrlRegex.FindAllStringSubmatch(path, -1) + if len(matches) == 0 || len(matches[0]) < 5 { + doRemove = true + } + + channelId := matches[0][2] + if channelId != post.ChannelId { + doRemove = true + } + + userId := matches[0][3] + if userId != post.UserId { + doRemove = true + } + } else { + doRemove = true + } + if doRemove { + l4g.Error("Bad filename discarded, filename=%v", path) + post.Filenames = append(post.Filenames[:i], post.Filenames[i+1:]...) + } + } + } + + var rpost *model.Post + if result := <-Srv.Store.Post().Save(post); result.Err != nil { + return nil, result.Err + } else if doUpdateLastViewed && (<-Srv.Store.Channel().UpdateLastViewedAt(post.ChannelId, c.Session.UserId)).Err != nil { + return nil, result.Err + } else { + rpost = result.Data.(*model.Post) + + fireAndForgetNotifications(rpost, c.Session.TeamId, c.TeamUrl) + + } + + return rpost, nil +} + +func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) { + + go func() { + // Get a list of user names (to be used as keywords) and ids for the given team + uchan := Srv.Store.User().GetProfiles(teamId) + echan := Srv.Store.Channel().GetMembers(post.ChannelId) + cchan := Srv.Store.Channel().Get(post.ChannelId) + tchan := Srv.Store.Team().Get(teamId) + + var channel *model.Channel + var channelName string + var bodyText string + var subjectText string + if result := <-cchan; result.Err != nil { + l4g.Error("Failed to retrieve channel channel_id=%v, err=%v", post.ChannelId, result.Err) + return + } else { + channel = result.Data.(*model.Channel) + if channel.Type == model.CHANNEL_DIRECT { + bodyText = "You have one new message." + subjectText = "New Direct Message" + } else { + bodyText = "You have one new mention." + subjectText = "New Mention" + channelName = channel.DisplayName + } + } + + var mentionedUsers []string + + if result := <-uchan; result.Err != nil { + l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", teamId, result.Err) + return + } else { + profileMap := result.Data.(map[string]*model.User) + + if _, ok := profileMap[post.UserId]; !ok { + l4g.Error("Post user_id not returned by GetProfiles user_id=%v", post.UserId) + return + } + senderName := profileMap[post.UserId].Username + + toEmailMap := make(map[string]bool) + + if channel.Type == model.CHANNEL_DIRECT { + + var otherUserId string + if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { + otherUserId = userIds[1] + channelName = profileMap[userIds[1]].Username + } else { + otherUserId = userIds[0] + channelName = profileMap[userIds[0]].Username + } + + otherUser := profileMap[otherUserId] + sendEmail := true + if _, ok := otherUser.NotifyProps["email"]; ok && otherUser.NotifyProps["email"] == "false" { + sendEmail = false + } + if sendEmail && (otherUser.IsOffline() || otherUser.IsAway()) { + toEmailMap[otherUserId] = true + } + + } else { + + // Find out who is a member of the channel only keep those profiles + if eResult := <-echan; eResult.Err != nil { + l4g.Error("Failed to get channel members channel_id=%v err=%v", post.ChannelId, eResult.Err.Message) + return + } else { + tempProfileMap := make(map[string]*model.User) + members := eResult.Data.([]model.ChannelMember) + for _, member := range members { + tempProfileMap[member.UserId] = profileMap[member.UserId] + } + + profileMap = tempProfileMap + } + + // Build map for keywords + keywordMap := make(map[string][]string) + for _, profile := range profileMap { + if len(profile.NotifyProps["mention_keys"]) > 0 { + + // Add all the user's mention keys + splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") + for _, k := range splitKeys { + keywordMap[k] = append(keywordMap[strings.ToLower(k)], profile.Id) + } + + // If turned on, add the user's case sensitive first name + if profile.NotifyProps["first_name"] == "true" { + splitName := strings.Split(profile.FullName, " ") + if len(splitName) > 0 && splitName[0] != "" { + keywordMap[splitName[0]] = append(keywordMap[splitName[0]], profile.Id) + } + } + } + } + + // Build a map as a list of unique user_ids that are mentioned in this post + splitF := func(c rune) bool { + return c == ',' || c == ' ' || c == '.' || c == '!' || c == '?' || c == ':' || c == '<' || c == '>' + } + splitMessage := strings.FieldsFunc(strings.Replace(post.Message, "<br>", " ", -1), splitF) + for _, word := range splitMessage { + + // Non-case-sensitive check for regular keys + userIds1, keyMatch := keywordMap[strings.ToLower(word)] + + // Case-sensitive check for first name + userIds2, firstNameMatch := keywordMap[word] + + userIds := append(userIds1, userIds2...) + + // If one of the non-case-senstive keys or the first name matches the word + // then we add en entry to the sendEmail map + if keyMatch || firstNameMatch { + for _, userId := range userIds { + if post.UserId == userId { + continue + } + sendEmail := true + if _, ok := profileMap[userId].NotifyProps["email"]; ok && profileMap[userId].NotifyProps["email"] == "false" { + sendEmail = false + } + if sendEmail && (profileMap[userId].IsAway() || profileMap[userId].IsOffline()) { + toEmailMap[userId] = true + } else { + toEmailMap[userId] = false + } + } + } + } + + for id, _ := range toEmailMap { + fireAndForgetMentionUpdate(post.ChannelId, id) + } + } + + if len(toEmailMap) != 0 { + mentionedUsers = make([]string, 0, len(toEmailMap)) + for k := range toEmailMap { + mentionedUsers = append(mentionedUsers, k) + } + + var teamName string + if result := <-tchan; result.Err != nil { + l4g.Error("Failed to retrieve team team_id=%v, err=%v", teamId, result.Err) + return + } else { + teamName = result.Data.(*model.Team).Name + } + + // Build and send the emails + location, _ := time.LoadLocation("UTC") + tm := time.Unix(post.CreateAt/1000, 0).In(location) + + subjectPage := NewServerTemplatePage("post_subject", teamUrl) + subjectPage.Props["TeamName"] = teamName + subjectPage.Props["SubjectText"] = subjectText + subjectPage.Props["Month"] = tm.Month().String()[:3] + subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) + subjectPage.Props["Year"] = fmt.Sprintf("%d", tm.Year()) + + for id, doSend := range toEmailMap { + + if !doSend { + continue + } + + // skip if inactive + if profileMap[id].DeleteAt > 0 { + continue + } + + firstName := strings.Split(profileMap[id].FullName, " ")[0] + + bodyPage := NewServerTemplatePage("post_body", teamUrl) + bodyPage.Props["FullName"] = firstName + bodyPage.Props["TeamName"] = teamName + bodyPage.Props["ChannelName"] = channelName + bodyPage.Props["BodyText"] = bodyText + bodyPage.Props["SenderName"] = senderName + bodyPage.Props["Hour"] = fmt.Sprintf("%02d", tm.Hour()) + bodyPage.Props["Minute"] = fmt.Sprintf("%02d", tm.Minute()) + bodyPage.Props["Month"] = tm.Month().String()[:3] + bodyPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) + bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) + bodyPage.Props["TeamLink"] = teamUrl + "/channels/" + channel.Name + + if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("Failed to send mention email successfully email=%v err=%v", profileMap[id].Email, err) + } + + if len(utils.Cfg.EmailSettings.ApplePushServer) > 0 { + sessionChan := Srv.Store.Session().GetSessions(id) + if result := <-sessionChan; result.Err != nil { + l4g.Error("Failed to retrieve sessions in notifications id=%v, err=%v", id, result.Err) + } else { + sessions := result.Data.([]*model.Session) + alreadySeen := make(map[string]string) + + for _, session := range sessions { + if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" { + + alreadySeen[session.DeviceId] = session.DeviceId + + utils.FireAndForgetSendAppleNotify(session.DeviceId, subjectPage.Render(), 1) + } + } + } + } + } + } + } + + message := model.NewMessage(teamId, post.ChannelId, post.UserId, model.ACTION_POSTED) + message.Add("post", post.ToJson()) + if len(mentionedUsers) != 0 { + message.Add("mentions", model.ArrayToJson(mentionedUsers)) + } + + store.PublishAndForget(message) + }() +} + +func fireAndForgetMentionUpdate(channelId, userId string) { + go func() { + if result := <-Srv.Store.Channel().IncrementMentionCount(channelId, userId); result.Err != nil { + l4g.Error("Failed to update mention count for user_id=%v on channel_id=%v err=%v", userId, channelId, result.Err) + } + }() +} + +func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { + post := model.PostFromJson(r.Body) + + if post == nil { + c.SetInvalidParam("updatePost", "post") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId) + pchan := Srv.Store.Post().Get(post.Id) + + if !c.HasPermissionsToChannel(cchan, "updatePost") { + return + } + + var oldPost *model.Post + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else { + oldPost = result.Data.(*model.PostList).Posts[post.Id] + + if oldPost == nil { + c.Err = model.NewAppError("updatePost", "We couldn't find the existing post or comment to update.", "id="+post.Id) + c.Err.StatusCode = http.StatusBadRequest + return + } + + if oldPost.UserId != c.Session.UserId { + c.Err = model.NewAppError("updatePost", "You do not have the appropriate permissions", "oldUserId="+oldPost.UserId) + c.Err.StatusCode = http.StatusForbidden + return + } + + if oldPost.DeleteAt != 0 { + c.Err = model.NewAppError("updatePost", "You do not have the appropriate permissions", "Already delted id="+post.Id) + c.Err.StatusCode = http.StatusForbidden + return + } + } + + hashtags, _ := model.ParseHashtags(post.Message) + + if result := <-Srv.Store.Post().Update(oldPost, post.Message, hashtags); result.Err != nil { + c.Err = result.Err + return + } else { + rpost := result.Data.(*model.Post) + + message := model.NewMessage(c.Session.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED) + message.Add("post_id", rpost.Id) + message.Add("channel_id", rpost.ChannelId) + message.Add("message", rpost.Message) + + store.PublishAndForget(message) + + w.Write([]byte(rpost.ToJson())) + } +} + +func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + id := params["id"] + if len(id) != 26 { + c.SetInvalidParam("getPosts", "channelId") + return + } + + offset, err := strconv.Atoi(params["offset"]) + if err != nil { + c.SetInvalidParam("getPosts", "offset") + return + } + + limit, err := strconv.Atoi(params["limit"]) + if err != nil { + c.SetInvalidParam("getPosts", "limit") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + etagChan := Srv.Store.Post().GetEtag(id) + + if !c.HasPermissionsToChannel(cchan, "getPosts") { + return + } + + etag := (<-etagChan).Data.(string) + + if HandleEtag(etag, w, r) { + return + } + + pchan := Srv.Store.Post().GetPosts(id, offset, limit) + + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else { + list := result.Data.(*model.PostList) + + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + w.Write([]byte(list.ToJson())) + } + +} + +func getPost(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + channelId := params["id"] + if len(channelId) != 26 { + c.SetInvalidParam("getPost", "channelId") + return + } + + postId := params["post_id"] + if len(postId) != 26 { + c.SetInvalidParam("getPost", "postId") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + pchan := Srv.Store.Post().Get(postId) + + if !c.HasPermissionsToChannel(cchan, "getPost") { + return + } + + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else if HandleEtag(result.Data.(*model.PostList).Etag(), w, r) { + return + } else { + list := result.Data.(*model.PostList) + + if !list.IsChannelId(channelId) { + c.Err = model.NewAppError("getPost", "You do not have the appropriate permissions", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag()) + w.Write([]byte(list.ToJson())) + } +} + +func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + channelId := params["id"] + if len(channelId) != 26 { + c.SetInvalidParam("deletePost", "channelId") + return + } + + postId := params["post_id"] + if len(postId) != 26 { + c.SetInvalidParam("deletePost", "postId") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + pchan := Srv.Store.Post().Get(postId) + + if !c.HasPermissionsToChannel(cchan, "deletePost") { + return + } + + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else { + post := result.Data.(*model.PostList).Posts[postId] + + if post == nil { + c.SetInvalidParam("deletePost", "postId") + return + } + + if post.ChannelId != channelId { + c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if post.UserId != c.Session.UserId { + c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if dresult := <-Srv.Store.Post().Delete(postId, model.GetMillis()); dresult.Err != nil { + c.Err = dresult.Err + return + } + + message := model.NewMessage(c.Session.TeamId, post.ChannelId, c.Session.UserId, model.ACTION_POST_DELETED) + message.Add("post_id", post.Id) + message.Add("channel_id", post.ChannelId) + + store.PublishAndForget(message) + + result := make(map[string]string) + result["id"] = postId + w.Write([]byte(model.MapToJson(result))) + } +} + +func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { + terms := r.FormValue("terms") + + if len(terms) == 0 { + c.SetInvalidParam("search", "terms") + return + } + + hashtagTerms, plainTerms := model.ParseHashtags(terms) + + var hchan store.StoreChannel + if len(hashtagTerms) != 0 { + hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagTerms, true) + } + + var pchan store.StoreChannel + if len(plainTerms) != 0 { + pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, terms, false) + } + + mainList := &model.PostList{} + if hchan != nil { + if result := <-hchan; result.Err != nil { + c.Err = result.Err + return + } else { + mainList = result.Data.(*model.PostList) + } + } + + plainList := &model.PostList{} + if pchan != nil { + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else { + plainList = result.Data.(*model.PostList) + } + } + + for _, postId := range plainList.Order { + if _, ok := mainList.Posts[postId]; !ok { + mainList.AddPost(plainList.Posts[postId]) + mainList.AddOrder(postId) + } + + } + + w.Write([]byte(mainList.ToJson())) +} diff --git a/api/post_benchmark_test.go b/api/post_benchmark_test.go new file mode 100644 index 000000000..861c687fb --- /dev/null +++ b/api/post_benchmark_test.go @@ -0,0 +1,130 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/utils" + "testing" +) + +const ( + NUM_POSTS = 100 +) + +func BenchmarkCreatePost(b *testing.B) { + var ( + NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} + ) + _, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + testPoster.CreateTestPosts(NUM_POSTS_RANGE) + } +} + +func BenchmarkUpdatePost(b *testing.B) { + var ( + NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} + UPDATE_POST_LEN = 100 + ) + _, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + posts, valid := testPoster.CreateTestPosts(NUM_POSTS_RANGE) + if valid == false { + b.Fatal("Unable to create test posts") + } + + for i := range posts { + posts[i].Message = utils.RandString(UPDATE_POST_LEN, utils.ALPHANUMERIC) + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for i := range posts { + if _, err := Client.UpdatePost(posts[i]); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkGetPosts(b *testing.B) { + var ( + NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} + ) + _, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + testPoster.CreateTestPosts(NUM_POSTS_RANGE) + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + Client.Must(Client.GetPosts(channel.Id, 0, NUM_POSTS, "")) + } +} + +func BenchmarkSearchPosts(b *testing.B) { + var ( + NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} + ) + _, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + testPoster.CreateTestPosts(NUM_POSTS_RANGE) + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + Client.Must(Client.SearchPosts("nothere")) + Client.Must(Client.SearchPosts("n")) + Client.Must(Client.SearchPosts("#tag")) + } +} + +func BenchmarkEtagCache(b *testing.B) { + var ( + NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} + ) + _, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + testPoster.CreateTestPosts(NUM_POSTS_RANGE) + + etag := Client.Must(Client.GetPosts(channel.Id, 0, NUM_POSTS/2, "")).Etag + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + Client.Must(Client.GetPosts(channel.Id, 0, NUM_POSTS/2, etag)) + } +} + +func BenchmarkDeletePosts(b *testing.B) { + var ( + NUM_POSTS_RANGE = utils.Range{NUM_POSTS, NUM_POSTS} + ) + _, _, channel := SetupBenchmark() + + testPoster := NewAutoPostCreator(Client, channel.Id) + posts, valid := testPoster.CreateTestPosts(NUM_POSTS_RANGE) + if valid == false { + b.Fatal("Unable to create test posts") + } + + // Benchmark Start + b.ResetTimer() + for i := 0; i < b.N; i++ { + for i := range posts { + Client.Must(Client.DeletePost(channel.Id, posts[i].Id)) + } + } + +} diff --git a/api/post_test.go b/api/post_test.go new file mode 100644 index 000000000..4b40bc06a --- /dev/null +++ b/api/post_test.go @@ -0,0 +1,566 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/http" + "testing" + "time" +) + +func TestCreatePost(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + team2 := &model.Team{Name: "Name Team 2", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + filenames := []string{"/api/v1/files/get/12345678901234567890123456/12345678901234567890123456/test.png", "/api/v1/files/get/" + channel1.Id + "/" + user1.Id + "/test.png", "www.mattermost.com/fake/url", "junk"} + + post1 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag a" + model.NewId() + "a", Filenames: filenames} + rpost1, err := Client.CreatePost(post1) + if err != nil { + t.Fatal(err) + } + + if rpost1.Data.(*model.Post).Message != post1.Message { + t.Fatal("message didn't match") + } + + if rpost1.Data.(*model.Post).Hashtags != "#hashtag" { + t.Fatal("hashtag didn't match") + } + + if len(rpost1.Data.(*model.Post).Filenames) != 2 { + t.Fatal("filenames didn't parse correctly") + } + + post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id} + rpost2, err := Client.CreatePost(post2) + if err != nil { + t.Fatal(err) + } + + post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id, ParentId: rpost2.Data.(*model.Post).Id} + _, err = Client.CreatePost(post3) + if err != nil { + t.Fatal(err) + } + + post4 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: "junk"} + _, err = Client.CreatePost(post4) + if err.StatusCode != http.StatusBadRequest { + t.Fatal("Should have been invalid param") + } + + post5 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id, ParentId: "junk"} + _, err = Client.CreatePost(post5) + if err.StatusCode != http.StatusBadRequest { + t.Fatal("Should have been invalid param") + } + + post1c2 := &model.Post{ChannelId: channel2.Id, Message: "a" + model.NewId() + "a"} + rpost1c2, err := Client.CreatePost(post1c2) + + post2c2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1c2.Data.(*model.Post).Id} + _, err = Client.CreatePost(post2c2) + if err.StatusCode != http.StatusBadRequest { + t.Fatal("Should have been invalid param") + } + + post6 := &model.Post{ChannelId: "junk", Message: "a" + model.NewId() + "a"} + _, err = Client.CreatePost(post6) + if err.StatusCode != http.StatusForbidden { + t.Fatal("Should have been forbidden") + } + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + post7 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + _, err = Client.CreatePost(post7) + if err.StatusCode != http.StatusForbidden { + t.Fatal("Should have been forbidden") + } + + user3 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user3.Id) + + Client.LoginByEmail(team2.Domain, user3.Email, "pwd") + + channel3 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id} + channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel) + + post8 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + _, err = Client.CreatePost(post8) + if err.StatusCode != http.StatusForbidden { + t.Fatal("Should have been forbidden") + } + + if _, err = Client.DoPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { + t.Fatal("should have been an error") + } +} + +func TestCreateValetPost(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + team2 := &model.Team{Name: "Name Team 2", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag a" + model.NewId() + "a"} + rpost1, err := Client.CreateValetPost(post1) + if err != nil { + t.Fatal(err) + } + + if rpost1.Data.(*model.Post).Message != post1.Message { + t.Fatal("message didn't match") + } + + if rpost1.Data.(*model.Post).Hashtags != "#hashtag" { + t.Fatal("hashtag didn't match") + } + + post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id} + rpost2, err := Client.CreateValetPost(post2) + if err != nil { + t.Fatal(err) + } + + post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id, ParentId: rpost2.Data.(*model.Post).Id} + _, err = Client.CreateValetPost(post3) + if err != nil { + t.Fatal(err) + } + + post4 := &model.Post{ChannelId: "junk", Message: "a" + model.NewId() + "a"} + _, err = Client.CreateValetPost(post4) + if err.StatusCode != http.StatusForbidden { + t.Fatal("Should have been forbidden") + } + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + post5 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + _, err = Client.CreateValetPost(post5) + if err != nil { + t.Fatal(err) + } + + user3 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user3.Id) + + Client.LoginByEmail(team2.Domain, user3.Email, "pwd") + + channel3 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id} + channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel) + + post6 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + _, err = Client.CreateValetPost(post6) + if err.StatusCode != http.StatusForbidden { + t.Fatal("Should have been forbidden") + } + + if _, err = Client.DoPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { + t.Fatal("should have been an error") + } +} + +func TestUpdatePost(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + team2 := &model.Team{Name: "Name Team 2", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + rpost1, err := Client.CreatePost(post1) + if err != nil { + t.Fatal(err) + } + + if rpost1.Data.(*model.Post).Message != post1.Message { + t.Fatal("full name didn't match") + } + + post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id} + rpost2, err := Client.CreatePost(post2) + if err != nil { + t.Fatal(err) + } + + msg2 := "a" + model.NewId() + " update post 1" + rpost2.Data.(*model.Post).Message = msg2 + if rupost2, err := Client.UpdatePost(rpost2.Data.(*model.Post)); err != nil { + t.Fatal(err) + } else { + if rupost2.Data.(*model.Post).Message != msg2 { + t.Fatal("failed to updates") + } + } + + msg1 := "#hashtag a" + model.NewId() + " update post 2" + rpost1.Data.(*model.Post).Message = msg1 + if rupost1, err := Client.UpdatePost(rpost1.Data.(*model.Post)); err != nil { + t.Fatal(err) + } else { + if rupost1.Data.(*model.Post).Message != msg1 && rupost1.Data.(*model.Post).Hashtags != "#hashtag" { + t.Fatal("failed to updates") + } + } + + up12 := &model.Post{Id: rpost1.Data.(*model.Post).Id, ChannelId: channel1.Id, Message: "a" + model.NewId() + " updaet post 1 update 2"} + if rup12, err := Client.UpdatePost(up12); err != nil { + t.Fatal(err) + } else { + if rup12.Data.(*model.Post).Message != up12.Message { + t.Fatal("failed to updates") + } + } +} + +func TestGetPosts(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + time.Sleep(10 * time.Millisecond) + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post1a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post1.Id} + post1a1 = Client.Must(Client.CreatePost(post1a1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post3.Id} + post3a1 = Client.Must(Client.CreatePost(post3a1)).Data.(*model.Post) + + r1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList) + + if r1.Order[0] != post3a1.Id { + t.Fatal("wrong order") + } + + if r1.Order[1] != post3.Id { + t.Fatal("wrong order") + } + + if len(r1.Posts) != 4 { + t.Fatal("wrong size") + } + + r2 := Client.Must(Client.GetPosts(channel1.Id, 2, 2, "")).Data.(*model.PostList) + + if r2.Order[0] != post2.Id { + t.Fatal("wrong order") + } + + if r2.Order[1] != post1a1.Id { + t.Fatal("wrong order") + } + + if len(r2.Posts) != 4 { + t.Log(r2.Posts) + t.Fatal("wrong size") + } +} + +func TestSearchPosts(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "search for post1"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + post2 := &model.Post{ChannelId: channel1.Id, Message: "search for post2"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + post3 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag search for post3"} + post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post) + + post4 := &model.Post{ChannelId: channel1.Id, Message: "hashtag for post4"} + post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post) + + r1 := Client.Must(Client.SearchPosts("search")).Data.(*model.PostList) + + if len(r1.Order) != 3 { + t.Fatal("wrong serach") + } + + r2 := Client.Must(Client.SearchPosts("post2")).Data.(*model.PostList) + + if len(r2.Order) != 1 && r2.Order[0] == post2.Id { + t.Fatal("wrong serach") + } + + r3 := Client.Must(Client.SearchPosts("#hashtag")).Data.(*model.PostList) + + if len(r3.Order) != 1 && r3.Order[0] == post3.Id { + t.Fatal("wrong serach") + } +} + +func TestSearchHashtagPosts(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "#sgtitlereview with space"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + post2 := &model.Post{ChannelId: channel1.Id, Message: "#sgtitlereview\n with return"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + post3 := &model.Post{ChannelId: channel1.Id, Message: "no hashtag"} + post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post) + + r1 := Client.Must(Client.SearchPosts("#sgtitlereview")).Data.(*model.PostList) + + if len(r1.Order) != 2 { + t.Fatal("wrong search") + } +} + +func TestGetPostsCache(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + time.Sleep(10 * time.Millisecond) + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post) + + etag := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Etag + + // test etag caching + if cache_result, err := Client.GetPosts(channel1.Id, 0, 2, etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.PostList) != nil { + t.Log(cache_result.Data) + t.Fatal("cache should be empty") + } + + etag = Client.Must(Client.GetPost(channel1.Id, post1.Id, "")).Etag + + // test etag caching + if cache_result, err := Client.GetPost(channel1.Id, post1.Id, etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.PostList) != nil { + t.Log(cache_result.Data) + t.Fatal("cache should be empty") + } + +} + +func TestDeletePosts(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + time.Sleep(10 * time.Millisecond) + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post1a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post1.Id} + post1a1 = Client.Must(Client.CreatePost(post1a1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post1a2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post1.Id, ParentId: post1a1.Id} + post1a2 = Client.Must(Client.CreatePost(post1a2)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post3.Id} + post3a1 = Client.Must(Client.CreatePost(post3a1)).Data.(*model.Post) + + Client.Must(Client.DeletePost(channel1.Id, post3.Id)) + + r2 := Client.Must(Client.GetPosts(channel1.Id, 0, 10, "")).Data.(*model.PostList) + + if len(r2.Posts) != 4 { + t.Fatal("should have returned 5 items") + } +} + +func TestEmailMention(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: "corey@test.com", FullName: "Bob Bobby", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + post1 := &model.Post{ChannelId: channel1.Id, Message: "bob"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + // No easy way to verify the email was sent, but this will at least cause the server to throw errors if the code is broken + +} + +func TestFuzzyPosts(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + filenames := []string{"junk"} + + for i := 0; i < len(utils.FUZZY_STRINGS_POSTS); i++ { + post := &model.Post{ChannelId: channel1.Id, Message: utils.FUZZY_STRINGS_POSTS[i], Filenames: filenames} + + _, err := Client.CreatePost(post) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/api/server.go b/api/server.go new file mode 100644 index 000000000..58986a8d4 --- /dev/null +++ b/api/server.go @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "github.com/braintree/manners" + "github.com/gorilla/mux" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "net/http" + "time" +) + +type Server struct { + Server *manners.GracefulServer + Store store.Store + Router *mux.Router +} + +var Srv *Server + +func NewServer() { + + l4g.Info("Server is initializing...") + + Srv = &Server{} + Srv.Server = manners.NewServer() + Srv.Store = store.NewSqlStore() + store.RedisClient() + + Srv.Router = mux.NewRouter() + Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404) +} + +func StartServer() { + l4g.Info("Starting Server...") + + l4g.Info("Server is listening on " + utils.Cfg.ServiceSettings.Port) + go func() { + err := Srv.Server.ListenAndServe(":"+utils.Cfg.ServiceSettings.Port, Srv.Router) + if err != nil { + l4g.Critical("Error starting server, err:%v", err) + time.Sleep(time.Second) + panic("Error starting server " + err.Error()) + } + }() +} + +func StopServer() { + + l4g.Info("Stopping Server...") + + Srv.Server.Shutdown <- true + Srv.Store.Close() + store.RedisClose() + + l4g.Info("Server stopped") +} diff --git a/api/server_test.go b/api/server_test.go new file mode 100644 index 000000000..2d1d57392 --- /dev/null +++ b/api/server_test.go @@ -0,0 +1,11 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "testing" +) + +func TestServer(t *testing.T) { +} diff --git a/api/team.go b/api/team.go new file mode 100644 index 000000000..b04d8c588 --- /dev/null +++ b/api/team.go @@ -0,0 +1,542 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "fmt" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/route53" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/http" + "net/url" + "strconv" + "strings" +) + +func InitTeam(r *mux.Router) { + l4g.Debug("Initializing team api routes") + + sr := r.PathPrefix("/teams").Subrouter() + sr.Handle("/create", ApiAppHandler(createTeam)).Methods("POST") + sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST") + sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") + sr.Handle("/find_team_by_domain", ApiAppHandler(findTeamByDomain)).Methods("POST") + sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") + sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST") + sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") + sr.Handle("/update_name", ApiUserRequired(updateTeamName)).Methods("POST") +} + +func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { + + m := model.MapFromJson(r.Body) + email := strings.ToLower(strings.TrimSpace(m["email"])) + name := strings.TrimSpace(m["name"]) + + if len(email) == 0 { + c.SetInvalidParam("signupTeam", "email") + return + } + + if len(name) == 0 { + c.SetInvalidParam("signupTeam", "name") + return + } + + subjectPage := NewServerTemplatePage("signup_team_subject", c.TeamUrl) + bodyPage := NewServerTemplatePage("signup_team_body", c.TeamUrl) + bodyPage.Props["TourUrl"] = utils.Cfg.TeamSettings.TourLink + + props := make(map[string]string) + props["email"] = email + props["name"] = name + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + + data := model.MapToJson(props) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) + + bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_team_complete/?d=%s&h=%s", c.TeamUrl, url.QueryEscape(data), url.QueryEscape(hash)) + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + c.Err = err + return + } + + if utils.Cfg.ServiceSettings.Mode == utils.MODE_DEV { + m["follow_link"] = bodyPage.Props["Link"] + } + + w.Header().Set("Access-Control-Allow-Origin", " *") + w.Write([]byte(model.MapToJson(m))) +} + +func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { + + teamSignup := model.TeamSignupFromJson(r.Body) + + if teamSignup == nil { + c.SetInvalidParam("createTeam", "teamSignup") + return + } + + props := model.MapFromJson(strings.NewReader(teamSignup.Data)) + teamSignup.Team.Email = props["email"] + teamSignup.User.Email = props["email"] + + teamSignup.Team.PreSave() + + if err := teamSignup.Team.IsValid(); err != nil { + c.Err = err + return + } + teamSignup.Team.Id = "" + + password := teamSignup.User.Password + teamSignup.User.PreSave() + teamSignup.User.TeamId = model.NewId() + if err := teamSignup.User.IsValid(); err != nil { + c.Err = err + return + } + teamSignup.User.Id = "" + teamSignup.User.TeamId = "" + teamSignup.User.Password = password + + if !model.ComparePassword(teamSignup.Hash, fmt.Sprintf("%v:%v", teamSignup.Data, utils.Cfg.ServiceSettings.InviteSalt)) { + c.Err = model.NewAppError("createTeamFromSignup", "The signup link does not appear to be valid", "") + return + } + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour + c.Err = model.NewAppError("createTeamFromSignup", "The signup link has expired", "") + return + } + + found := FindTeamByDomain(c, teamSignup.Team.Domain, "true") + if c.Err != nil { + return + } + + if found { + c.Err = model.NewAppError("createTeamFromSignup", "This URL is unavailable. Please try another.", "d="+teamSignup.Team.Domain) + return + } + + if IsBetaDomain(r) { + for key, value := range utils.Cfg.ServiceSettings.Shards { + if strings.Index(r.Host, key) == 0 { + createSubDomain(teamSignup.Team.Domain, value) + break + } + } + } + + if result := <-Srv.Store.Team().Save(&teamSignup.Team); result.Err != nil { + c.Err = result.Err + return + } else { + rteam := result.Data.(*model.Team) + + channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id} + + if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil { + c.Err = err + return + } + + teamSignup.User.TeamId = rteam.Id + teamSignup.User.EmailVerified = true + + ruser := CreateUser(c, rteam, &teamSignup.User) + if c.Err != nil { + return + } + + CreateValet(c, rteam) + if c.Err != nil { + return + } + + InviteMembers(rteam, ruser, teamSignup.Invites) + + teamSignup.Team = *rteam + teamSignup.User = *ruser + + w.Write([]byte(teamSignup.ToJson())) + } +} + +func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { + + team := model.TeamFromJson(r.Body) + + if team == nil { + c.SetInvalidParam("createTeam", "team") + return + } + + if utils.Cfg.ServiceSettings.Mode != utils.MODE_DEV { + c.Err = model.NewAppError("createTeam", "The mode does not allow network creation without a valid invite", "") + return + } + + if result := <-Srv.Store.Team().Save(team); result.Err != nil { + c.Err = result.Err + return + } else { + rteam := result.Data.(*model.Team) + + channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: rteam.Id} + + if _, err := CreateChannel(c, channel, r.URL.Path, false); err != nil { + c.Err = err + return + } + + w.Write([]byte(rteam.ToJson())) + } +} + +func doesSubDomainExist(subDomain string) bool { + + // if it's configured for testing then skip this step + if utils.Cfg.AWSSettings.Route53AccessKeyId == "" { + return false + } + + creds := aws.Creds(utils.Cfg.AWSSettings.Route53AccessKeyId, utils.Cfg.AWSSettings.Route53SecretAccessKey, "") + r53 := route53.New(aws.DefaultConfig.Merge(&aws.Config{Credentials: creds, Region: utils.Cfg.AWSSettings.Route53Region})) + + r53req := &route53.ListResourceRecordSetsInput{ + HostedZoneID: aws.String(utils.Cfg.AWSSettings.Route53ZoneId), + MaxItems: aws.String("1"), + StartRecordName: aws.String(fmt.Sprintf("%v.%v.", subDomain, utils.Cfg.ServiceSettings.Domain)), + } + + if result, err := r53.ListResourceRecordSets(r53req); err != nil { + l4g.Error("error in doesSubDomainExist domain=%v err=%v", subDomain, err) + return true + } else { + + for _, v := range result.ResourceRecordSets { + if v.Name != nil && *v.Name == fmt.Sprintf("%v.%v.", subDomain, utils.Cfg.ServiceSettings.Domain) { + return true + } + } + } + + return false +} + +func createSubDomain(subDomain string, target string) { + + if utils.Cfg.AWSSettings.Route53AccessKeyId == "" { + return + } + + creds := aws.Creds(utils.Cfg.AWSSettings.Route53AccessKeyId, utils.Cfg.AWSSettings.Route53SecretAccessKey, "") + r53 := route53.New(aws.DefaultConfig.Merge(&aws.Config{Credentials: creds, Region: utils.Cfg.AWSSettings.Route53Region})) + + rr := route53.ResourceRecord{ + Value: aws.String(target), + } + + rrs := make([]*route53.ResourceRecord, 1) + rrs[0] = &rr + + change := route53.Change{ + Action: aws.String("CREATE"), + ResourceRecordSet: &route53.ResourceRecordSet{ + Name: aws.String(fmt.Sprintf("%v.%v", subDomain, utils.Cfg.ServiceSettings.Domain)), + TTL: aws.Long(300), + Type: aws.String("CNAME"), + ResourceRecords: rrs, + }, + } + + changes := make([]*route53.Change, 1) + changes[0] = &change + + r53req := &route53.ChangeResourceRecordSetsInput{ + HostedZoneID: aws.String(utils.Cfg.AWSSettings.Route53ZoneId), + ChangeBatch: &route53.ChangeBatch{ + Changes: changes, + }, + } + + if _, err := r53.ChangeResourceRecordSets(r53req); err != nil { + l4g.Error("erro in createSubDomain domain=%v err=%v", subDomain, err) + return + } +} + +func findTeamByDomain(c *Context, w http.ResponseWriter, r *http.Request) { + + m := model.MapFromJson(r.Body) + + domain := strings.ToLower(strings.TrimSpace(m["domain"])) + all := strings.ToLower(strings.TrimSpace(m["all"])) + + found := FindTeamByDomain(c, domain, all) + + if c.Err != nil { + return + } + + if found { + w.Write([]byte("true")) + } else { + w.Write([]byte("false")) + } +} + +func FindTeamByDomain(c *Context, domain string, all string) bool { + + if domain == "" || len(domain) > 64 { + c.SetInvalidParam("findTeamByDomain", "domain") + return false + } + + if model.IsReservedDomain(domain) { + c.Err = model.NewAppError("findTeamByDomain", "This URL is unavailable. Please try another.", "d="+domain) + return false + } + + if all == "false" { + if result := <-Srv.Store.Team().GetByDomain(domain); result.Err != nil { + return false + } else { + return true + } + } else { + if doesSubDomainExist(domain) { + return true + } + + protocol := "http" + + if utils.Cfg.ServiceSettings.UseSSL { + protocol = "https" + } + + for key, _ := range utils.Cfg.ServiceSettings.Shards { + url := fmt.Sprintf("%v://%v.%v/api/v1", protocol, key, utils.Cfg.ServiceSettings.Domain) + + if strings.Index(utils.Cfg.ServiceSettings.Domain, "localhost") == 0 { + url = fmt.Sprintf("%v://%v/api/v1", protocol, utils.Cfg.ServiceSettings.Domain) + } + + client := model.NewClient(url) + + if result, err := client.FindTeamByDomain(domain, false); err != nil { + c.Err = err + return false + } else { + if result.Data.(bool) { + return true + } + } + } + + return false + } +} + +func findTeams(c *Context, w http.ResponseWriter, r *http.Request) { + + m := model.MapFromJson(r.Body) + + email := strings.ToLower(strings.TrimSpace(m["email"])) + + if email == "" { + c.SetInvalidParam("findTeam", "email") + return + } + + if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { + c.Err = result.Err + return + } else { + teams := result.Data.([]*model.Team) + + s := make([]string, 0, len(teams)) + + for _, v := range teams { + s = append(s, v.Domain) + } + + w.Write([]byte(model.ArrayToJson(s))) + } +} + +func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { + + m := model.MapFromJson(r.Body) + + email := strings.ToLower(strings.TrimSpace(m["email"])) + + if email == "" { + c.SetInvalidParam("findTeam", "email") + return + } + + protocol := "http" + + if utils.Cfg.ServiceSettings.UseSSL { + protocol = "https" + } + + subjectPage := NewServerTemplatePage("find_teams_subject", c.TeamUrl) + bodyPage := NewServerTemplatePage("find_teams_body", c.TeamUrl) + + for key, _ := range utils.Cfg.ServiceSettings.Shards { + url := fmt.Sprintf("%v://%v.%v/api/v1", protocol, key, utils.Cfg.ServiceSettings.Domain) + + if strings.Index(utils.Cfg.ServiceSettings.Domain, "localhost") == 0 { + url = fmt.Sprintf("%v://%v/api/v1", protocol, utils.Cfg.ServiceSettings.Domain) + } + + client := model.NewClient(url) + + if result, err := client.FindTeams(email); err != nil { + l4g.Error("An error occured while finding teams at %v err=%v", key, err) + } else { + data := result.Data.([]string) + for _, domain := range data { + bodyPage.Props[fmt.Sprintf("%v://%v.%v", protocol, domain, utils.Cfg.ServiceSettings.Domain)] = "" + } + } + } + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("An error occured while sending an email in emailTeams err=%v", err) + } + + w.Write([]byte(model.MapToJson(m))) +} + +func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { + invites := model.InvitesFromJson(r.Body) + if len(invites.Invites) == 0 { + c.Err = model.NewAppError("Team.InviteMembers", "No one to invite.", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + tchan := Srv.Store.Team().Get(c.Session.TeamId) + uchan := Srv.Store.User().Get(c.Session.UserId) + + var team *model.Team + if result := <-tchan; result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + ia := make([]string, len(invites.Invites)) + for _, invite := range invites.Invites { + ia = append(ia, invite["email"]) + } + + InviteMembers(team, user, ia) + + w.Write([]byte(invites.ToJson())) +} + +func InviteMembers(team *model.Team, user *model.User, invites []string) { + for _, invite := range invites { + if len(invite) > 0 { + teamUrl := "" + if utils.Cfg.ServiceSettings.Mode == utils.MODE_DEV { + teamUrl = "http://localhost:8065" + } else if utils.Cfg.ServiceSettings.UseSSL { + teamUrl = fmt.Sprintf("https://%v.%v", team.Domain, utils.Cfg.ServiceSettings.Domain) + } else { + teamUrl = fmt.Sprintf("http://%v.%v", team.Domain, utils.Cfg.ServiceSettings.Domain) + } + + sender := "" + if len(strings.TrimSpace(user.FullName)) == 0 { + sender = user.Username + } else { + sender = user.FullName + } + subjectPage := NewServerTemplatePage("invite_subject", teamUrl) + subjectPage.Props["SenderName"] = sender + subjectPage.Props["TeamName"] = team.Name + bodyPage := NewServerTemplatePage("invite_body", teamUrl) + bodyPage.Props["TeamName"] = team.Name + bodyPage.Props["SenderName"] = sender + + bodyPage.Props["Email"] = invite + + props := make(map[string]string) + props["email"] = invite + props["id"] = team.Id + props["name"] = team.Name + props["domain"] = team.Domain + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + data := model.MapToJson(props) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) + bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", teamUrl, url.QueryEscape(data), url.QueryEscape(hash)) + + if utils.Cfg.ServiceSettings.Mode == utils.MODE_DEV { + l4g.Info("sending invitation to %v %v", invite, bodyPage.Props["Link"]) + } + + if err := utils.SendMail(invite, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("Failed to send invite email successfully err=%v", err) + } + } + } +} + +func updateTeamName(c *Context, w http.ResponseWriter, r *http.Request) { + + props := model.MapFromJson(r.Body) + + new_name := props["new_name"] + if len(new_name) == 0 { + c.SetInvalidParam("updateTeamName", "new_name") + return + } + + teamId := props["team_id"] + if len(teamId) > 0 && len(teamId) != 26 { + c.SetInvalidParam("updateTeamName", "team_id") + return + } else if len(teamId) == 0 { + teamId = c.Session.TeamId + } + + if !c.HasPermissionsToTeam(teamId, "updateTeamName") { + return + } + + if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + c.Err = model.NewAppError("updateTeamName", "You do not have the appropriate permissions", "userId="+c.Session.UserId) + c.Err.StatusCode = http.StatusForbidden + return + } + + if result := <-Srv.Store.Team().UpdateName(new_name, c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } + + w.Write([]byte(model.MapToJson(props))) +} diff --git a/api/team_test.go b/api/team_test.go new file mode 100644 index 000000000..74a184634 --- /dev/null +++ b/api/team_test.go @@ -0,0 +1,288 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "fmt" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "strings" + "testing" +) + +func TestSignupTeam(t *testing.T) { + Setup() + + _, err := Client.SignupTeam("test@nowhere.com", "name") + if err != nil { + t.Fatal(err) + } +} + +func TestCreateFromSignupTeam(t *testing.T) { + Setup() + + props := make(map[string]string) + props["email"] = strings.ToLower(model.NewId()) + "corey@test.com" + props["name"] = "Test Company name" + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + + data := model.MapToJson(props) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + user := model.User{Email: props["email"], FullName: "Corey Hulen", Password: "hello"} + + ts := model.TeamSignup{Team: team, User: user, Invites: []string{"corey@test.com"}, Data: data, Hash: hash} + + rts, err := Client.CreateTeamFromSignup(&ts) + if err != nil { + t.Fatal(err) + } + + if rts.Data.(*model.TeamSignup).Team.Name != team.Name { + t.Fatal("full name didn't match") + } + + ruser := rts.Data.(*model.TeamSignup).User + + if result, err := Client.LoginById(ruser.Id, user.Password); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).Email != user.Email { + t.Fatal("email's didn't match") + } + } + + ts.Data = "garbage" + _, err = Client.CreateTeamFromSignup(&ts) + if err == nil { + t.Fatal(err) + } +} + +func TestCreateTeam(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, err := Client.CreateTeam(&team) + if err != nil { + t.Fatal(err) + } + + if rteam.Data.(*model.Team).Name != team.Name { + t.Fatal("full name didn't match") + } + + if _, err := Client.CreateTeam(rteam.Data.(*model.Team)); err == nil { + t.Fatal("Cannot create an existing") + } + + rteam.Data.(*model.Team).Id = "" + if _, err := Client.CreateTeam(rteam.Data.(*model.Team)); err != nil { + if err.Message != "A team with that domain already exists" { + t.Fatal(err) + } + } + + if _, err := Client.DoPost("/teams/create", "garbage"); err == nil { + t.Fatal("should have been an error") + } +} + +func TestFindTeamByEmail(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + if r1, err := Client.FindTeams(user.Email); err != nil { + t.Fatal(err) + } else { + domains := r1.Data.([]string) + if domains[0] != team.Domain { + t.Fatal(domains) + } + } + + if _, err := Client.FindTeams("missing"); err != nil { + t.Fatal(err) + } +} + +/* + +XXXXXX investigate and fix failing test + +func TestFindTeamByDomain(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + if r1, err := Client.FindTeamByDomain(team.Domain, false); err != nil { + t.Fatal(err) + } else { + val := r1.Data.(bool) + if !val { + t.Fatal("should be a valid domain") + } + } + + if r1, err := Client.FindTeamByDomain(team.Domain, true); err != nil { + t.Fatal(err) + } else { + val := r1.Data.(bool) + if !val { + t.Fatal("should be a valid domain") + } + } + + if r1, err := Client.FindTeamByDomain("a"+model.NewId()+"a", false); err != nil { + t.Fatal(err) + } else { + val := r1.Data.(bool) + if val { + t.Fatal("shouldn't be a valid domain") + } + } +} + +*/ + +func TestFindTeamByEmailSend(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + if _, err := Client.FindTeamsSendEmail(user.Email); err != nil { + t.Fatal(err) + } else { + } + + if _, err := Client.FindTeamsSendEmail("missing"); err != nil { + + // It should actually succeed at sending the email since it doesn't exist + if !strings.Contains(err.DetailedError, "Failed to add to email address") { + t.Fatal(err) + } + } +} + +func TestInviteMembers(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + invite := make(map[string]string) + invite["email"] = model.NewId() + "corey@test.com" + invite["first_name"] = "Test" + invite["last_name"] = "Guy" + invites := &model.Invites{Invites: []map[string]string{invite}} + invites.Invites = append(invites.Invites, invite) + + if _, err := Client.InviteMembers(invites); err != nil { + t.Fatal(err) + } + + invites = &model.Invites{Invites: []map[string]string{}} + if _, err := Client.InviteMembers(invites); err == nil { + t.Fatal("Should have errored out on no invites to send") + } +} + +func TestUpdateTeamName(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: "test@nowhere.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + data := make(map[string]string) + data["new_name"] = "NewName" + if _, err := Client.UpdateTeamName(data); err == nil { + t.Fatal("Should have errored, not admin") + } + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + data["new_name"] = "" + if _, err := Client.UpdateTeamName(data); err == nil { + t.Fatal("Should have errored, empty name") + } + + data["new_name"] = "NewName" + if _, err := Client.UpdateTeamName(data); err != nil { + t.Fatal(err) + } + // No GET team web service, so hard to confirm here that team name updated + + data["team_id"] = "junk" + if _, err := Client.UpdateTeamName(data); err == nil { + t.Fatal("Should have errored, junk team id") + } + + data["team_id"] = "12345678901234567890123456" + if _, err := Client.UpdateTeamName(data); err == nil { + t.Fatal("Should have errored, bad team id") + } + + data["team_id"] = team.Id + data["new_name"] = "NewNameAgain" + if _, err := Client.UpdateTeamName(data); err != nil { + t.Fatal(err) + } + // No GET team web service, so hard to confirm here that team name updated +} + +func TestFuzzyTeamCreate(t *testing.T) { + + for i := 0; i < len(utils.FUZZY_STRINGS_NAMES) || i < len(utils.FUZZY_STRINGS_EMAILS); i++ { + testName := "Name" + testEmail := "test@nowhere.com" + + if i < len(utils.FUZZY_STRINGS_NAMES) { + testName = utils.FUZZY_STRINGS_NAMES[i] + } + if i < len(utils.FUZZY_STRINGS_EMAILS) { + testEmail = utils.FUZZY_STRINGS_EMAILS[i] + } + + team := model.Team{Name: testName, Domain: "z-z-" + model.NewId() + "a", Email: testEmail, Type: model.TEAM_OPEN} + + _, err := Client.CreateTeam(&team) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html new file mode 100644 index 000000000..dffe589cd --- /dev/null +++ b/api/templates/email_change_body.html @@ -0,0 +1,54 @@ +{{define "email_change_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You updated your email</h2> + <p>You updated your email for {{.Props.TeamName}} on {{ .TeamUrl }}<br> If this change wasn't initiated by you, please reply to this email and let us know.</p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} + diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html new file mode 100644 index 000000000..612dfcbe7 --- /dev/null +++ b/api/templates/email_change_subject.html @@ -0,0 +1 @@ +{{define "email_change_subject"}}You updated your email for {{.Props.TeamName}} on {{ .Props.Domain }}{{end}} diff --git a/api/templates/error.html b/api/templates/error.html new file mode 100644 index 000000000..ab4d91378 --- /dev/null +++ b/api/templates/error.html @@ -0,0 +1,26 @@ +<html> +<head> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> + <title>{{ .SiteName }} - Error</title> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"> + <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> + <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script> + <link href='https://fonts.googleapis.com/css?family=Lato:400,700,900' rel='stylesheet' type='text/css'> + <link rel="stylesheet" href="/static/css/styles.css"> +</head> +<body class="white error"> + <div class="container-fluid"> + <div class="error__container"> + <div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div> + <h2>{{ .SiteName }} needs your help:</h2> + <p>{{.Message}}</p> + </div> + </div> +</body> +<script> + var details = "{{ .Details }}"; + if (details.length > 0) { + console.log("error details: " + details); + } +</script> +</html> diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html new file mode 100644 index 000000000..d8b582b8a --- /dev/null +++ b/api/templates/find_teams_body.html @@ -0,0 +1,64 @@ +{{define "find_teams_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">Finding teams</h2> + <p>{{ if .Props }} + The following teams were found:<br> + {{range $index, $element := .Props}} + {{ $index }}<br> + {{ end }} + {{ else }} + We could not find any teams for the given email. + {{ end }} + </p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} + + + diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html new file mode 100644 index 000000000..e5ba2d23f --- /dev/null +++ b/api/templates/find_teams_subject.html @@ -0,0 +1 @@ +{{define "find_teams_subject"}}Your {{ .SiteName }} Teams{{end}} diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html new file mode 100644 index 000000000..06f48759c --- /dev/null +++ b/api/templates/invite_body.html @@ -0,0 +1,56 @@ +{{define "invite_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2> + <p>{{.Props.TeamName}} started using {{.SiteName}}.<br> The team administrator <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamName}}</strong>.</p> + <p style="margin: 20px 0 15px"> + <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Join Team</a> + </p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html new file mode 100644 index 000000000..4be15e343 --- /dev/null +++ b/api/templates/invite_subject.html @@ -0,0 +1 @@ +{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamName }} Team on {{.SiteName}}{{end}} diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html new file mode 100644 index 000000000..f6499d46d --- /dev/null +++ b/api/templates/password_change_body.html @@ -0,0 +1,55 @@ +{{define "password_change_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You updated your password</h2> + <p>You updated your password for {{.Props.TeamName}} on {{ .TeamUrl }} by {{.Props.Method}}.<br> If this change wasn't initiated by you, please reply to this email and let us know.</p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} + + diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html new file mode 100644 index 000000000..57422c692 --- /dev/null +++ b/api/templates/password_change_subject.html @@ -0,0 +1 @@ +{{define "password_change_subject"}}You updated your password for {{.Props.TeamName}} on {{ .SiteName }}{{end}} diff --git a/api/templates/post_body.html b/api/templates/post_body.html new file mode 100644 index 000000000..663ec66d2 --- /dev/null +++ b/api/templates/post_body.html @@ -0,0 +1,57 @@ +{{define "post_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2> + <p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} GMT, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif;">{{.Props.PostMessage}}</pre></p> + <p style="margin: 20px 0 15px"> + <a href="{{.Props.TeamLink}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a> + </p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} + diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html new file mode 100644 index 000000000..32df31018 --- /dev/null +++ b/api/templates/post_subject.html @@ -0,0 +1 @@ +{{define "post_subject"}}[{{.Props.TeamName}} {{.SiteName}}] {{.Props.SubjectText}} for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}}
\ No newline at end of file diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html new file mode 100644 index 000000000..3a5d62ab4 --- /dev/null +++ b/api/templates/reset_body.html @@ -0,0 +1,58 @@ +{{define "reset_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You requested a password reset</h2> + <p>To change your password, click below.<br>If you did not mean to reset your password, please ignore this email and your password will remain the same.</p> + <p style="margin: 20px 0 15px"> + <a href="{{.Props.ResetUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Reset Password</a> + </p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} + + diff --git a/api/templates/reset_subject.html b/api/templates/reset_subject.html new file mode 100644 index 000000000..87ad7bc38 --- /dev/null +++ b/api/templates/reset_subject.html @@ -0,0 +1 @@ +{{define "reset_subject"}}Reset your password{{end}} diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html new file mode 100644 index 000000000..6f8bbd92d --- /dev/null +++ b/api/templates/signup_team_body.html @@ -0,0 +1,59 @@ +{{define "signup_team_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">Thanks for creating a team!</h2> + <p style="margin: 20px 0 25px"> + <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a> + </p> + {{ .SiteName }} is free for an unlimited time, for unlimited users.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board.<br></p> + <p> + Learn more by <a href="{{.Props.TourUrl}}" style="text-decoration: none; color:#2389D7;">taking a tour</a> + </p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html new file mode 100644 index 000000000..1cd3427d2 --- /dev/null +++ b/api/templates/signup_team_subject.html @@ -0,0 +1 @@ +{{define "signup_team_subject"}}Invitation to {{ .SiteName }}{{end}}
\ No newline at end of file diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html new file mode 100644 index 000000000..56d85572b --- /dev/null +++ b/api/templates/verify_body.html @@ -0,0 +1,56 @@ +{{define "verify_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2> + <p>Please verify your email address by clicking below.</p> + <p style="margin: 20px 0 15px"> + <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Verify Email</a> + </p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html new file mode 100644 index 000000000..de0ef1d7a --- /dev/null +++ b/api/templates/verify_subject.html @@ -0,0 +1 @@ +{{define "verify_subject"}}[{{ .Props.TeamName }} {{ .SiteName }}] Email Verification{{end}}
\ No newline at end of file diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html new file mode 100644 index 000000000..fbc2e5551 --- /dev/null +++ b/api/templates/welcome_body.html @@ -0,0 +1,54 @@ +{{define "welcome_body"}} + +<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;"> + <tr> + <td> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;"> + <tr> + <td style="border: 1px solid #ddd;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> + <tr> + <td style="padding: 20px 30px 10px; text-align:left;"> + <img src="{{.TeamUrl}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + </td> + </tr> + <tr> + <td> + <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> + <tr> + <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> + <h2 style="font-weight: normal; margin-top: 10px;">You joined the {{.Props.TeamName}} team at {{.SiteName}}!</h2> + <p>Please let me know if you have any questions.<br>Enjoy your stay at <a href="{{.TeamUrl}}">{{.SiteName}}</a>.</p> + </td> + </tr> + <tr> + <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> + Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Best wishes,<br> + The {{.SiteName}} Team<br> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td style="text-align: center;color: #AAA; font-size: 13px; padding-bottom: 10px;"> + <p style="margin: 25px 0;"> + <img width="65" src="{{.TeamUrl}}/static/images/circles.png" alt=""> + </p> + <p style="padding: 0 50px;"> + (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + </p> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> +</table> + +{{end}} + diff --git a/api/templates/welcome_subject.html b/api/templates/welcome_subject.html new file mode 100644 index 000000000..106cc3ae6 --- /dev/null +++ b/api/templates/welcome_subject.html @@ -0,0 +1 @@ +{{define "welcome_subject"}}Welcome to {{ .SiteName }}{{end}}
\ No newline at end of file diff --git a/api/user.go b/api/user.go new file mode 100644 index 000000000..c0ebc05e0 --- /dev/null +++ b/api/user.go @@ -0,0 +1,1258 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "bytes" + "code.google.com/p/draw2d/draw2d" + l4g "code.google.com/p/log4go" + "fmt" + "github.com/goamz/goamz/aws" + "github.com/goamz/goamz/s3" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "github.com/mssola/user_agent" + "github.com/nfnt/resize" + "hash/fnv" + "image" + "image/color" + _ "image/gif" + _ "image/jpeg" + "image/png" + "net/http" + "net/url" + "strconv" + "strings" +) + +func InitUser(r *mux.Router) { + l4g.Debug("Initializing user api routes") + + sr := r.PathPrefix("/users").Subrouter() + sr.Handle("/create", ApiAppHandler(createUser)).Methods("POST") + sr.Handle("/update", ApiUserRequired(updateUser)).Methods("POST") + sr.Handle("/update_roles", ApiUserRequired(updateRoles)).Methods("POST") + sr.Handle("/update_active", ApiUserRequired(updateActive)).Methods("POST") + sr.Handle("/update_notify", ApiUserRequired(updateUserNotify)).Methods("POST") + sr.Handle("/newpassword", ApiUserRequired(updatePassword)).Methods("POST") + sr.Handle("/send_password_reset", ApiAppHandler(sendPasswordReset)).Methods("POST") + sr.Handle("/reset_password", ApiAppHandler(resetPassword)).Methods("POST") + sr.Handle("/login", ApiAppHandler(login)).Methods("POST") + sr.Handle("/logout", ApiUserRequired(logout)).Methods("POST") + sr.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST") + + sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") + + sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET") + sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("GET") + sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET") + sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET") + sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET") + sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET") + sr.Handle("/{id:[A-Za-z0-9]+}/image", ApiUserRequired(getProfileImage)).Methods("GET") +} + +func createUser(c *Context, w http.ResponseWriter, r *http.Request) { + + user := model.UserFromJson(r.Body) + + if user == nil { + c.SetInvalidParam("createUser", "user") + return + } + + if !model.IsUsernameValid(user.Username) { + c.Err = model.NewAppError("createUser", "That username is invalid", "might be using a resrved username") + return + } + + user.EmailVerified = false + + var team *model.Team + + if result := <-Srv.Store.Team().Get(user.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + hash := r.URL.Query().Get("h") + + shouldVerifyHash := true + + if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 { + domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(team.AllowedDomains, "@", " ", -1), ",", " ", -1)))) + + matched := false + for _, d := range domains { + if strings.HasSuffix(user.Email, "@"+d) { + matched = true + break + } + } + + if matched { + shouldVerifyHash = false + } else { + c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "allowed domains failed") + return + } + } + + if team.Type == model.TEAM_OPEN { + shouldVerifyHash = false + } + + if len(hash) > 0 { + shouldVerifyHash = true + } + + if shouldVerifyHash { + data := r.URL.Query().Get("d") + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) { + c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "") + return + } + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + c.Err = model.NewAppError("createUser", "The signup link has expired", "") + return + } + + if user.TeamId != props["id"] { + c.Err = model.NewAppError("createUser", "Invalid team name", data) + return + } + + user.Email = props["email"] + user.EmailVerified = true + } + + ruser := CreateUser(c, team, user) + if c.Err != nil { + return + } + + w.Write([]byte(ruser.ToJson())) + +} + +func CreateValet(c *Context, team *model.Team) *model.User { + valet := &model.User{} + valet.TeamId = team.Id + valet.Email = utils.Cfg.EmailSettings.FeedbackEmail + valet.EmailVerified = true + valet.Username = model.BOT_USERNAME + valet.Password = model.NewId() + + return CreateUser(c, team, valet) +} + +func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { + + channelRole := "" + if team.Email == user.Email { + user.Roles = model.ROLE_ADMIN + channelRole = model.CHANNEL_ROLE_ADMIN + } else { + user.Roles = "" + } + + user.MakeNonNil() + if len(user.Props["theme"]) == 0 { + user.AddProp("theme", utils.Cfg.TeamSettings.DefaultThemeColor) + } + + if result := <-Srv.Store.User().Save(user); result.Err != nil { + c.Err = result.Err + return nil + } else { + ruser := result.Data.(*model.User) + + // Do not error if user cannot be added to the town-square channel + if cresult := <-Srv.Store.Channel().GetByName(team.Id, "town-square"); cresult.Err != nil { + l4g.Error("Failed to get town-square err=%v", cresult.Err) + } else { + cm := &model.ChannelMember{ChannelId: cresult.Data.(*model.Channel).Id, UserId: ruser.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole} + if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { + l4g.Error("Failed to add member town-square err=%v", cmresult.Err) + } + } + + //fireAndForgetWelcomeEmail(strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl+"/channels/town-square") + + if user.EmailVerified { + if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil { + l4g.Error("Failed to get town-square err=%v", cresult.Err) + } + } else { + FireAndForgetVerifyEmail(result.Data.(*model.User).Id, strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl) + } + + ruser.Sanitize(map[string]bool{}) + + //This message goes to every channel, so the channelId is irrelevant + message := model.NewMessage(team.Id, "", ruser.Id, model.ACTION_NEW_USER) + + store.PublishAndForget(message) + + return ruser + } +} + +func fireAndForgetWelcomeEmail(name, email, teamName, link string) { + go func() { + + subjectPage := NewServerTemplatePage("welcome_subject", link) + bodyPage := NewServerTemplatePage("welcome_body", link) + bodyPage.Props["FullName"] = name + bodyPage.Props["TeamName"] = teamName + bodyPage.Props["FeedbackName"] = utils.Cfg.EmailSettings.FeedbackName + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("Failed to send welcome email successfully err=%v", err) + } + + }() +} + +func FireAndForgetVerifyEmail(userId, name, email, teamName, teamUrl string) { + go func() { + + link := fmt.Sprintf("%s/verify?uid=%s&hid=%s", teamUrl, userId, model.HashPassword(userId)) + + subjectPage := NewServerTemplatePage("verify_subject", teamUrl) + subjectPage.Props["TeamName"] = teamName + bodyPage := NewServerTemplatePage("verify_body", teamUrl) + bodyPage.Props["FullName"] = name + bodyPage.Props["TeamName"] = teamName + bodyPage.Props["VerifyUrl"] = link + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("Failed to send verification email successfully err=%v", err) + } + }() +} + +func login(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + extraInfo := "" + var result store.StoreResult + + if len(props["id"]) != 0 { + extraInfo = props["id"] + if result = <-Srv.Store.User().Get(props["id"]); result.Err != nil { + c.Err = result.Err + return + } + } + + var team *model.Team + if result.Data == nil && len(props["email"]) != 0 && len(props["domain"]) != 0 { + extraInfo = props["email"] + " in " + props["domain"] + + if nr := <-Srv.Store.Team().GetByDomain(props["domain"]); nr.Err != nil { + c.Err = nr.Err + return + } else { + team = nr.Data.(*model.Team) + + if result = <-Srv.Store.User().GetByEmail(team.Id, props["email"]); result.Err != nil { + c.Err = result.Err + return + } + } + } + + if result.Data == nil { + c.Err = model.NewAppError("login", "Login failed because we couldn't find a valid account", extraInfo) + c.Err.StatusCode = http.StatusBadRequest + return + } + + user := result.Data.(*model.User) + + if team == nil { + if tResult := <-Srv.Store.Team().Get(user.TeamId); tResult.Err != nil { + c.Err = tResult.Err + return + } else { + team = tResult.Data.(*model.Team) + } + } + + c.LogAuditWithUserId(user.Id, "attempt") + + if !model.ComparePassword(user.Password, props["password"]) { + c.LogAuditWithUserId(user.Id, "fail") + c.Err = model.NewAppError("login", "Login failed because of invalid password", extraInfo) + c.Err.StatusCode = http.StatusBadRequest + return + } + + if !user.EmailVerified { + c.Err = model.NewAppError("login", "Login failed because email address has not been verified", extraInfo) + c.Err.StatusCode = http.StatusForbidden + return + } + + if user.DeleteAt > 0 { + c.Err = model.NewAppError("login", "Login failed because your account has been set to inactive. Please contact an administrator.", extraInfo) + c.Err.StatusCode = http.StatusForbidden + return + } + + session := &model.Session{UserId: user.Id, TeamId: team.Id, Roles: user.Roles, DeviceId: props["device_id"]} + + maxAge := model.SESSION_TIME_WEB_IN_SECS + + if len(props["device_id"]) > 0 { + session.SetExpireInDays(model.SESSION_TIME_MOBILE_IN_DAYS) + maxAge = model.SESSION_TIME_MOBILE_IN_SECS + } else { + session.SetExpireInDays(model.SESSION_TIME_WEB_IN_DAYS) + } + + ua := user_agent.New(r.UserAgent()) + + plat := ua.Platform() + if plat == "" { + plat = "unknown" + } + + os := ua.OS() + if os == "" { + os = "unknown" + } + + bname, bversion := ua.Browser() + if bname == "" { + bname = "unknown" + } + + if bversion == "" { + bversion = "0.0" + } + + session.AddProp(model.SESSION_PROP_PLATFORM, plat) + session.AddProp(model.SESSION_PROP_OS, os) + session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion)) + + if result := <-Srv.Store.Session().Save(session); result.Err != nil { + c.Err = result.Err + c.Err.StatusCode = http.StatusForbidden + return + } else { + session = result.Data.(*model.Session) + sessionCache.Add(session.Id, session) + } + + w.Header().Set(model.HEADER_TOKEN, session.Id) + sessionCookie := &http.Cookie{ + Name: model.SESSION_TOKEN, + Value: session.Id, + Path: "/", + MaxAge: maxAge, + HttpOnly: true, + } + + http.SetCookie(w, sessionCookie) + user.Sanitize(map[string]bool{}) + + c.Session = *session + c.LogAuditWithUserId(user.Id, "success") + + w.Write([]byte(result.Data.(*model.User).ToJson())) +} + +func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + altId := props["id"] + + if result := <-Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil { + c.Err = result.Err + return + } else { + sessions := result.Data.([]*model.Session) + + for _, session := range sessions { + if session.AltId == altId { + c.LogAudit("session_id=" + session.AltId) + sessionCache.Remove(session.Id) + if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + c.Err = result.Err + return + } else { + w.Write([]byte(model.MapToJson(props))) + return + } + } + } + } +} + +func RevokeAllSession(c *Context, userId string) { + if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil { + c.Err = result.Err + return + } else { + sessions := result.Data.([]*model.Session) + + for _, session := range sessions { + c.LogAuditWithUserId(userId, "session_id="+session.AltId) + sessionCache.Remove(session.Id) + if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + c.Err = result.Err + return + } + } + } +} + +func getSessions(c *Context, w http.ResponseWriter, r *http.Request) { + + params := mux.Vars(r) + id := params["id"] + + if !c.HasPermissionsToUser(id, "getAudits") { + return + } + + if result := <-Srv.Store.Session().GetSessions(id); result.Err != nil { + c.Err = result.Err + return + } else { + sessions := result.Data.([]*model.Session) + for _, session := range sessions { + session.Sanitize() + } + + w.Write([]byte(model.SessionsToJson(sessions))) + } +} + +func logout(c *Context, w http.ResponseWriter, r *http.Request) { + data := make(map[string]string) + data["user_id"] = c.Session.UserId + + Logout(c, w, r) + if c.Err == nil { + w.Write([]byte(model.MapToJson(data))) + } +} + +func Logout(c *Context, w http.ResponseWriter, r *http.Request) { + c.LogAudit("") + c.RemoveSessionCookie(w) + if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil { + c.Err = result.Err + return + } +} + +func getMe(c *Context, w http.ResponseWriter, r *http.Request) { + + if len(c.Session.UserId) == 0 { + return + } + + if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + c.Err = result.Err + c.RemoveSessionCookie(w) + l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) + return + } else if HandleEtag(result.Data.(*model.User).Etag(), w, r) { + return + } else { + result.Data.(*model.User).Sanitize(map[string]bool{}) + w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag()) + w.Header().Set("Expires", "-1") + w.Write([]byte(result.Data.(*model.User).ToJson())) + return + } +} + +func getUser(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + if !c.HasPermissionsToUser(id, "getUser") { + return + } + + if result := <-Srv.Store.User().Get(id); result.Err != nil { + c.Err = result.Err + return + } else if HandleEtag(result.Data.(*model.User).Etag(), w, r) { + return + } else { + result.Data.(*model.User).Sanitize(map[string]bool{}) + w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag()) + w.Write([]byte(result.Data.(*model.User).ToJson())) + return + } +} + +func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { + + etag := (<-Srv.Store.User().GetEtagForProfiles(c.Session.TeamId)).Data.(string) + if HandleEtag(etag, w, r) { + return + } + + if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + profiles := result.Data.(map[string]*model.User) + + for k, p := range profiles { + options := utils.SanitizeOptions + options["passwordupdate"] = false + p.Sanitize(options) + profiles[k] = p + } + + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + w.Header().Set("Cache-Control", "max-age=120, public") // 2 mins + w.Write([]byte(model.UserMapToJson(profiles))) + return + } +} + +func getAudits(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + if !c.HasPermissionsToUser(id, "getAudits") { + return + } + + userChan := Srv.Store.User().Get(id) + auditChan := 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 + return + } else { + audits := result.Data.(model.Audits) + etag := audits.Etag() + + if HandleEtag(etag, w, r) { + return + } + + if len(etag) > 0 { + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + } + + w.Write([]byte(audits.ToJson())) + return + } +} + +func createProfileImage(username string, userId string) *image.RGBA { + + 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() + + initials := "" + parts := strings.Split(username, " ") + + for _, v := range parts { + + if len(v) > 0 { + initials += string(strings.ToUpper(v)[0]) + } + } + + if len(initials) == 0 { + initials = "^" + } + + if len(initials) > 2 { + initials = initials[0:2] + } + + draw2d.SetFontFolder(utils.FindDir("web/static/fonts")) + i := image.NewRGBA(image.Rect(0, 0, 128, 128)) + gc := draw2d.NewGraphicContext(i) + draw2d.Rect(gc, 0, 0, 128, 128) + gc.SetFillColor(colors[int(seed)%len(colors)]) + gc.Fill() + gc.SetFontSize(50) + gc.SetFontData(draw2d.FontData{"luxi", draw2d.FontFamilyMono, draw2d.FontStyleBold | draw2d.FontStyleItalic}) + left, top, right, bottom := gc.GetStringBounds("CH") + width := (128 - (right - left + 10)) / 2 + height := (128 - (top - bottom + 6)) / 2 + gc.Translate(width, height) + gc.SetFillColor(image.White) + gc.FillString(initials) + return i +} + +func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.IsS3Configured() { + c.Err = model.NewAppError("getProfileImage", "Unable to get image. Amazon S3 not configured. ", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + params := mux.Vars(r) + id := params["id"] + + if result := <-Srv.Store.User().Get(id); result.Err != nil { + c.Err = result.Err + return + } else { + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + path := "teams/" + c.Session.TeamId + "/users/" + id + "/profile.png" + + var img []byte + + if data, getErr := bucket.Get(path); getErr != nil { + rawImg := createProfileImage(result.Data.(*model.User).Username, id) + buf := new(bytes.Buffer) + + if imgErr := png.Encode(buf, rawImg); imgErr != nil { + c.Err = model.NewAppError("getProfileImage", "Could not encode default profile image", imgErr.Error()) + return + } else { + img = buf.Bytes() + } + + options := s3.Options{} + if err := bucket.Put(path, buf.Bytes(), "image", s3.Private, options); err != nil { + c.Err = model.NewAppError("getImage", "Couldn't upload default profile image", err.Error()) + return + } + + } else { + img = data + } + + 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 + } + + w.Write(img) + } +} + +func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.IsS3Configured() { + c.Err = model.NewAppError("uploadProfileImage", "Unable to upload image. Amazon S3 not configured. ", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if err := r.ParseMultipartForm(10000000); err != nil { + c.Err = model.NewAppError("uploadProfileImage", "Could not parse multipart form", "") + return + } + + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + m := r.MultipartForm + + imageArray, ok := m.File["image"] + if !ok { + c.Err = model.NewAppError("uploadProfileImage", "No file under 'image' in request", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if len(imageArray) <= 0 { + c.Err = model.NewAppError("uploadProfileImage", "Empty array under 'image' in request", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + imageData := imageArray[0] + + file, err := imageData.Open() + defer file.Close() + if err != nil { + c.Err = model.NewAppError("uploadProfileImage", "Could not open image file", err.Error()) + return + } + + // Decode image into Image object + img, _, err := image.Decode(file) + if err != nil { + c.Err = model.NewAppError("uploadProfileImage", "Could not decode profile image", err.Error()) + return + } + + // Scale profile image + img = resize.Resize(utils.Cfg.ImageSettings.ProfileWidth, utils.Cfg.ImageSettings.ProfileHeight, img, resize.Lanczos3) + + buf := new(bytes.Buffer) + err = png.Encode(buf, img) + if err != nil { + c.Err = model.NewAppError("uploadProfileImage", "Could not encode profile image", err.Error()) + return + } + + path := "teams/" + c.Session.TeamId + "/users/" + c.Session.UserId + "/profile.png" + + options := s3.Options{} + if err := bucket.Put(path, buf.Bytes(), "image", s3.Private, options); err != nil { + c.Err = model.NewAppError("uploadProfileImage", "Couldn't upload profile image", "") + return + } + + c.LogAudit("") +} + +func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { + user := model.UserFromJson(r.Body) + + if user == nil { + c.SetInvalidParam("updateUser", "user") + return + } + + if !c.HasPermissionsToUser(user.Id, "updateUsers") { + return + } + + if result := <-Srv.Store.User().Update(user, false); result.Err != nil { + c.Err = result.Err + return + } else { + c.LogAudit("") + + rusers := result.Data.([2]*model.User) + + if rusers[0].Email != rusers[1].Email { + if tresult := <-Srv.Store.Team().Get(rusers[1].TeamId); tresult.Err != nil { + l4g.Error(tresult.Err.Message) + } else { + fireAndForgetEmailChangeEmail(rusers[1].Email, tresult.Data.(*model.Team).Name, c.TeamUrl) + } + } + + rusers[0].Password = "" + rusers[0].AuthData = "" + w.Write([]byte(rusers[0].ToJson())) + } +} + +func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { + c.LogAudit("attempted") + + props := model.MapFromJson(r.Body) + userId := props["user_id"] + if len(userId) != 26 { + c.SetInvalidParam("updatePassword", "user_id") + return + } + + currentPassword := props["current_password"] + if len(currentPassword) <= 0 { + c.SetInvalidParam("updatePassword", "current_password") + return + } + + newPassword := props["new_password"] + if len(newPassword) < 5 { + c.SetInvalidParam("updatePassword", "new_password") + return + } + + if userId != c.Session.UserId { + c.Err = model.NewAppError("updatePassword", "Update password failed because context user_id did not match props user_id", "") + c.Err.StatusCode = http.StatusForbidden + return + } + + var result store.StoreResult + + if result = <-Srv.Store.User().Get(userId); result.Err != nil { + c.Err = result.Err + return + } + + if result.Data == nil { + c.Err = model.NewAppError("updatePassword", "Update password failed because we couldn't find a valid account", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + user := result.Data.(*model.User) + + tchan := Srv.Store.Team().Get(user.TeamId) + + if !model.ComparePassword(user.Password, currentPassword) { + c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if uresult := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(newPassword)); uresult.Err != nil { + c.Err = uresult.Err + return + } else { + c.LogAudit("completed") + + if tresult := <-tchan; tresult.Err != nil { + l4g.Error(tresult.Err.Message) + } else { + fireAndForgetPasswordChangeEmail(user.Email, tresult.Data.(*model.Team).Name, c.TeamUrl, "using the settings menu") + } + + data := make(map[string]string) + data["user_id"] = uresult.Data.(string) + w.Write([]byte(model.MapToJson(data))) + } +} + +func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + user_id := props["user_id"] + if len(user_id) != 26 { + c.SetInvalidParam("updateRoles", "user_id") + return + } + + new_roles := props["new_roles"] + // no check since we allow the clearing of Roles + + var user *model.User + if result := <-Srv.Store.User().Get(user_id); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if !c.HasPermissionsToTeam(user.TeamId, "updateRoles") { + return + } + + if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() { + c.Err = model.NewAppError("updateRoles", "You do not have the appropriate permissions", "userId="+user_id) + c.Err.StatusCode = http.StatusForbidden + return + } + + // make sure there is at least 1 other active admin + if strings.Contains(user.Roles, model.ROLE_ADMIN) && !strings.Contains(new_roles, model.ROLE_ADMIN) { + if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + activeAdmins := -1 + profileUsers := result.Data.(map[string]*model.User) + for _, profileUser := range profileUsers { + if profileUser.DeleteAt == 0 && strings.Contains(profileUser.Roles, model.ROLE_ADMIN) { + activeAdmins = activeAdmins + 1 + } + } + + if activeAdmins <= 0 { + c.Err = model.NewAppError("updateRoles", "There must be at least one active admin", "userId="+user_id) + return + } + } + } + + user.Roles = new_roles + + if result := <-Srv.Store.User().Update(user, true); result.Err != nil { + c.Err = result.Err + return + } else { + c.LogAuditWithUserId(user.Id, "roles="+new_roles) + + ruser := result.Data.([2]*model.User)[0] + options := utils.SanitizeOptions + options["passwordupdate"] = false + ruser.Sanitize(options) + w.Write([]byte(ruser.ToJson())) + } +} + +func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + user_id := props["user_id"] + if len(user_id) != 26 { + c.SetInvalidParam("updateActive", "user_id") + return + } + + active := props["active"] == "true" + + var user *model.User + if result := <-Srv.Store.User().Get(user_id); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if !c.HasPermissionsToTeam(user.TeamId, "updateActive") { + return + } + + if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() { + c.Err = model.NewAppError("updateActive", "You do not have the appropriate permissions", "userId="+user_id) + c.Err.StatusCode = http.StatusForbidden + return + } + + // make sure there is at least 1 other active admin + if !active && strings.Contains(user.Roles, model.ROLE_ADMIN) { + if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + activeAdmins := -1 + profileUsers := result.Data.(map[string]*model.User) + for _, profileUser := range profileUsers { + if profileUser.DeleteAt == 0 && strings.Contains(profileUser.Roles, model.ROLE_ADMIN) { + activeAdmins = activeAdmins + 1 + } + } + + if activeAdmins <= 0 { + c.Err = model.NewAppError("updateRoles", "There must be at least one active admin", "userId="+user_id) + return + } + } + } + + if active { + user.DeleteAt = 0 + } else { + user.DeleteAt = model.GetMillis() + } + + if result := <-Srv.Store.User().Update(user, true); result.Err != nil { + c.Err = result.Err + return + } else { + c.LogAuditWithUserId(user.Id, fmt.Sprintf("active=%v", active)) + + if user.DeleteAt > 0 { + RevokeAllSession(c, user.Id) + } + + ruser := result.Data.([2]*model.User)[0] + options := utils.SanitizeOptions + options["passwordupdate"] = false + ruser.Sanitize(options) + w.Write([]byte(ruser.ToJson())) + } +} + +func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("sendPasswordReset", "email") + return + } + + domain := props["domain"] + if len(domain) == 0 { + c.SetInvalidParam("sendPasswordReset", "domain") + return + } + + var team *model.Team + if result := <-Srv.Store.Team().GetByDomain(domain); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.Err = model.NewAppError("sendPasswordReset", "We couldn’t find an account with that address.", "email="+email+" team_id="+team.Id) + return + } else { + user = result.Data.(*model.User) + } + + newProps := make(map[string]string) + newProps["user_id"] = user.Id + newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) + + data := model.MapToJson(newProps) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.ResetSalt)) + + link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.TeamUrl, url.QueryEscape(data), url.QueryEscape(hash)) + + subjectPage := NewServerTemplatePage("reset_subject", c.TeamUrl) + bodyPage := NewServerTemplatePage("reset_body", c.TeamUrl) + bodyPage.Props["ResetUrl"] = link + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + c.Err = model.NewAppError("sendPasswordReset", "Failed to send password reset email successfully", "err="+err.Message) + return + } + + c.LogAuditWithUserId(user.Id, "sent="+email) + + w.Write([]byte(model.MapToJson(props))) +} + +func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + newPassword := props["new_password"] + if len(newPassword) < 5 { + c.SetInvalidParam("resetPassword", "new_password") + return + } + + hash := props["hash"] + if len(hash) == 0 { + c.SetInvalidParam("resetPassword", "hash") + return + } + + data := model.MapFromJson(strings.NewReader(props["data"])) + + userId := data["user_id"] + if len(userId) != 26 { + c.SetInvalidParam("resetPassword", "data:user_id") + return + } + + timeStr := data["time"] + if len(timeStr) == 0 { + c.SetInvalidParam("resetPassword", "data:time") + return + } + + domain := props["domain"] + if len(domain) == 0 { + c.SetInvalidParam("resetPassword", "domain") + return + } + + c.LogAuditWithUserId(userId, "attempt") + + var team *model.Team + if result := <-Srv.Store.Team().GetByDomain(domain); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-Srv.Store.User().Get(userId); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + if user.TeamId != team.Id { + c.Err = model.NewAppError("resetPassword", "Trying to reset password for user on wrong team.", "userId="+user.Id+", teamId="+team.Id) + c.Err.StatusCode = http.StatusForbidden + return + } + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.ServiceSettings.ResetSalt)) { + c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "") + return + } + + t, err := strconv.ParseInt(timeStr, 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour + c.Err = model.NewAppError("resetPassword", "The reset link has expired", "") + return + } + + if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil { + c.Err = result.Err + return + } else { + c.LogAuditWithUserId(userId, "success") + } + + fireAndForgetPasswordChangeEmail(user.Email, team.Name, c.TeamUrl, "using a reset password link") + + props["new_password"] = "" + w.Write([]byte(model.MapToJson(props))) +} + +func fireAndForgetPasswordChangeEmail(email, teamName, teamUrl, method string) { + go func() { + + subjectPage := NewServerTemplatePage("password_change_subject", teamUrl) + subjectPage.Props["TeamName"] = teamName + bodyPage := NewServerTemplatePage("password_change_body", teamUrl) + bodyPage.Props["TeamName"] = teamName + bodyPage.Props["Method"] = method + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("Failed to send update password email successfully err=%v", err) + } + + }() +} + +func fireAndForgetEmailChangeEmail(email, teamName, teamUrl string) { + go func() { + + subjectPage := NewServerTemplatePage("email_change_subject", teamUrl) + subjectPage.Props["TeamName"] = teamName + bodyPage := NewServerTemplatePage("email_change_body", teamUrl) + bodyPage.Props["TeamName"] = teamName + + if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error("Failed to send update password email successfully err=%v", err) + } + + }() +} + +func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + user_id := props["user_id"] + if len(user_id) != 26 { + c.SetInvalidParam("updateUserNotify", "user_id") + return + } + + uchan := Srv.Store.User().Get(user_id) + + if !c.HasPermissionsToUser(user_id, "updateUserNotify") { + return + } + + delete(props, "user_id") + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("updateUserNotify", "email") + return + } + + desktop_sound := props["desktop_sound"] + if len(desktop_sound) == 0 { + c.SetInvalidParam("updateUserNotify", "desktop_sound") + return + } + + desktop := props["desktop"] + if len(desktop) == 0 { + c.SetInvalidParam("updateUserNotify", "desktop") + return + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + user.NotifyProps = props + + if result := <-Srv.Store.User().Update(user, false); result.Err != nil { + c.Err = result.Err + return + } else { + c.LogAuditWithUserId(user.Id, "") + + ruser := result.Data.([2]*model.User)[0] + options := utils.SanitizeOptions + options["passwordupdate"] = false + ruser.Sanitize(options) + w.Write([]byte(ruser.ToJson())) + } +} + +func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { + + if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + + profiles := result.Data.(map[string]*model.User) + + statuses := map[string]string{} + for _, profile := range profiles { + if profile.IsOffline() { + statuses[profile.Id] = model.USER_OFFLINE + } else if profile.IsAway() { + statuses[profile.Id] = model.USER_AWAY + } else { + statuses[profile.Id] = model.USER_ONLINE + } + } + + //w.Header().Set("Cache-Control", "max-age=9, public") // 2 mins + w.Write([]byte(model.MapToJson(statuses))) + return + } +} diff --git a/api/user_test.go b/api/user_test.go new file mode 100644 index 000000000..4d5d2b3f0 --- /dev/null +++ b/api/user_test.go @@ -0,0 +1,960 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "bytes" + "fmt" + "github.com/goamz/goamz/aws" + "github.com/goamz/goamz/s3" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "image/color" + "io" + "mime/multipart" + "net/http" + "os" + "strings" + "testing" + "time" +) + +func TestCreateUser(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "hello"} + + ruser, err := Client.CreateUser(&user, "") + if err != nil { + t.Fatal(err) + } + + if ruser.Data.(*model.User).FullName != user.FullName { + t.Fatal("full name didn't match") + } + + if ruser.Data.(*model.User).Password != "" { + t.Fatal("password wasn't blank") + } + + if _, err := Client.CreateUser(ruser.Data.(*model.User), ""); err == nil { + t.Fatal("Cannot create an existing") + } + + ruser.Data.(*model.User).Id = "" + if _, err := Client.CreateUser(ruser.Data.(*model.User), ""); err != nil { + if err.Message != "An account with that email already exists." { + t.Fatal(err) + } + } + + ruser.Data.(*model.User).Email = "" + if _, err := Client.CreateUser(ruser.Data.(*model.User), ""); err != nil { + if err.Message != "Invalid email" { + t.Fatal(err) + } + } + + user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "hello", Username: model.BOT_USERNAME} + + if _, err := Client.CreateUser(&user2, ""); err == nil { + t.Fatal("Should have failed using reserved bot name") + } + + if _, err := Client.DoPost("/users/create", "garbage"); err == nil { + t.Fatal("should have been an error") + } +} + +func TestCreateUserAllowedDomains(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_INVITE, AllowedDomains: "spinpunch.com, @nowh.com,@hello.com"} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "hello"} + + _, err := Client.CreateUser(&user, "") + if err == nil { + t.Fatal("should have failed") + } + + user.Email = "test@nowh.com" + _, err = Client.CreateUser(&user, "") + if err != nil { + t.Fatal(err) + } +} + +func TestLogin(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + ruser, _ := Client.CreateUser(&user, "") + Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id) + + if result, err := Client.LoginById(ruser.Data.(*model.User).Id, user.Password); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).Email != user.Email { + t.Fatal("email's didn't match") + } + } + + if result, err := Client.LoginByEmail(team.Domain, user.Email, user.Password); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).Email != user.Email { + t.Fatal("emails didn't match") + } + } + + if _, err := Client.LoginByEmail(team.Domain, user.Email, user.Password+"invalid"); err == nil { + t.Fatal("Invalid Password") + } + + if _, err := Client.LoginByEmail(team.Domain, "", user.Password); err == nil { + t.Fatal("should have failed") + } + + authToken := Client.AuthToken + Client.AuthToken = "invalid" + + if _, err := Client.GetUser(ruser.Data.(*model.User).Id, ""); err == nil { + t.Fatal("should have failed") + } + + Client.AuthToken = "" + + team2 := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_INVITE} + rteam2 := Client.Must(Client.CreateTeam(&team2)) + + user2 := model.User{TeamId: rteam2.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + + if _, err := Client.CreateUserFromSignup(&user2, "junk", "1231312"); err == nil { + t.Fatal("Should have errored, signed up without hashed email") + } + + props := make(map[string]string) + props["email"] = user2.Email + props["id"] = rteam2.Data.(*model.Team).Id + props["name"] = rteam2.Data.(*model.Team).Name + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + data := model.MapToJson(props) + hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) + + ruser2, _ := Client.CreateUserFromSignup(&user2, data, hash) + + if _, err := Client.LoginByEmail(team2.Domain, ruser2.Data.(*model.User).Email, user2.Password); err != nil { + t.Fatal("From verfied hash") + } + + Client.AuthToken = authToken +} + +func TestSessions(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(ruser.Id) + + deviceId := model.NewId() + Client.LoginByEmailWithDevice(team.Domain, user.Email, user.Password, deviceId) + Client.LoginByEmail(team.Domain, user.Email, user.Password) + + r1, err := Client.GetSessions(ruser.Id) + if err != nil { + t.Fatal(err) + } + + sessions := r1.Data.([]*model.Session) + otherSession := "" + + if len(sessions) != 2 { + t.Fatal("invalid number of sessions") + } + + for _, session := range sessions { + if session.DeviceId == deviceId { + otherSession = session.AltId + } + + if len(session.Id) != 0 { + t.Fatal("shouldn't return sessions") + } + } + + if _, err := Client.RevokeSession(otherSession); err != nil { + t.Fatal(err) + } + + r2, err := Client.GetSessions(ruser.Id) + if err != nil { + t.Fatal(err) + } + + sessions2 := r2.Data.([]*model.Session) + + if len(sessions2) != 1 { + t.Fatal("invalid number of sessions") + } + + if _, err := Client.RevokeSession(otherSession); err != nil { + t.Fatal(err) + } + +} + +func TestGetUser(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + ruser, _ := Client.CreateUser(&user, "") + Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id) + + user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + ruser2, _ := Client.CreateUser(&user2, "") + Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id) + + Client.LoginByEmail(team.Domain, user.Email, user.Password) + + rId := ruser.Data.(*model.User).Id + if result, err := Client.GetUser(rId, ""); err != nil { + t.Fatal("Failed to get user") + } else { + if result.Data.(*model.User).Password != "" { + t.Fatal("User shouldn't have any password data once set") + } + + if cache_result, err := Client.GetUser(rId, result.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(*model.User) != nil { + t.Fatal("cache should be empty") + } + } + + if result, err := Client.GetMe(""); err != nil { + t.Fatal("Failed to get user") + } else { + if result.Data.(*model.User).Password != "" { + t.Fatal("User shouldn't have any password data once set") + } + } + + if _, err := Client.GetUser("FORBIDDENERROR", ""); err == nil { + t.Fatal("shouldn't exist") + } + + if _, err := Client.GetUser(ruser2.Data.(*model.User).Id, ""); err == nil { + t.Fatal("shouldn't have accss") + } + + if userMap, err := Client.GetProfiles(rteam.Data.(*model.Team).Id, ""); err != nil { + t.Fatal(err) + } else if len(userMap.Data.(map[string]*model.User)) != 2 { + t.Fatal("should have been 2") + } else if userMap.Data.(map[string]*model.User)[rId].Id != rId { + t.Fatal("should have been valid") + } else { + + // test etag caching + if cache_result, err := Client.GetProfiles(rteam.Data.(*model.Team).Id, userMap.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(map[string]*model.User) != nil { + t.Log(cache_result.Data) + t.Fatal("cache should be empty") + } + + } + + Client.AuthToken = "" + if _, err := Client.GetUser(ruser2.Data.(*model.User).Id, ""); err == nil { + t.Fatal("shouldn't have accss") + } +} + +func TestGetAudits(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + ruser, _ := Client.CreateUser(&user, "") + Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id) + + Client.LoginByEmail(team.Domain, user.Email, user.Password) + + time.Sleep(500 * time.Millisecond) + + if result, err := Client.GetAudits(ruser.Data.(*model.User).Id, ""); err != nil { + t.Fatal(err) + } else { + + if len(result.Data.(model.Audits)) != 2 { + t.Fatal(result.Data.(model.Audits)) + } + + if cache_result, err := Client.GetAudits(ruser.Data.(*model.User).Id, result.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(model.Audits) != nil { + t.Fatal("cache should be empty") + } + } + + if _, err := Client.GetAudits("FORBIDDENERROR", ""); err == nil { + t.Fatal("audit log shouldn't exist") + } +} + +func TestUserCreateImage(t *testing.T) { + Setup() + + i := createProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba") + if i == nil { + t.Fatal("Failed to gen image") + } + + colorful := color.RGBA{116, 49, 196, 255} + + if i.RGBAAt(1, 1) != colorful { + t.Fatal("Failed to create correct color") + } + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + Client.DoGet("/users/"+user.Id+"/image", "", "") + +} + +func TestUserUploadProfileImage(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + if utils.IsS3Configured() { + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr == nil { + t.Fatal("Should have errored") + } + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr == nil { + t.Fatal("Should have errored") + } + + part, err := writer.CreateFormFile("blargh", "test.png") + if err != nil { + t.Fatal(err) + } + + path := utils.FindDir("web/static/images") + file, err := os.Open(path + "/test.png") + if err != nil { + t.Fatal(err) + } + defer file.Close() + + _, err = io.Copy(part, file) + if err != nil { + t.Fatal(err) + } + + if err := writer.Close(); err != nil { + t.Fatal(err) + } + + if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr == nil { + t.Fatal("Should have errored") + } + + file2, err := os.Open(path + "/test.png") + if err != nil { + t.Fatal(err) + } + defer file2.Close() + + body = &bytes.Buffer{} + writer = multipart.NewWriter(body) + + part, err = writer.CreateFormFile("image", "test.png") + if err != nil { + t.Fatal(err) + } + + if _, err := io.Copy(part, file2); err != nil { + t.Fatal(err) + } + + if err := writer.Close(); err != nil { + t.Fatal(err) + } + + if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr != nil { + t.Fatal(upErr) + } + + Client.DoGet("/users/"+user.Id+"/image", "", "") + + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + if err := bucket.Del("teams/" + user.TeamId + "/users/" + user.Id + "/profile.png"); err != nil { + t.Fatal(err) + } + } else { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + if _, upErr := Client.UploadFile("/users/newimage", body.Bytes(), writer.FormDataContentType()); upErr.StatusCode != http.StatusNotImplemented { + t.Fatal("Should have failed with 501 - Not Implemented") + } + } +} + +func TestUserUpdate(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + time1 := model.GetMillis() + + user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd", LastActivityAt: time1, LastPingAt: time1, Roles: ""} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + if _, err := Client.UpdateUser(user); err == nil { + t.Fatal("Should have errored") + } + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + time2 := model.GetMillis() + + user.FullName = "Jim Jimmy" + user.TeamId = "12345678901234567890123456" + user.LastActivityAt = time2 + user.LastPingAt = time2 + user.Roles = model.ROLE_ADMIN + user.LastPasswordUpdate = 123 + + if result, err := Client.UpdateUser(user); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).FullName != "Jim Jimmy" { + t.Fatal("FullName did not update properly") + } + if result.Data.(*model.User).TeamId != team.Id { + t.Fatal("TeamId should not have updated") + } + if result.Data.(*model.User).LastActivityAt != time1 { + t.Fatal("LastActivityAt should not have updated") + } + if result.Data.(*model.User).LastPingAt != time1 { + t.Fatal("LastPingAt should not have updated") + } + if result.Data.(*model.User).Roles != "" { + t.Fatal("Roles should not have updated") + } + if result.Data.(*model.User).LastPasswordUpdate == 123 { + t.Fatal("LastPasswordUpdate should not have updated") + } + } + + user.TeamId = "junk" + if _, err := Client.UpdateUser(user); err == nil { + t.Fatal("Should have errored - tried to change teamId to junk") + } + + user.TeamId = team.Id + if _, err := Client.UpdateUser(nil); err == nil { + t.Fatal("Should have errored") + } + + user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + user.FullName = "Tim Timmy" + + if _, err := Client.UpdateUser(user); err == nil { + t.Fatal("Should have errored") + } +} + +func TestUserUpdatePassword(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + if _, err := Client.UpdateUserPassword(user.Id, "pwd", "newpwd"); err == nil { + t.Fatal("Should have errored") + } + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + if _, err := Client.UpdateUserPassword("123", "pwd", "newpwd"); err == nil { + t.Fatal("Should have errored") + } + + if _, err := Client.UpdateUserPassword(user.Id, "", "newpwd"); err == nil { + t.Fatal("Should have errored") + } + + if _, err := Client.UpdateUserPassword(user.Id, "pwd", "npwd"); err == nil { + t.Fatal("Should have errored") + } + + if _, err := Client.UpdateUserPassword("12345678901234567890123456", "pwd", "newpwd"); err == nil { + t.Fatal("Should have errored") + } + + if _, err := Client.UpdateUserPassword(user.Id, "badpwd", "newpwd"); err == nil { + t.Fatal("Should have errored") + } + + if _, err := Client.UpdateUserPassword(user.Id, "pwd", "newpwd"); err != nil { + t.Fatal(err) + } + + updatedUser := Client.Must(Client.GetUser(user.Id, "")).Data.(*model.User) + if updatedUser.LastPasswordUpdate == user.LastPasswordUpdate { + t.Fatal("LastPasswordUpdate should have changed") + } + + if _, err := Client.LoginByEmail(team.Domain, user.Email, "newpwd"); err != nil { + t.Fatal(err) + } + + user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + if _, err := Client.UpdateUserPassword(user.Id, "pwd", "newpwd"); err == nil { + t.Fatal("Should have errored") + } +} + +func TestUserUpdateRoles(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: "test@nowhere.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + data := make(map[string]string) + data["user_id"] = user.Id + data["new_roles"] = "" + + if _, err := Client.UpdateUserRoles(data); err == nil { + t.Fatal("Should have errored, not logged in") + } + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + if _, err := Client.UpdateUserRoles(data); err == nil { + t.Fatal("Should have errored, not admin") + } + + team2 := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user3 := &model.User{TeamId: team2.Id, Email: "test@nowhere.com", FullName: "Corey Hulen", Password: "pwd"} + user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user3.Id) + + Client.LoginByEmail(team2.Domain, user3.Email, "pwd") + + data["user_id"] = user2.Id + + if _, err := Client.UpdateUserRoles(data); err == nil { + t.Fatal("Should have errored, wrong team") + } + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + data["user_id"] = "junk" + data["new_roles"] = "admin" + + if _, err := Client.UpdateUserRoles(data); err == nil { + t.Fatal("Should have errored, bad id") + } + + data["user_id"] = "12345678901234567890123456" + + if _, err := Client.UpdateUserRoles(data); err == nil { + t.Fatal("Should have errored, bad id") + } + + data["user_id"] = user2.Id + + if result, err := Client.UpdateUserRoles(data); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).Roles != "admin" { + t.Fatal("Roles did not update properly") + } + } +} + +func TestUserUpdateActive(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: "test@nowhere.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + if _, err := Client.UpdateActive(user.Id, false); err == nil { + t.Fatal("Should have errored, not logged in") + } + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + if _, err := Client.UpdateActive(user.Id, false); err == nil { + t.Fatal("Should have errored, not admin") + } + + team2 := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user3 := &model.User{TeamId: team2.Id, Email: "test@nowhere.com", FullName: "Corey Hulen", Password: "pwd"} + user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user3.Id) + + Client.LoginByEmail(team2.Domain, user3.Email, "pwd") + + if _, err := Client.UpdateActive(user.Id, false); err == nil { + t.Fatal("Should have errored, wrong team") + } + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + if _, err := Client.UpdateActive("junk", false); err == nil { + t.Fatal("Should have errored, bad id") + } + + if _, err := Client.UpdateActive("12345678901234567890123456", false); err == nil { + t.Fatal("Should have errored, bad id") + } + + if result, err := Client.UpdateActive(user2.Id, false); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).DeleteAt == 0 { + t.Fatal("active did not update properly") + } + } + + if result, err := Client.UpdateActive(user2.Id, true); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).DeleteAt != 0 { + t.Fatal("active did not update properly true") + } + } +} + +func TestSendPasswordReset(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + data := make(map[string]string) + data["email"] = user.Email + data["domain"] = team.Domain + + if _, err := Client.SendPasswordReset(data); err != nil { + t.Fatal(err) + } + + data["email"] = "" + if _, err := Client.SendPasswordReset(data); err == nil { + t.Fatal("Should have errored - no email") + } + + data["email"] = "junk@junk.com" + if _, err := Client.SendPasswordReset(data); err == nil { + t.Fatal("Should have errored - bad email") + } + + data["email"] = user.Email + data["domain"] = "" + if _, err := Client.SendPasswordReset(data); err == nil { + t.Fatal("Should have errored - no domain") + } + + data["domain"] = "junk" + if _, err := Client.SendPasswordReset(data); err == nil { + t.Fatal("Should have errored - bad domain") + } +} + +func TestResetPassword(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + data := make(map[string]string) + data["new_password"] = "newpwd" + props := make(map[string]string) + props["user_id"] = user.Id + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + data["data"] = model.MapToJson(props) + data["hash"] = model.HashPassword(fmt.Sprintf("%v:%v", data["data"], utils.Cfg.ServiceSettings.ResetSalt)) + data["domain"] = team.Domain + + if _, err := Client.ResetPassword(data); err != nil { + t.Fatal(err) + } + + data["new_password"] = "" + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - no password") + } + + data["new_password"] = "npwd" + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - password too short") + } + + data["new_password"] = "newpwd" + data["hash"] = "" + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - no hash") + } + + data["hash"] = "junk" + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - bad hash") + } + + props["user_id"] = "" + data["data"] = model.MapToJson(props) + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - no user id") + } + + data["user_id"] = "12345678901234567890123456" + data["data"] = model.MapToJson(props) + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - bad user id") + } + + props["user_id"] = user.Id + props["time"] = "" + data["data"] = model.MapToJson(props) + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - no time") + } + + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + data["data"] = model.MapToJson(props) + data["domain"] = "" + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - no domain") + } + + data["domain"] = "junk" + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - bad domain") + } + + team2 := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test2@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + data["domain"] = team2.Domain + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("Should have errored - domain team doesn't match user team") + } +} + +func TestUserUpdateNotify(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd", Roles: ""} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + data := make(map[string]string) + data["user_id"] = user.Id + data["email"] = "true" + data["desktop"] = "all" + data["desktop_sound"] = "false" + + if _, err := Client.UpdateUserNotify(data); err == nil { + t.Fatal("Should have errored - not logged in") + } + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + if result, err := Client.UpdateUserNotify(data); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.User).NotifyProps["desktop"] != data["desktop"] { + t.Fatal("NotifyProps did not update properly - desktop") + } + if result.Data.(*model.User).NotifyProps["desktop_sound"] != data["desktop_sound"] { + t.Fatal("NotifyProps did not update properly - desktop_sound") + } + if result.Data.(*model.User).NotifyProps["email"] != data["email"] { + t.Fatal("NotifyProps did not update properly - email") + } + } + + if _, err := Client.UpdateUserNotify(nil); err == nil { + t.Fatal("Should have errored") + } + + data["user_id"] = "junk" + if _, err := Client.UpdateUserNotify(data); err == nil { + t.Fatal("Should have errored - junk user id") + } + + data["user_id"] = "12345678901234567890123456" + if _, err := Client.UpdateUserNotify(data); err == nil { + t.Fatal("Should have errored - bad user id") + } + + data["user_id"] = user.Id + data["desktop"] = "" + if _, err := Client.UpdateUserNotify(data); err == nil { + t.Fatal("Should have errored - empty desktop notify") + } + + data["desktop"] = "all" + data["desktop_sound"] = "" + if _, err := Client.UpdateUserNotify(data); err == nil { + t.Fatal("Should have errored - empty desktop sound") + } + + data["desktop_sound"] = "false" + data["email"] = "" + if _, err := Client.UpdateUserNotify(data); err == nil { + t.Fatal("Should have errored - empty email") + } +} + +func TestFuzzyUserCreate(t *testing.T) { + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + for i := 0; i < len(utils.FUZZY_STRINGS_NAMES) || i < len(utils.FUZZY_STRINGS_EMAILS); i++ { + testName := "Name" + testEmail := "test@nowhere.com" + + if i < len(utils.FUZZY_STRINGS_NAMES) { + testName = utils.FUZZY_STRINGS_NAMES[i] + } + if i < len(utils.FUZZY_STRINGS_EMAILS) { + testEmail = utils.FUZZY_STRINGS_EMAILS[i] + } + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + testEmail, FullName: testName, Password: "hello"} + + _, err := Client.CreateUser(&user, "") + if err != nil { + t.Fatal(err) + } + } +} + +func TestStatuses(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(ruser.Id) + + Client.LoginByEmail(team.Domain, user.Email, user.Password) + + r1, err := Client.GetStatuses() + if err != nil { + t.Fatal(err) + } + + statuses := r1.Data.(map[string]string) + + if len(statuses) != 1 { + t.Fatal("invalid number of statuses") + } + + for _, status := range statuses { + if status != model.USER_OFFLINE && status != model.USER_AWAY && status != model.USER_ONLINE { + t.Fatal("one of the statuses had an invalid value") + } + } + +} diff --git a/api/web_conn.go b/api/web_conn.go new file mode 100644 index 000000000..751f6f407 --- /dev/null +++ b/api/web_conn.go @@ -0,0 +1,132 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "github.com/gorilla/websocket" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "time" +) + +const ( + WRITE_WAIT = 10 * time.Second + PONG_WAIT = 60 * time.Second + PING_PERIOD = (PONG_WAIT * 9) / 10 + MAX_SIZE = 512 + REDIS_WAIT = 60 * time.Second +) + +type WebConn struct { + WebSocket *websocket.Conn + Send chan *model.Message + TeamId string + UserId string + ChannelAccessCache map[string]bool +} + +func NewWebConn(ws *websocket.Conn, teamId string, userId string, sessionId string) *WebConn { + go func() { + achan := Srv.Store.User().UpdateUserAndSessionActivity(userId, sessionId, model.GetMillis()) + pchan := Srv.Store.User().UpdateLastPingAt(userId, model.GetMillis()) + + if result := <-achan; result.Err != nil { + l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", userId, sessionId, result.Err) + } + + if result := <-pchan; result.Err != nil { + l4g.Error("Failed to updated LastPingAt for user_id=%v, err=%v", userId, result.Err) + } + }() + + return &WebConn{Send: make(chan *model.Message, 64), WebSocket: ws, UserId: userId, TeamId: teamId, ChannelAccessCache: make(map[string]bool)} +} + +func (c *WebConn) readPump() { + defer func() { + hub.Unregister(c) + c.WebSocket.Close() + }() + c.WebSocket.SetReadLimit(MAX_SIZE) + c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) + c.WebSocket.SetPongHandler(func(string) error { + c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) + + go func() { + if result := <-Srv.Store.User().UpdateLastPingAt(c.UserId, model.GetMillis()); result.Err != nil { + l4g.Error("Failed to updated LastPingAt for user_id=%v, err=%v", c.UserId, result.Err) + } + }() + + return nil + }) + + for { + var msg model.Message + if err := c.WebSocket.ReadJSON(&msg); err != nil { + return + } else { + msg.TeamId = c.TeamId + msg.UserId = c.UserId + store.PublishAndForget(&msg) + } + } +} + +func (c *WebConn) writePump() { + ticker := time.NewTicker(PING_PERIOD) + + defer func() { + ticker.Stop() + c.WebSocket.Close() + }() + + for { + select { + case msg, ok := <-c.Send: + if !ok { + c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) + c.WebSocket.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + if len(msg.ChannelId) > 0 { + allowed, ok := c.ChannelAccessCache[msg.ChannelId] + if !ok { + allowed = hasPermissionsToChannel(Srv.Store.Channel().CheckPermissionsTo(c.TeamId, msg.ChannelId, c.UserId)) + c.ChannelAccessCache[msg.ChannelId] = allowed + } + + if allowed { + c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) + if err := c.WebSocket.WriteJSON(msg); err != nil { + return + } + } + } else { + c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) + if err := c.WebSocket.WriteJSON(msg); err != nil { + return + } + } + + case <-ticker.C: + c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) + if err := c.WebSocket.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +func hasPermissionsToChannel(sc store.StoreChannel) bool { + if cresult := <-sc; cresult.Err != nil { + return false + } else if cresult.Data.(int64) != 1 { + return false + } + + return true +} diff --git a/api/web_hub.go b/api/web_hub.go new file mode 100644 index 000000000..bf5fbb321 --- /dev/null +++ b/api/web_hub.go @@ -0,0 +1,71 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" +) + +type Hub struct { + teamHubs map[string]*TeamHub + register chan *WebConn + unregister chan *WebConn + stop chan string +} + +var hub = &Hub{ + register: make(chan *WebConn), + unregister: make(chan *WebConn), + teamHubs: make(map[string]*TeamHub), + stop: make(chan string), +} + +func (h *Hub) Register(webConn *WebConn) { + h.register <- webConn +} + +func (h *Hub) Unregister(webConn *WebConn) { + h.unregister <- webConn +} + +func (h *Hub) Stop(teamId string) { + h.stop <- teamId +} + +func (h *Hub) Start() { + go func() { + for { + select { + + case c := <-h.register: + nh := h.teamHubs[c.TeamId] + + if nh == nil { + nh = NewTeamHub(c.TeamId) + h.teamHubs[c.TeamId] = nh + nh.Start() + } + + nh.Register(c) + + case c := <-h.unregister: + if nh, ok := h.teamHubs[c.TeamId]; ok { + nh.Unregister(c) + } + + case s := <-h.stop: + if len(s) == 0 { + l4g.Debug("stopping all connections") + for _, v := range h.teamHubs { + v.Stop() + } + return + } else if nh, ok := h.teamHubs[s]; ok { + delete(h.teamHubs, s) + nh.Stop() + } + } + } + }() +} diff --git a/api/web_socket.go b/api/web_socket.go new file mode 100644 index 000000000..75936a8d5 --- /dev/null +++ b/api/web_socket.go @@ -0,0 +1,40 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/mattermost/platform/model" + "net/http" +) + +func InitWebSocket(r *mux.Router) { + l4g.Debug("Initializing web socket api routes") + r.Handle("/websocket", ApiUserRequired(connect)).Methods("GET") + hub.Start() +} + +func connect(c *Context, w http.ResponseWriter, r *http.Request) { + upgrader := websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + l4g.Error("websocket connect err: %v", err) + c.Err = model.NewAppError("connect", "Failed to upgrade websocket connection", "") + return + } + + wc := NewWebConn(ws, c.Session.TeamId, c.Session.UserId, c.Session.Id) + hub.Register(wc) + go wc.writePump() + wc.readPump() +} diff --git a/api/web_socket_test.go b/api/web_socket_test.go new file mode 100644 index 000000000..c7b612cde --- /dev/null +++ b/api/web_socket_test.go @@ -0,0 +1,129 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/gorilla/websocket" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/http" + "testing" + "time" +) + +func TestSocket(t *testing.T) { + Setup() + + url := "ws://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1/websocket" + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user1.Id) + Client.LoginByEmail(team.Domain, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "Test Web Scoket 1", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + channel2 := &model.Channel{DisplayName: "Test Web Scoket 2", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + header1 := http.Header{} + header1.Set(model.HEADER_AUTH, "BEARER "+Client.AuthToken) + + c1, _, err := websocket.DefaultDialer.Dial(url, header1) + if err != nil { + t.Fatal(err) + } + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + header2 := http.Header{} + header2.Set(model.HEADER_AUTH, "BEARER "+Client.AuthToken) + + c2, _, err := websocket.DefaultDialer.Dial(url, header2) + if err != nil { + t.Fatal(err) + } + + time.Sleep(300 * time.Millisecond) + Client.Must(Client.JoinChannel(channel1.Id)) + + // Read the join channel message that gets generated + var rmsg model.Message + if err := c2.ReadJSON(&rmsg); err != nil { + t.Fatal(err) + } + + // Test sending message without a channelId + m := model.NewMessage("", "", "", model.ACTION_TYPING) + m.Add("RootId", model.NewId()) + m.Add("ParentId", model.NewId()) + + c1.WriteJSON(m) + + if err := c2.ReadJSON(&rmsg); err != nil { + t.Fatal(err) + } + + if team.Id != rmsg.TeamId { + t.Fatal("Ids do not match") + } + + if m.Props["RootId"] != rmsg.Props["RootId"] { + t.Fatal("Ids do not match") + } + + // Test sending messsage to Channel you have access to + m = model.NewMessage("", channel1.Id, "", model.ACTION_TYPING) + m.Add("RootId", model.NewId()) + m.Add("ParentId", model.NewId()) + + c1.WriteJSON(m) + + if err := c2.ReadJSON(&rmsg); err != nil { + t.Fatal(err) + } + + if team.Id != rmsg.TeamId { + t.Fatal("Ids do not match") + } + + if m.Props["RootId"] != rmsg.Props["RootId"] { + t.Fatal("Ids do not match") + } + + // Test sending message to Channel you *do not* have access too + m = model.NewMessage("", channel2.Id, "", model.ACTION_TYPING) + m.Add("RootId", model.NewId()) + m.Add("ParentId", model.NewId()) + + c1.WriteJSON(m) + + go func() { + if err := c2.ReadJSON(&rmsg); err != nil { + t.Fatal(err) + } + + t.Fatal(err) + }() + + time.Sleep(2 * time.Second) + + hub.Stop(team.Id) + +} + +func TestZZWebScoketTearDown(t *testing.T) { + // *IMPORTANT* - Kind of hacky + // This should be the last function in any test file + // that calls Setup() + // Should be in the last file too sorted by name + time.Sleep(2 * time.Second) + TearDown() +} diff --git a/api/web_team_hub.go b/api/web_team_hub.go new file mode 100644 index 000000000..7c7981e76 --- /dev/null +++ b/api/web_team_hub.go @@ -0,0 +1,119 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "strings" +) + +type TeamHub struct { + connections map[*WebConn]bool + broadcast chan *model.Message + register chan *WebConn + unregister chan *WebConn + stop chan bool + teamId string +} + +func NewTeamHub(teamId string) *TeamHub { + return &TeamHub{ + broadcast: make(chan *model.Message), + register: make(chan *WebConn), + unregister: make(chan *WebConn), + connections: make(map[*WebConn]bool), + stop: make(chan bool), + teamId: teamId, + } +} + +func (h *TeamHub) Register(webConn *WebConn) { + h.register <- webConn +} + +func (h *TeamHub) Unregister(webConn *WebConn) { + h.unregister <- webConn +} + +func (h *TeamHub) Stop() { + h.stop <- true +} + +func (h *TeamHub) Start() { + + pubsub := store.RedisClient().PubSub() + + go func() { + defer func() { + l4g.Debug("redis reader finished for teamId=%v", h.teamId) + hub.Stop(h.teamId) + }() + + l4g.Debug("redis reader starting for teamId=%v", h.teamId) + + err := pubsub.Subscribe(h.teamId) + if err != nil { + l4g.Error("Error while subscribing to redis %v %v", h.teamId, err) + return + } + + for { + if payload, err := pubsub.ReceiveTimeout(REDIS_WAIT); err != nil { + if strings.Contains(err.Error(), "i/o timeout") { + if len(h.connections) == 0 { + l4g.Debug("No active connections so sending stop %v", h.teamId) + return + } + } else { + return + } + } else { + msg := store.GetMessageFromPayload(payload) + if msg != nil { + h.broadcast <- msg + } + } + } + + }() + + go func() { + for { + select { + case webCon := <-h.register: + h.connections[webCon] = true + case webCon := <-h.unregister: + if _, ok := h.connections[webCon]; ok { + delete(h.connections, webCon) + close(webCon.Send) + } + case msg := <-h.broadcast: + for webCon := range h.connections { + if !(webCon.UserId == msg.UserId && msg.Action == model.ACTION_TYPING) { + select { + case webCon.Send <- msg: + default: + close(webCon.Send) + delete(h.connections, webCon) + } + } + } + case s := <-h.stop: + if s { + + l4g.Debug("team hub stopping for teamId=%v", h.teamId) + + for webCon := range h.connections { + webCon.WebSocket.Close() + } + + pubsub.Close() + return + } + } + } + }() +} |