diff options
Diffstat (limited to 'api4')
39 files changed, 1449 insertions, 37 deletions
diff --git a/api4/api.go b/api4/api.go index dffed60e4..a91fb80b5 100644 --- a/api4/api.go +++ b/api4/api.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -47,6 +47,7 @@ type Routes struct { Posts *mux.Router // 'api/v4/posts' Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}' PostsForChannel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts' + PostsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts' Files *mux.Router // 'api/v4/files' File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}' @@ -126,6 +127,7 @@ func InitApi(full bool) { BaseRoutes.Posts = BaseRoutes.ApiRoot.PathPrefix("/posts").Subrouter() BaseRoutes.Post = BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter() BaseRoutes.PostsForChannel = BaseRoutes.Channel.PathPrefix("/posts").Subrouter() + BaseRoutes.PostsForUser = BaseRoutes.User.PathPrefix("/posts").Subrouter() BaseRoutes.Files = BaseRoutes.ApiRoot.PathPrefix("/files").Subrouter() BaseRoutes.File = BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter() @@ -174,6 +176,9 @@ func InitApi(full bool) { InitCommand() InitStatus() InitWebSocket() + InitEmoji() + InitReaction() + InitWebrtc() app.Srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404)) diff --git a/api4/apitestlib.go b/api4/apitestlib.go index bd36f49cc..81a9ca311 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -564,6 +564,21 @@ func CheckInternalErrorStatus(t *testing.T, resp *model.Response) { } } +func CheckPayLoadTooLargeStatus(t *testing.T, resp *model.Response) { + if resp.Error == nil { + debug.PrintStack() + t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusRequestEntityTooLarge)) + return + } + + if resp.StatusCode != http.StatusRequestEntityTooLarge { + debug.PrintStack() + t.Log("actual: " + strconv.Itoa(resp.StatusCode)) + t.Log("expected: " + strconv.Itoa(http.StatusRequestEntityTooLarge)) + t.Fatal("wrong status code") + } +} + func readTestFile(name string) ([]byte, error) { path := utils.FindDir("tests") file, err := os.Open(path + "/" + name) diff --git a/api4/brand.go b/api4/brand.go index f9a301acf..ac69f623b 100644 --- a/api4/brand.go +++ b/api4/brand.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/brand_test.go b/api4/brand_test.go index fd5e472a8..98a539574 100644 --- a/api4/brand_test.go +++ b/api4/brand_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/channel.go b/api4/channel.go index 78d4cc733..acef92415 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/channel_test.go b/api4/channel_test.go index c5deda83e..50755fbe0 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -715,6 +715,30 @@ func TestDeleteChannel(t *testing.T) { t.Fatal("should have failed") } + // check system admin can delete a channel without any appropriate team or channel membership. + sdTeam := th.CreateTeamWithClient(Client) + sdPublicChannel := &model.Channel{ + DisplayName: "dn_" + model.NewId(), + Name: GenerateTestChannelName(), + Type: model.CHANNEL_OPEN, + TeamId: sdTeam.Id, + } + sdPublicChannel, resp = Client.CreateChannel(sdPublicChannel) + CheckNoError(t, resp) + _, resp = th.SystemAdminClient.DeleteChannel(sdPublicChannel.Id) + CheckNoError(t, resp) + + sdPrivateChannel := &model.Channel{ + DisplayName: "dn_" + model.NewId(), + Name: GenerateTestChannelName(), + Type: model.CHANNEL_PRIVATE, + TeamId: sdTeam.Id, + } + sdPrivateChannel, resp = Client.CreateChannel(sdPrivateChannel) + CheckNoError(t, resp) + _, resp = th.SystemAdminClient.DeleteChannel(sdPrivateChannel.Id) + CheckNoError(t, resp) + th.LoginBasic() publicChannel5 := th.CreatePublicChannel() Client.Logout() diff --git a/api4/cluster.go b/api4/cluster.go index dbf198590..54ff15cf1 100644 --- a/api4/cluster.go +++ b/api4/cluster.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/cluster_test.go b/api4/cluster_test.go index 6d44ca209..573e309a4 100644 --- a/api4/cluster_test.go +++ b/api4/cluster_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/command.go b/api4/command.go index d6102bd70..f44363522 100644 --- a/api4/command.go +++ b/api4/command.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -19,7 +19,11 @@ func InitCommand() { BaseRoutes.Commands.Handle("", ApiSessionRequired(createCommand)).Methods("POST") BaseRoutes.Commands.Handle("", ApiSessionRequired(listCommands)).Methods("GET") + BaseRoutes.Command.Handle("", ApiSessionRequired(updateCommand)).Methods("PUT") + BaseRoutes.Command.Handle("", ApiSessionRequired(deleteCommand)).Methods("DELETE") + BaseRoutes.Team.Handle("/commands/autocomplete", ApiSessionRequired(listAutocompleteCommands)).Methods("GET") + BaseRoutes.Command.Handle("/regen_token", ApiSessionRequired(regenCommandToken)).Methods("PUT") } func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { @@ -49,6 +53,91 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(rcmd.ToJson())) } +func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireCommandId() + if c.Err != nil { + return + } + + cmd := model.CommandFromJson(r.Body) + if cmd == nil || cmd.Id != c.Params.CommandId { + c.SetInvalidParam("command") + return + } + + c.LogAudit("attempt") + + oldCmd, err := app.GetCommand(c.Params.CommandId) + if err != nil { + c.Err = err + return + } + + if cmd.TeamId != oldCmd.TeamId { + c.Err = model.NewAppError("updateCommand", "api.command.team_mismatch.app_error", nil, "user_id="+c.Session.UserId, http.StatusBadRequest) + return + } + + if !app.SessionHasPermissionToTeam(c.Session, oldCmd.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) { + c.LogAudit("fail - inappropriate permissions") + c.SetPermissionError(model.PERMISSION_MANAGE_SLASH_COMMANDS) + return + } + + if c.Session.UserId != oldCmd.CreatorId && !app.SessionHasPermissionToTeam(c.Session, oldCmd.TeamId, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) { + c.LogAudit("fail - inappropriate permissions") + c.SetPermissionError(model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) + return + } + + rcmd, err := app.UpdateCommand(oldCmd, cmd) + if err != nil { + c.Err = err + return + } + + c.LogAudit("success") + + w.Write([]byte(rcmd.ToJson())) +} + +func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireCommandId() + if c.Err != nil { + return + } + + c.LogAudit("attempt") + + cmd, err := app.GetCommand(c.Params.CommandId) + if err != nil { + c.Err = err + return + } + + if !app.SessionHasPermissionToTeam(c.Session, cmd.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) { + c.LogAudit("fail - inappropriate permissions") + c.SetPermissionError(model.PERMISSION_MANAGE_SLASH_COMMANDS) + return + } + + if c.Session.UserId != cmd.CreatorId && !app.SessionHasPermissionToTeam(c.Session, cmd.TeamId, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) { + c.LogAudit("fail - inappropriate permissions") + c.SetPermissionError(model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) + return + } + + err = app.DeleteCommand(cmd.Id) + if err != nil { + c.Err = err + return + } + + c.LogAudit("success") + + ReturnStatusOK(w) +} + func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { customOnly, failConv := strconv.ParseBool(r.URL.Query().Get("custom_only")) if failConv != nil { @@ -113,3 +202,40 @@ func listAutocompleteCommands(c *Context, w http.ResponseWriter, r *http.Request w.Write([]byte(model.CommandListToJson(commands))) } + +func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireCommandId() + if c.Err != nil { + return + } + + c.LogAudit("attempt") + cmd, err := app.GetCommand(c.Params.CommandId) + if err != nil { + c.Err = err + return + } + + if !app.SessionHasPermissionToTeam(c.Session, cmd.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) { + c.LogAudit("fail - inappropriate permissions") + c.SetPermissionError(model.PERMISSION_MANAGE_SLASH_COMMANDS) + return + } + + if c.Session.UserId != cmd.CreatorId && !app.SessionHasPermissionToTeam(c.Session, cmd.TeamId, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) { + c.LogAudit("fail - inappropriate permissions") + c.SetPermissionError(model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) + return + } + + rcmd, err := app.RegenCommandToken(cmd) + if err != nil { + c.Err = err + return + } + + resp := make(map[string]string) + resp["token"] = rcmd.Token + + w.Write([]byte(model.MapToJson(resp))) +} diff --git a/api4/command_test.go b/api4/command_test.go index 75842886c..0aaca3c0f 100644 --- a/api4/command_test.go +++ b/api4/command_test.go @@ -1,12 +1,12 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 import ( "testing" - // "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -60,6 +60,155 @@ func TestCreateCommand(t *testing.T) { CheckErrorMessage(t, resp, "api.command.disabled.app_error") } +func TestUpdateCommand(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.SystemAdminClient + user := th.SystemAdminUser + team := th.BasicTeam + + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands + defer func() { + utils.Cfg.ServiceSettings.EnableCommands = &enableCommands + }() + *utils.Cfg.ServiceSettings.EnableCommands = true + + cmd1 := &model.Command{ + CreatorId: user.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.COMMAND_METHOD_POST, + Trigger: "trigger1", + } + + cmd1, _ = app.CreateCommand(cmd1) + + cmd2 := &model.Command{ + CreatorId: GenerateTestId(), + TeamId: team.Id, + URL: "http://nowhere.com/change", + Method: model.COMMAND_METHOD_GET, + Trigger: "trigger2", + Id: cmd1.Id, + Token: "tokenchange", + } + + rcmd, resp := Client.UpdateCommand(cmd2) + CheckNoError(t, resp) + + if rcmd.Trigger != cmd2.Trigger { + t.Fatal("Trigger should have updated") + } + + if rcmd.Method != cmd2.Method { + t.Fatal("Method should have updated") + } + + if rcmd.URL != cmd2.URL { + t.Fatal("URL should have updated") + } + + if rcmd.CreatorId != cmd1.CreatorId { + t.Fatal("CreatorId should have not updated") + } + + if rcmd.Token != cmd1.Token { + t.Fatal("Token should have not updated") + } + + cmd2.Id = GenerateTestId() + + rcmd, resp = Client.UpdateCommand(cmd2) + CheckNotFoundStatus(t, resp) + + if rcmd != nil { + t.Fatal("should be empty") + } + + cmd2.Id = "junk" + + _, resp = Client.UpdateCommand(cmd2) + CheckBadRequestStatus(t, resp) + + cmd2.Id = cmd1.Id + cmd2.TeamId = GenerateTestId() + + _, resp = Client.UpdateCommand(cmd2) + CheckBadRequestStatus(t, resp) + + cmd2.TeamId = team.Id + + _, resp = th.Client.UpdateCommand(cmd2) + CheckForbiddenStatus(t, resp) + + Client.Logout() + _, resp = Client.UpdateCommand(cmd2) + CheckUnauthorizedStatus(t, resp) +} + +func TestDeleteCommand(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.SystemAdminClient + user := th.SystemAdminUser + team := th.BasicTeam + + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands + defer func() { + utils.Cfg.ServiceSettings.EnableCommands = &enableCommands + }() + *utils.Cfg.ServiceSettings.EnableCommands = true + + cmd1 := &model.Command{ + CreatorId: user.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.COMMAND_METHOD_POST, + Trigger: "trigger1", + } + + rcmd1, _ := app.CreateCommand(cmd1) + + ok, resp := Client.DeleteCommand(rcmd1.Id) + CheckNoError(t, resp) + + if !ok { + t.Fatal("should have returned true") + } + + rcmd1, _ = app.GetCommand(rcmd1.Id) + if rcmd1 != nil { + t.Fatal("should be nil") + } + + ok, resp = Client.DeleteCommand("junk") + CheckBadRequestStatus(t, resp) + + if ok { + t.Fatal("should have returned false") + } + + _, resp = Client.DeleteCommand(GenerateTestId()) + CheckNotFoundStatus(t, resp) + + cmd2 := &model.Command{ + CreatorId: user.Id, + TeamId: team.Id, + URL: "http://nowhere.com", + Method: model.COMMAND_METHOD_POST, + Trigger: "trigger2", + } + + rcmd2, _ := app.CreateCommand(cmd2) + + _, resp = th.Client.DeleteCommand(rcmd2.Id) + CheckForbiddenStatus(t, resp) + + Client.Logout() + _, resp = Client.DeleteCommand(rcmd2.Id) + CheckUnauthorizedStatus(t, resp) +} + func TestListCommands(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() @@ -196,3 +345,39 @@ func TestListAutocompleteCommands(t *testing.T) { } }) } + +func TestRegenToken(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands + defer func() { + utils.Cfg.ServiceSettings.EnableCommands = &enableCommands + }() + *utils.Cfg.ServiceSettings.EnableCommands = true + + newCmd := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.COMMAND_METHOD_POST, + Trigger: "trigger"} + + createdCmd, resp := th.SystemAdminClient.CreateCommand(newCmd) + CheckNoError(t, resp) + CheckCreatedStatus(t, resp) + + token, resp := th.SystemAdminClient.RegenCommandToken(createdCmd.Id) + CheckNoError(t, resp) + if token == createdCmd.Token { + t.Fatal("should update the token") + } + + token, resp = Client.RegenCommandToken(createdCmd.Id) + CheckForbiddenStatus(t, resp) + if token != "" { + t.Fatal("should not return the token") + } + +} diff --git a/api4/compliance.go b/api4/compliance.go index 37196c853..cabac6e21 100644 --- a/api4/compliance.go +++ b/api4/compliance.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/context.go b/api4/context.go index 484a6432f..847a8d55f 100644 --- a/api4/context.go +++ b/api4/context.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -242,8 +242,7 @@ func (c *Context) IsSystemAdmin() bool { func (c *Context) SessionRequired() { if len(c.Session.UserId) == 0 { - c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "UserRequired") - c.Err.StatusCode = http.StatusUnauthorized + c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized) return } } @@ -406,6 +405,17 @@ func (c *Context) RequireReportId() *Context { return c } +func (c *Context) RequireEmojiId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.EmojiId) != 26 { + c.SetInvalidUrlParam("emoji_id") + } + return c +} + func (c *Context) RequireTeamName() *Context { if c.Err != nil { return c @@ -477,3 +487,14 @@ func (c *Context) RequireHookId() *Context { return c } + +func (c *Context) RequireCommandId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.CommandId) != 26 { + c.SetInvalidUrlParam("command_id") + } + return c +} diff --git a/api4/emoji.go b/api4/emoji.go new file mode 100644 index 000000000..ff4919860 --- /dev/null +++ b/api4/emoji.go @@ -0,0 +1,131 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "net/http" + "strings" + + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func InitEmoji() { + l4g.Debug(utils.T("api.emoji.init.debug")) + + BaseRoutes.Emojis.Handle("", ApiSessionRequired(createEmoji)).Methods("POST") + BaseRoutes.Emojis.Handle("", ApiSessionRequired(getEmojiList)).Methods("GET") + BaseRoutes.Emoji.Handle("", ApiSessionRequired(deleteEmoji)).Methods("DELETE") + BaseRoutes.Emoji.Handle("", ApiSessionRequired(getEmoji)).Methods("GET") +} + +func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.ServiceSettings.EnableCustomEmoji { + c.Err = model.NewAppError("createEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + + if emojiInterface := einterfaces.GetEmojiInterface(); emojiInterface != nil && + !emojiInterface.CanUserCreateEmoji(c.Session.Roles, c.Session.TeamMembers) { + c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "user_id="+c.Session.UserId, http.StatusUnauthorized) + return + } + + if len(utils.Cfg.FileSettings.DriverName) == 0 { + c.Err = model.NewAppError("createEmoji", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented) + return + } + + if r.ContentLength > app.MaxEmojiFileSize { + c.Err = model.NewAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge) + return + } + + if err := r.ParseMultipartForm(app.MaxEmojiFileSize); err != nil { + c.Err = model.NewAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error(), http.StatusBadRequest) + return + } + + m := r.MultipartForm + props := m.Value + + emoji := model.EmojiFromJson(strings.NewReader(props["emoji"][0])) + if emoji == nil { + c.SetInvalidParam("createEmoji") + return + } + + newEmoji, err := app.CreateEmoji(c.Session.UserId, emoji, m) + if err != nil { + c.Err = err + return + } else { + w.Write([]byte(newEmoji.ToJson())) + } +} + +func getEmojiList(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.ServiceSettings.EnableCustomEmoji { + c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + + listEmoji, err := app.GetEmojiList() + if err != nil { + c.Err = err + return + } else { + w.Write([]byte(model.EmojiListToJson(listEmoji))) + } +} + +func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireEmojiId() + if c.Err != nil { + return + } + + emoji, err := app.GetEmoji(c.Params.EmojiId) + if err != nil { + c.Err = err + return + } + + if c.Session.UserId != emoji.CreatorId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.Err = model.NewAppError("deleteImage", "api.emoji.delete.permissions.app_error", nil, "user_id="+c.Session.UserId, http.StatusUnauthorized) + return + } + + err = app.DeleteEmoji(emoji) + if err != nil { + c.Err = err + return + } else { + ReturnStatusOK(w) + } +} + +func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireEmojiId() + if c.Err != nil { + return + } + + if !*utils.Cfg.ServiceSettings.EnableCustomEmoji { + c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + + emoji, err := app.GetEmoji(c.Params.EmojiId) + if err != nil { + c.Err = err + return + } else { + w.Write([]byte(emoji.ToJson())) + } +} diff --git a/api4/emoji_test.go b/api4/emoji_test.go new file mode 100644 index 000000000..23188a3d2 --- /dev/null +++ b/api4/emoji_test.go @@ -0,0 +1,300 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "testing" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func TestCreateEmoji(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji + defer func() { + *utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji + }() + *utils.Cfg.ServiceSettings.EnableCustomEmoji = false + + emoji := &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + // try to create an emoji when they're disabled + _, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNotImplementedStatus(t, resp) + + *utils.Cfg.ServiceSettings.EnableCustomEmoji = true + // try to create a valid gif emoji when they're enabled + newEmoji, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + if newEmoji.Name != emoji.Name { + t.Fatal("create with wrong name") + } + + // try to create an emoji with a duplicate name + emoji2 := &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: newEmoji.Name, + } + _, resp = Client.CreateEmoji(emoji2, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckBadRequestStatus(t, resp) + CheckErrorMessage(t, resp, "api.emoji.create.duplicate.app_error") + + // try to create a valid animated gif emoji + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestAnimatedGif(t, 10, 10, 10), "image.gif") + CheckNoError(t, resp) + if newEmoji.Name != emoji.Name { + t.Fatal("create with wrong name") + } + + // try to create a valid jpeg emoji + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestJpeg(t, 10, 10), "image.gif") + CheckNoError(t, resp) + if newEmoji.Name != emoji.Name { + t.Fatal("create with wrong name") + } + + // try to create a valid png emoji + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestPng(t, 10, 10), "image.gif") + CheckNoError(t, resp) + if newEmoji.Name != emoji.Name { + t.Fatal("create with wrong name") + } + + // try to create an emoji that's too wide + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 1000, 10), "image.gif") + CheckNoError(t, resp) + if newEmoji.Name != emoji.Name { + t.Fatal("create with wrong name") + } + + // try to create an emoji that's too tall + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 1000), "image.gif") + CheckNoError(t, resp) + if newEmoji.Name != emoji.Name { + t.Fatal("create with wrong name") + } + + // try to create an emoji that's too large + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + _, resp = Client.CreateEmoji(emoji, utils.CreateTestAnimatedGif(t, 100, 100, 10000), "image.gif") + if resp.Error == nil { + t.Fatal("should fail - emoji is too big") + } + + // try to create an emoji with data that isn't an image + emoji = &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + _, resp = Client.CreateEmoji(emoji, make([]byte, 100, 100), "image.gif") + CheckBadRequestStatus(t, resp) + CheckErrorMessage(t, resp, "api.emoji.upload.image.app_error") + + // try to create an emoji as another user + emoji = &model.Emoji{ + CreatorId: th.BasicUser2.Id, + Name: model.NewId(), + } + + _, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckForbiddenStatus(t, resp) +} + +func TestGetEmojiList(t *testing.T) { + th := Setup().InitBasic() + defer TearDown() + Client := th.Client + + EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji + defer func() { + *utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji + }() + *utils.Cfg.ServiceSettings.EnableCustomEmoji = true + + emojis := []*model.Emoji{ + { + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + }, + { + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + }, + { + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + }, + } + + for idx, emoji := range emojis { + emoji, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + emojis[idx] = emoji + } + + listEmoji, resp := Client.GetEmojiList() + CheckNoError(t, resp) + for _, emoji := range emojis { + found := false + for _, savedEmoji := range listEmoji { + if emoji.Id == savedEmoji.Id { + found = true + break + } + } + if !found { + t.Fatalf("failed to get emoji with id %v", emoji.Id) + } + } + + _, resp = Client.DeleteEmoji(emojis[0].Id) + CheckNoError(t, resp) + listEmoji, resp = Client.GetEmojiList() + CheckNoError(t, resp) + found := false + for _, savedEmoji := range listEmoji { + if savedEmoji.Id == emojis[0].Id { + found = true + break + } + if found { + t.Fatalf("should not get a deleted emoji %v", emojis[0].Id) + } + } + +} + +func TestDeleteEmoji(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji + defer func() { + *utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji + }() + *utils.Cfg.ServiceSettings.EnableCustomEmoji = true + + emoji := &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + ok, resp := Client.DeleteEmoji(newEmoji.Id) + CheckNoError(t, resp) + if ok != true { + t.Fatal("should return true") + } else { + _, err := Client.GetEmoji(newEmoji.Id) + if err == nil { + t.Fatal("should not return the emoji it was deleted") + } + } + + //Admin can delete other users emoji + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + ok, resp = th.SystemAdminClient.DeleteEmoji(newEmoji.Id) + CheckNoError(t, resp) + if ok != true { + t.Fatal("should return true") + } else { + _, err := th.SystemAdminClient.GetEmoji(newEmoji.Id) + if err == nil { + t.Fatal("should not return the emoji it was deleted") + } + } + + // Try to delete just deleted emoji + _, resp = Client.DeleteEmoji(newEmoji.Id) + CheckInternalErrorStatus(t, resp) + + //Try to delete non-existing emoji + _, resp = Client.DeleteEmoji(model.NewId()) + CheckInternalErrorStatus(t, resp) + + //Try to delete without Id + _, resp = Client.DeleteEmoji("") + CheckNotFoundStatus(t, resp) + + //Try to delete other user's custom emoji + newEmoji, resp = Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + Client.Logout() + th.LoginBasic2() + ok, resp = Client.DeleteEmoji(newEmoji.Id) + CheckUnauthorizedStatus(t, resp) +} + +func TestGetEmoji(t *testing.T) { + th := Setup().InitBasic() + defer TearDown() + Client := th.Client + + EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji + defer func() { + *utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji + }() + *utils.Cfg.ServiceSettings.EnableCustomEmoji = true + + emoji := &model.Emoji{ + CreatorId: th.BasicUser.Id, + Name: model.NewId(), + } + + newEmoji, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif") + CheckNoError(t, resp) + + emoji, resp = Client.GetEmoji(newEmoji.Id) + CheckNoError(t, resp) + if emoji.Id != newEmoji.Id { + t.Fatal("wrong emoji was returned") + } + + _, resp = Client.GetEmoji(model.NewId()) + CheckInternalErrorStatus(t, resp) + +} diff --git a/api4/file.go b/api4/file.go index e4bdbcc3c..de1ce454d 100644 --- a/api4/file.go +++ b/api4/file.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/file_test.go b/api4/file_test.go index 5e0824d45..9124e893b 100644 --- a/api4/file_test.go +++ b/api4/file_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/ldap.go b/api4/ldap.go index e138fdc97..eac5778b6 100644 --- a/api4/ldap.go +++ b/api4/ldap.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/ldap_test.go b/api4/ldap_test.go index d8eaedc50..6d79a92ad 100644 --- a/api4/ldap_test.go +++ b/api4/ldap_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/params.go b/api4/params.go index 8bb072742..fa5d96d88 100644 --- a/api4/params.go +++ b/api4/params.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/post.go b/api4/post.go index 5cbfeae92..f8e4cc54b 100644 --- a/api4/post.go +++ b/api4/post.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -22,6 +22,7 @@ func InitPost() { BaseRoutes.Post.Handle("/thread", ApiSessionRequired(getPostThread)).Methods("GET") BaseRoutes.Post.Handle("/files/info", ApiSessionRequired(getFileInfosForPost)).Methods("GET") BaseRoutes.PostsForChannel.Handle("", ApiSessionRequired(getPostsForChannel)).Methods("GET") + BaseRoutes.PostsForUser.Handle("/flagged", ApiSessionRequired(getFlaggedPostsForUser)).Methods("GET") BaseRoutes.Team.Handle("/posts/search", ApiSessionRequired(searchPosts)).Methods("POST") BaseRoutes.Post.Handle("", ApiSessionRequired(updatePost)).Methods("PUT") @@ -127,6 +128,39 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(list.ToJson())) } +func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if !app.SessionHasPermissionToUser(c.Session, c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + channelId := r.URL.Query().Get("in_channel") + teamId := r.URL.Query().Get("in_team") + + var posts *model.PostList + var err *model.AppError + + if len(channelId) > 0 { + posts, err = app.GetFlaggedPostsForChannel(c.Params.UserId, channelId, c.Params.Page, c.Params.PerPage) + } else if len(teamId) > 0 { + posts, err = app.GetFlaggedPostsForTeam(c.Params.UserId, teamId, c.Params.Page, c.Params.PerPage) + } else { + posts, err = app.GetFlaggedPosts(c.Params.UserId, c.Params.Page, c.Params.PerPage) + } + + if err != nil { + c.Err = err + return + } + + w.Write([]byte(posts.ToJson())) +} + func getPost(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePostId() if c.Err != nil { diff --git a/api4/post_test.go b/api4/post_test.go index e5c72ae9e..c6e9dcb59 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -459,6 +459,187 @@ func TestGetPostsForChannel(t *testing.T) { CheckNoError(t, resp) } +func TestGetFlaggedPostsForUser(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + user := th.BasicUser + team1 := th.BasicTeam + channel1 := th.BasicChannel + post1 := th.CreatePost() + channel2 := th.CreatePublicChannel() + post2 := th.CreatePostWithClient(Client, channel2) + + preference := model.Preference{ + UserId: user.Id, + Category: model.PREFERENCE_CATEGORY_FLAGGED_POST, + Name: post1.Id, + Value: "true", + } + Client.UpdatePreferences(user.Id, &model.Preferences{preference}) + preference.Name = post2.Id + Client.UpdatePreferences(user.Id, &model.Preferences{preference}) + + opl := model.NewPostList() + opl.AddPost(post1) + opl.AddOrder(post1.Id) + + rpl, resp := Client.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 0, 10) + CheckNoError(t, resp) + + if len(rpl.Posts) != 1 { + t.Fatal("should have returned 1 post") + } + + if !reflect.DeepEqual(rpl.Posts, opl.Posts) { + t.Fatal("posts should have matched") + } + + rpl, resp = Client.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 0, 1) + CheckNoError(t, resp) + + if len(rpl.Posts) != 1 { + t.Fatal("should have returned 1 post") + } + + rpl, resp = Client.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 1, 1) + CheckNoError(t, resp) + + if len(rpl.Posts) != 0 { + t.Fatal("should be empty") + } + + rpl, resp = Client.GetFlaggedPostsForUserInChannel(user.Id, GenerateTestId(), 0, 10) + CheckNoError(t, resp) + + if len(rpl.Posts) != 0 { + t.Fatal("should be empty") + } + + rpl, resp = Client.GetFlaggedPostsForUserInChannel(user.Id, "junk", 0, 10) + CheckBadRequestStatus(t, resp) + + if rpl != nil { + t.Fatal("should be nil") + } + + opl.AddPost(post2) + opl.AddOrder(post2.Id) + + rpl, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 0, 10) + CheckNoError(t, resp) + + if len(rpl.Posts) != 2 { + t.Fatal("should have returned 2 posts") + } + + if !reflect.DeepEqual(rpl.Posts, opl.Posts) { + t.Fatal("posts should have matched") + } + + rpl, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 0, 1) + CheckNoError(t, resp) + + if len(rpl.Posts) != 1 { + t.Fatal("should have returned 1 post") + } + + rpl, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 1, 1) + CheckNoError(t, resp) + + if len(rpl.Posts) != 1 { + t.Fatal("should have returned 1 post") + } + + rpl, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 1000, 10) + CheckNoError(t, resp) + + if len(rpl.Posts) != 0 { + t.Fatal("should be empty") + } + + rpl, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, GenerateTestId(), 0, 10) + CheckNoError(t, resp) + + if len(rpl.Posts) != 0 { + t.Fatal("should be empty") + } + + rpl, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, "junk", 0, 10) + CheckBadRequestStatus(t, resp) + + if rpl != nil { + t.Fatal("should be nil") + } + + channel3 := th.CreatePrivateChannel() + post4 := th.CreatePostWithClient(Client, channel3) + + preference.Name = post4.Id + Client.UpdatePreferences(user.Id, &model.Preferences{preference}) + + opl.AddPost(post4) + opl.AddOrder(post4.Id) + + rpl, resp = Client.GetFlaggedPostsForUser(user.Id, 0, 10) + CheckNoError(t, resp) + + if len(rpl.Posts) != 3 { + t.Fatal("should have returned 3 posts") + } + + if !reflect.DeepEqual(rpl.Posts, opl.Posts) { + t.Fatal("posts should have matched") + } + + rpl, resp = Client.GetFlaggedPostsForUser(user.Id, 0, 2) + CheckNoError(t, resp) + + if len(rpl.Posts) != 2 { + t.Fatal("should have returned 2 posts") + } + + rpl, resp = Client.GetFlaggedPostsForUser(user.Id, 2, 2) + CheckNoError(t, resp) + + if len(rpl.Posts) != 1 { + t.Fatal("should have returned 1 post") + } + + rpl, resp = Client.GetFlaggedPostsForUser(user.Id, 1000, 10) + CheckNoError(t, resp) + + if len(rpl.Posts) != 0 { + t.Fatal("should be empty") + } + + _, resp = Client.GetFlaggedPostsForUser("junk", 0, 10) + CheckBadRequestStatus(t, resp) + + _, resp = Client.GetFlaggedPostsForUser(GenerateTestId(), 0, 10) + CheckForbiddenStatus(t, resp) + + Client.Logout() + + rpl, resp = Client.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 0, 10) + CheckUnauthorizedStatus(t, resp) + + rpl, resp = Client.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 0, 10) + CheckUnauthorizedStatus(t, resp) + + rpl, resp = Client.GetFlaggedPostsForUser(user.Id, 0, 10) + CheckUnauthorizedStatus(t, resp) + + rpl, resp = th.SystemAdminClient.GetFlaggedPostsForUserInChannel(user.Id, channel1.Id, 0, 10) + CheckNoError(t, resp) + + rpl, resp = th.SystemAdminClient.GetFlaggedPostsForUserInTeam(user.Id, team1.Id, 0, 10) + CheckNoError(t, resp) + + rpl, resp = th.SystemAdminClient.GetFlaggedPostsForUser(user.Id, 0, 10) + CheckNoError(t, resp) +} + func TestGetPostsAfterAndBefore(t *testing.T) { th := Setup().InitBasic() defer TearDown() diff --git a/api4/preference.go b/api4/preference.go index 9ba6b85d2..f9a5bfba6 100644 --- a/api4/preference.go +++ b/api4/preference.go @@ -1,4 +1,4 @@ -// // Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // // See License.txt for license information. package api4 diff --git a/api4/preference_test.go b/api4/preference_test.go index 02df99b9b..6efc96ff3 100644 --- a/api4/preference_test.go +++ b/api4/preference_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/reaction.go b/api4/reaction.go new file mode 100644 index 000000000..4deae4370 --- /dev/null +++ b/api4/reaction.go @@ -0,0 +1,39 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "net/http" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func InitReaction() { + l4g.Debug(utils.T("api.reaction.init.debug")) + + BaseRoutes.Post.Handle("/reactions", ApiSessionRequired(getReactions)).Methods("GET") +} + +func getReactions(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequirePostId() + if c.Err != nil { + return + } + + if !app.SessionHasPermissionToChannelByPost(c.Session, c.Params.PostId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + if reactions, err := app.GetReactionsForPost(c.Params.PostId); err != nil { + c.Err = err + return + } else { + w.Write([]byte(model.ReactionsToJson(reactions))) + return + } +} diff --git a/api4/reaction_test.go b/api4/reaction_test.go new file mode 100644 index 000000000..9e0847c2d --- /dev/null +++ b/api4/reaction_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "testing" + + "reflect" + + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" +) + +func TestGetReactions(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + userId := th.BasicUser.Id + user2Id := th.BasicUser2.Id + postId := th.BasicPost.Id + + userReactions := []*model.Reaction{ + { + UserId: userId, + PostId: postId, + EmojiName: "smile", + }, + { + UserId: userId, + PostId: postId, + EmojiName: "happy", + }, + { + UserId: userId, + PostId: postId, + EmojiName: "sad", + }, + { + UserId: user2Id, + PostId: postId, + EmojiName: "smile", + }, + { + UserId: user2Id, + PostId: postId, + EmojiName: "sad", + }, + } + + var reactions []*model.Reaction + + for _, userReaction := range userReactions { + if result := <-app.Srv.Store.Reaction().Save(userReaction); result.Err != nil { + t.Fatal(result.Err) + } else { + reactions = append(reactions, result.Data.(*model.Reaction)) + } + } + + rr, resp := Client.GetReactions(postId) + CheckNoError(t, resp) + + if len(rr) != 5 { + t.Fatal("reactions should returned correct length") + } + + if !reflect.DeepEqual(rr, reactions) { + t.Fatal("reactions should have matched") + } + + rr, resp = Client.GetReactions("junk") + CheckBadRequestStatus(t, resp) + + if len(rr) != 0 { + t.Fatal("reactions should return empty") + } + + _, resp = Client.GetReactions(GenerateTestId()) + CheckForbiddenStatus(t, resp) + + Client.Logout() + + _, resp = Client.GetReactions(postId) + CheckUnauthorizedStatus(t, resp) + + _, resp = th.SystemAdminClient.GetReactions(postId) + CheckNoError(t, resp) +} diff --git a/api4/saml.go b/api4/saml.go index e2c35f30d..b8167b505 100644 --- a/api4/saml.go +++ b/api4/saml.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/saml_test.go b/api4/saml_test.go index 7e4722a3b..0e16612d7 100644 --- a/api4/saml_test.go +++ b/api4/saml_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/status.go b/api4/status.go index 46d4f963b..3a2c5c762 100644 --- a/api4/status.go +++ b/api4/status.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/system.go b/api4/system.go index 098f7e4b7..dfc702c8c 100644 --- a/api4/system.go +++ b/api4/system.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/team.go b/api4/team.go index 22ab15bfd..b8ba47054 100644 --- a/api4/team.go +++ b/api4/team.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/team_test.go b/api4/team_test.go index b58a4dc72..7fd240480 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/user.go b/api4/user.go index b30d066ab..70182c1ab 100644 --- a/api4/user.go +++ b/api4/user.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -32,6 +32,7 @@ func InitUser() { BaseRoutes.User.Handle("/patch", ApiSessionRequired(patchUser)).Methods("PUT") BaseRoutes.User.Handle("", ApiSessionRequired(deleteUser)).Methods("DELETE") BaseRoutes.User.Handle("/roles", ApiSessionRequired(updateUserRoles)).Methods("PUT") + BaseRoutes.User.Handle("/active", ApiSessionRequired(updateUserActive)).Methods("PUT") BaseRoutes.User.Handle("/password", ApiSessionRequired(updatePassword)).Methods("PUT") BaseRoutes.Users.Handle("/password/reset", ApiHandler(resetPassword)).Methods("POST") BaseRoutes.Users.Handle("/password/reset/send", ApiHandler(sendPasswordReset)).Methods("POST") @@ -43,6 +44,7 @@ func InitUser() { BaseRoutes.User.Handle("/mfa/generate", ApiSessionRequired(generateMfaSecret)).Methods("POST") BaseRoutes.Users.Handle("/login", ApiHandler(login)).Methods("POST") + BaseRoutes.Users.Handle("/login/switch", ApiHandler(switchAccountType)).Methods("POST") BaseRoutes.Users.Handle("/logout", ApiHandler(logout)).Methods("POST") BaseRoutes.UserByUsername.Handle("", ApiSessionRequired(getUserByUsername)).Methods("GET") @@ -586,6 +588,37 @@ func updateUserRoles(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } +func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + props := model.StringInterfaceFromJson(r.Body) + + active, ok := props["active"].(bool) + if !ok { + c.SetInvalidParam("active") + return + } + + // true when you're trying to de-activate yourself + isSelfDeactive := !active && c.Params.UserId == c.Session.UserId + + if !isSelfDeactive && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.Err = model.NewLocAppError("updateUserActive", "api.user.update_active.permissions.app_error", nil, "userId="+c.Params.UserId) + c.Err.StatusCode = http.StatusForbidden + return + } + + if ruser, err := app.UpdateActiveNoLdap(c.Params.UserId, active); err != nil { + c.Err = err + } else { + c.LogAuditWithUserId(ruser.Id, fmt.Sprintf("active=%v", active)) + ReturnStatusOK(w) + } +} + func checkUserMfa(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) @@ -981,3 +1014,40 @@ func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } + +func switchAccountType(c *Context, w http.ResponseWriter, r *http.Request) { + switchRequest := model.SwitchRequestFromJson(r.Body) + if switchRequest == nil { + c.SetInvalidParam("switch_request") + return + } + + link := "" + var err *model.AppError + + if switchRequest.EmailToOAuth() { + link, err = app.SwitchEmailToOAuth(switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.NewService) + } else if switchRequest.OAuthToEmail() { + c.SessionRequired() + if c.Err != nil { + return + } + + link, err = app.SwitchOAuthToEmail(switchRequest.Email, switchRequest.NewPassword, c.Session.UserId) + } else if switchRequest.EmailToLdap() { + link, err = app.SwitchEmailToLdap(switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.LdapId, switchRequest.NewPassword) + } else if switchRequest.LdapToEmail() { + link, err = app.SwitchLdapToEmail(switchRequest.Password, switchRequest.MfaCode, switchRequest.Email, switchRequest.NewPassword) + } else { + c.SetInvalidParam("switch_request") + return + } + + if err != nil { + c.Err = err + return + } + + c.LogAudit("success") + w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) +} diff --git a/api4/user_test.go b/api4/user_test.go index b3e4edc3d..95271984c 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 @@ -850,6 +850,49 @@ func TestUpdateUserRoles(t *testing.T) { CheckBadRequestStatus(t, resp) } +func TestUpdateUserActive(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + Client := th.Client + SystemAdminClient := th.SystemAdminClient + user := th.BasicUser + + pass, resp := Client.UpdateUserActive(user.Id, false) + CheckNoError(t, resp) + + if !pass { + t.Fatal("should have returned true") + } + + pass, resp = Client.UpdateUserActive(user.Id, false) + CheckUnauthorizedStatus(t, resp) + + if pass { + t.Fatal("should have returned false") + } + + th.LoginBasic2() + + _, resp = Client.UpdateUserActive(user.Id, true) + CheckForbiddenStatus(t, resp) + + _, resp = Client.UpdateUserActive(GenerateTestId(), true) + CheckForbiddenStatus(t, resp) + + _, resp = Client.UpdateUserActive("junk", true) + CheckBadRequestStatus(t, resp) + + Client.Logout() + + _, resp = Client.UpdateUserActive(user.Id, true) + CheckUnauthorizedStatus(t, resp) + + _, resp = SystemAdminClient.UpdateUserActive(user.Id, true) + CheckNoError(t, resp) + + _, resp = SystemAdminClient.UpdateUserActive(user.Id, false) + CheckNoError(t, resp) +} + func TestGetUsers(t *testing.T) { th := Setup().InitBasic() defer TearDown() @@ -1297,7 +1340,7 @@ func TestUpdateUserPassword(t *testing.T) { // Should fail because account is locked out _, resp = Client.UpdateUserPassword(th.BasicUser.Id, th.BasicUser.Password, "newpwd") CheckErrorMessage(t, resp, "api.user.check_user_login_attempts.too_many.app_error") - CheckForbiddenStatus(t, resp) + CheckUnauthorizedStatus(t, resp) // System admin can update another user's password adminSetPassword := "pwdsetbyadmin" @@ -1651,3 +1694,94 @@ func TestSetProfileImage(t *testing.T) { t.Fatal(err) } } + +func TestSwitchAccount(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + enableGitLab := utils.Cfg.GitLabSettings.Enable + defer func() { + utils.Cfg.GitLabSettings.Enable = enableGitLab + }() + utils.Cfg.GitLabSettings.Enable = true + + Client.Logout() + + sr := &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_EMAIL, + NewService: model.USER_AUTH_SERVICE_GITLAB, + Email: th.BasicUser.Email, + Password: th.BasicUser.Password, + } + + link, resp := Client.SwitchAccountType(sr) + CheckNoError(t, resp) + + if link == "" { + t.Fatal("bad link") + } + + th.LoginBasic() + + fakeAuthData := "1" + if result := <-app.Srv.Store.User().UpdateAuthData(th.BasicUser.Id, model.USER_AUTH_SERVICE_GITLAB, &fakeAuthData, th.BasicUser.Email, true); result.Err != nil { + t.Fatal(result.Err) + } + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_GITLAB, + NewService: model.USER_AUTH_SERVICE_EMAIL, + Email: th.BasicUser.Email, + NewPassword: th.BasicUser.Password, + } + + link, resp = Client.SwitchAccountType(sr) + CheckNoError(t, resp) + + if link != "/login?extra=signin_change" { + t.Log(link) + t.Fatal("bad link") + } + + Client.Logout() + _, resp = Client.Login(th.BasicUser.Email, th.BasicUser.Password) + CheckNoError(t, resp) + Client.Logout() + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_GITLAB, + NewService: model.SERVICE_GOOGLE, + } + + _, resp = Client.SwitchAccountType(sr) + CheckBadRequestStatus(t, resp) + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_EMAIL, + NewService: model.USER_AUTH_SERVICE_GITLAB, + Password: th.BasicUser.Password, + } + + _, resp = Client.SwitchAccountType(sr) + CheckNotFoundStatus(t, resp) + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_EMAIL, + NewService: model.USER_AUTH_SERVICE_GITLAB, + Email: th.BasicUser.Email, + } + + _, resp = Client.SwitchAccountType(sr) + CheckUnauthorizedStatus(t, resp) + + sr = &model.SwitchRequest{ + CurrentService: model.USER_AUTH_SERVICE_GITLAB, + NewService: model.USER_AUTH_SERVICE_EMAIL, + Email: th.BasicUser.Email, + NewPassword: th.BasicUser.Password, + } + + _, resp = Client.SwitchAccountType(sr) + CheckUnauthorizedStatus(t, resp) +} diff --git a/api4/webhook.go b/api4/webhook.go index 86f8bcc2e..6602c7a26 100644 --- a/api4/webhook.go +++ b/api4/webhook.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/webhook_test.go b/api4/webhook_test.go index 1691b27e5..96451f8a7 100644 --- a/api4/webhook_test.go +++ b/api4/webhook_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/webrtc.go b/api4/webrtc.go new file mode 100644 index 000000000..81a1599b2 --- /dev/null +++ b/api4/webrtc.go @@ -0,0 +1,29 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "net/http" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/utils" +) + +func InitWebrtc() { + l4g.Debug(utils.T("api.webrtc.init.debug")) + + BaseRoutes.Webrtc.Handle("/token", ApiSessionRequired(webrtcToken)).Methods("GET") +} + +func webrtcToken(c *Context, w http.ResponseWriter, r *http.Request) { + result, err := app.GetWebrtcInfoForSession(c.Session.Id) + + if err != nil { + c.Err = err + return + } + + w.Write([]byte(result.ToJson())) +} diff --git a/api4/webrtc_test.go b/api4/webrtc_test.go new file mode 100644 index 000000000..806118f76 --- /dev/null +++ b/api4/webrtc_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "testing" + + "github.com/mattermost/platform/utils" +) + +func TestGetWebrtcToken(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + enableWebrtc := *utils.Cfg.WebrtcSettings.Enable + defer func() { + *utils.Cfg.WebrtcSettings.Enable = enableWebrtc + }() + *utils.Cfg.WebrtcSettings.Enable = false + + _, resp := Client.GetWebrtcToken() + CheckNotImplementedStatus(t, resp) + + Client.Logout() + _, resp = Client.GetWebrtcToken() + CheckUnauthorizedStatus(t, resp) +} diff --git a/api4/websocket.go b/api4/websocket.go index c70327222..1caef6c4a 100644 --- a/api4/websocket.go +++ b/api4/websocket.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 diff --git a/api4/websocket_test.go b/api4/websocket_test.go index 6018bf7da..1098f4759 100644 --- a/api4/websocket_test.go +++ b/api4/websocket_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package api4 |