diff --git a/api4/channel.go b/api4/channel.go index 84275acc09..99c7d65134 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -27,6 +27,7 @@ func (api *API) InitChannel() { api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.ApiSessionRequired(getDeletedChannelsForTeam)).Methods("GET") api.BaseRoutes.ChannelsForTeam.Handle("/ids", api.ApiSessionRequired(getPublicChannelsByIdsForTeam)).Methods("POST") api.BaseRoutes.ChannelsForTeam.Handle("/search", api.ApiSessionRequired(searchChannelsForTeam)).Methods("POST") + api.BaseRoutes.ChannelsForTeam.Handle("/search_archived", api.ApiSessionRequired(searchArchivedChannelsForTeam)).Methods("POST") api.BaseRoutes.ChannelsForTeam.Handle("/autocomplete", api.ApiSessionRequired(autocompleteChannelsForTeam)).Methods("GET") api.BaseRoutes.ChannelsForTeam.Handle("/search_autocomplete", api.ApiSessionRequired(autocompleteChannelsForTeamForSearch)).Methods("GET") api.BaseRoutes.User.Handle("/teams/{team_id:[A-Za-z0-9]+}/channels", api.ApiSessionRequired(getChannelsForTeamForUser)).Methods("GET") @@ -681,12 +682,7 @@ func getDeletedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Reques return } - if !c.App.SessionHasPermissionToTeam(c.App.Session, c.Params.TeamId, model.PERMISSION_MANAGE_TEAM) { - c.SetPermissionError(model.PERMISSION_MANAGE_TEAM) - return - } - - channels, err := c.App.GetDeletedChannels(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage) + channels, err := c.App.GetDeletedChannels(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage, c.App.Session.UserId) if err != nil { c.Err = err return @@ -860,6 +856,42 @@ func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(channels.ToJson())) } +func searchArchivedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + + props := model.ChannelSearchFromJson(r.Body) + if props == nil { + c.SetInvalidParam("channel_search") + return + } + + var channels *model.ChannelList + var err *model.AppError + if c.App.SessionHasPermissionToTeam(c.App.Session, c.Params.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) { + channels, err = c.App.SearchArchivedChannels(c.Params.TeamId, props.Term, c.App.Session.UserId) + } else { + // If the user is not a team member, return a 404 + if _, err = c.App.GetTeamMember(c.Params.TeamId, c.App.Session.UserId); err != nil { + c.Err = err + return + } + + channels, err = c.App.SearchArchivedChannels(c.Params.TeamId, props.Term, c.App.Session.UserId) + } + + if err != nil { + c.Err = err + return + } + + // Don't fill in channels props, since unused by client and potentially expensive. + + w.Write([]byte(channels.ToJson())) +} + func searchAllChannels(c *Context, w http.ResponseWriter, r *http.Request) { props := model.ChannelSearchFromJson(r.Body) if props == nil { diff --git a/api4/channel_test.go b/api4/channel_test.go index 3b634cd84a..4cefce513a 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -678,9 +678,6 @@ func TestGetDeletedChannelsForTeam(t *testing.T) { Client := th.Client team := th.BasicTeam - _, resp := Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") - CheckForbiddenStatus(t, resp) - th.LoginTeamAdmin() channels, resp := Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") @@ -706,6 +703,31 @@ func TestGetDeletedChannelsForTeam(t *testing.T) { t.Fatal("should be 2 deleted channels") } + th.LoginBasic() + + privateChannel1 := th.CreatePrivateChannel() + Client.DeleteChannel(privateChannel1.Id) + + channels, resp = Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") + CheckNoError(t, resp) + if len(channels) != numInitialChannelsForTeam+3 { + t.Fatal("should be 3 deleted channels") + } + + // Login as different user and create private channel + th.LoginBasic2() + privateChannel2 := th.CreatePrivateChannel() + Client.DeleteChannel(privateChannel2.Id) + + // Log back in as first user + th.LoginBasic() + + channels, resp = Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") + CheckNoError(t, resp) + if len(channels) != numInitialChannelsForTeam+3 { + t.Fatal("should still be 3 deleted channels", len(channels), numInitialChannelsForTeam+3) + } + channels, resp = Client.GetDeletedChannelsForTeam(team.Id, 0, 1, "") CheckNoError(t, resp) if len(channels) != 1 { @@ -1060,6 +1082,100 @@ func TestSearchChannels(t *testing.T) { }) } +func TestSearchArchivedChannels(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + Client := th.Client + + search := &model.ChannelSearch{Term: th.BasicChannel.Name} + + Client.DeleteChannel(th.BasicChannel.Id) + + channels, resp := Client.SearchArchivedChannels(th.BasicTeam.Id, search) + CheckNoError(t, resp) + + found := false + for _, c := range channels { + if c.Type != model.CHANNEL_OPEN { + t.Fatal("should only return public channels") + } + + if c.Id == th.BasicChannel.Id { + found = true + } + } + + if !found { + t.Fatal("didn't find channel") + } + + search.Term = th.BasicPrivateChannel.Name + Client.DeleteChannel(th.BasicPrivateChannel.Id) + + channels, resp = Client.SearchArchivedChannels(th.BasicTeam.Id, search) + CheckNoError(t, resp) + + found = false + for _, c := range channels { + if c.Id == th.BasicPrivateChannel.Id { + found = true + } + } + + if !found { + t.Fatal("couldn't find private channel") + } + + search.Term = "" + _, resp = Client.SearchArchivedChannels(th.BasicTeam.Id, search) + CheckNoError(t, resp) + + search.Term = th.BasicDeletedChannel.Name + _, resp = Client.SearchArchivedChannels(model.NewId(), search) + CheckNotFoundStatus(t, resp) + + _, resp = Client.SearchArchivedChannels("junk", search) + CheckBadRequestStatus(t, resp) + + _, resp = th.SystemAdminClient.SearchArchivedChannels(th.BasicTeam.Id, search) + CheckNoError(t, resp) + + // Check the appropriate permissions are enforced. + defaultRolePermissions := th.SaveDefaultRolePermissions() + defer func() { + th.RestoreDefaultRolePermissions(defaultRolePermissions) + }() + + // Remove list channels permission from the user + th.RemovePermissionFromRole(model.PERMISSION_LIST_TEAM_CHANNELS.Id, model.TEAM_USER_ROLE_ID) + + t.Run("Search for a BasicDeletedChannel, which the user is a member of", func(t *testing.T) { + search.Term = th.BasicDeletedChannel.Name + channelList, resp := Client.SearchArchivedChannels(th.BasicTeam.Id, search) + CheckNoError(t, resp) + + channelNames := []string{} + for _, c := range channelList { + channelNames = append(channelNames, c.Name) + } + require.Contains(t, channelNames, th.BasicDeletedChannel.Name) + }) + + t.Run("Remove the user from BasicDeletedChannel and search again, should still return", func(t *testing.T) { + th.App.RemoveUserFromChannel(th.BasicUser.Id, th.BasicUser.Id, th.BasicDeletedChannel) + + search.Term = th.BasicDeletedChannel.Name + channelList, resp := Client.SearchArchivedChannels(th.BasicTeam.Id, search) + CheckNoError(t, resp) + + channelNames := []string{} + for _, c := range channelList { + channelNames = append(channelNames, c.Name) + } + require.Contains(t, channelNames, th.BasicDeletedChannel.Name) + }) +} + func TestSearchAllChannels(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/channel.go b/app/channel.go index aa0e54bbb8..cfde9ccd17 100644 --- a/app/channel.go +++ b/app/channel.go @@ -1260,8 +1260,8 @@ func (a *App) GetAllChannelsCount(opts model.ChannelSearchOpts) (int64, *model.A return a.Srv.Store.Channel().GetAllChannelsCount(storeOpts) } -func (a *App) GetDeletedChannels(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError) { - return a.Srv.Store.Channel().GetDeleted(teamId, offset, limit) +func (a *App) GetDeletedChannels(teamId string, offset int, limit int, userId string) (*model.ChannelList, *model.AppError) { + return a.Srv.Store.Channel().GetDeleted(teamId, offset, limit, userId) } func (a *App) GetChannelsUserNotIn(teamId string, userId string, offset int, limit int) (*model.ChannelList, *model.AppError) { @@ -1860,6 +1860,12 @@ func (a *App) SearchChannels(teamId string, term string) (*model.ChannelList, *m return a.Srv.Store.Channel().SearchInTeam(teamId, term, includeDeleted) } +func (a *App) SearchArchivedChannels(teamId string, term string, userId string) (*model.ChannelList, *model.AppError) { + term = strings.TrimSpace(term) + + return a.Srv.Store.Channel().SearchArchivedInTeam(teamId, term, userId) +} + func (a *App) SearchChannelsForUser(userId, teamId, term string) (*model.ChannelList, *model.AppError) { includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels diff --git a/model/client4.go b/model/client4.go index 2dffa31291..7bc8730ddd 100644 --- a/model/client4.go +++ b/model/client4.go @@ -2235,6 +2235,16 @@ func (c *Client4) SearchChannels(teamId string, search *ChannelSearch) ([]*Chann return ChannelSliceFromJson(r.Body), BuildResponse(r) } +// SearchArchivedChannels returns the archived channels on a team matching the provided search term. +func (c *Client4) SearchArchivedChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelsForTeamRoute(teamId)+"/search_archived", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + // SearchAllChannels search in all the channels. Must be a system administrator. func (c *Client4) SearchAllChannels(search *ChannelSearch) (*ChannelListWithTeamData, *Response) { r, err := c.DoApiPost(c.GetChannelsRoute()+"/search", search.ToJson()) diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index aba5d38d77..bf05278aaf 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1036,7 +1036,7 @@ func (s SqlChannelStore) GetPublicChannelsForTeam(teamId string, offset int, lim PublicChannels pc ON (pc.Id = Channels.Id) WHERE pc.TeamId = :TeamId - AND pc.DeleteAt = 0 + AND pc.DeleteAt = 0 ORDER BY pc.DisplayName LIMIT :Limit OFFSET :Offset @@ -1242,10 +1242,24 @@ func (s SqlChannelStore) GetDeletedByName(teamId string, name string) (*model.Ch return &channel, nil } -func (s SqlChannelStore) GetDeleted(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError) { +func (s SqlChannelStore) GetDeleted(teamId string, offset int, limit int, userId string) (*model.ChannelList, *model.AppError) { channels := &model.ChannelList{} - if _, err := s.GetReplica().Select(channels, "SELECT * FROM Channels WHERE (TeamId = :TeamId OR TeamId = '') AND DeleteAt != 0 ORDER BY DisplayName LIMIT :Limit OFFSET :Offset", map[string]interface{}{"TeamId": teamId, "Limit": limit, "Offset": offset}); err != nil { + query := ` + SELECT * FROM Channels + WHERE (TeamId = :TeamId OR TeamId = '') + AND DeleteAt != 0 + AND Type != 'P' + UNION + SELECT * FROM Channels + WHERE (TeamId = :TeamId OR TeamId = '') + AND DeleteAt != 0 + AND Type = 'P' + AND Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId) + ORDER BY DisplayName LIMIT :Limit OFFSET :Offset + ` + + if _, err := s.GetReplica().Select(channels, query, map[string]interface{}{"TeamId": teamId, "Limit": limit, "Offset": offset, "UserId": userId}); err != nil { if err == sql.ErrNoRows { return nil, model.NewAppError("SqlChannelStore.GetDeleted", "store.sql_channel.get_deleted.missing.app_error", nil, "teamId="+teamId+", "+err.Error(), http.StatusNotFound) } @@ -2154,6 +2168,57 @@ func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted }) } +func (s SqlChannelStore) SearchArchivedInTeam(teamId string, term string, userId string) (*model.ChannelList, *model.AppError) { + publicChannels, publicErr := s.performSearch(` + SELECT + Channels.* + FROM + Channels + JOIN + Channels c ON (c.Id = Channels.Id) + WHERE + c.TeamId = :TeamId + SEARCH_CLAUSE + AND c.DeleteAt != 0 + AND c.Type != 'P' + ORDER BY c.DisplayName + LIMIT 100 + `, term, map[string]interface{}{ + "TeamId": teamId, + "UserId": userId, + }) + + privateChannels, privateErr := s.performSearch(` + SELECT + Channels.* + FROM + Channels + JOIN + Channels c ON (c.Id = Channels.Id) + WHERE + c.TeamId = :TeamId + SEARCH_CLAUSE + AND c.DeleteAt != 0 + AND c.Type = 'P' + AND c.Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId) + ORDER BY c.DisplayName + LIMIT 100 + `, term, map[string]interface{}{ + "TeamId": teamId, + "UserId": userId, + }) + + output := *publicChannels + output = append(output, *privateChannels...) + + outputErr := publicErr + if privateErr != nil { + outputErr = privateErr + } + + return &output, outputErr +} + func (s SqlChannelStore) SearchForUserInTeam(userId string, teamId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) { deleteFilter := "AND c.DeleteAt = 0" if includeDeleted { diff --git a/store/store.go b/store/store.go index 59ef9f693e..330a95496e 100644 --- a/store/store.go +++ b/store/store.go @@ -125,7 +125,7 @@ type ChannelStore interface { GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, *model.AppError) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, *model.AppError) GetDeletedByName(team_id string, name string) (*model.Channel, *model.AppError) - GetDeleted(team_id string, offset int, limit int) (*model.ChannelList, *model.AppError) + GetDeleted(team_id string, offset int, limit int, userId string) (*model.ChannelList, *model.AppError) GetChannels(teamId string, userId string, includeDeleted bool) (*model.ChannelList, *model.AppError) GetAllChannels(page, perPage int, opts ChannelSearchOpts) (*model.ChannelListWithTeamData, *model.AppError) GetAllChannelsCount(opts ChannelSearchOpts) (int64, *model.AppError) @@ -170,6 +170,7 @@ type ChannelStore interface { AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) SearchAllChannels(term string, opts ChannelSearchOpts) (*model.ChannelListWithTeamData, *model.AppError) SearchInTeam(teamId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) + SearchArchivedInTeam(teamId string, term string, userId string) (*model.ChannelList, *model.AppError) SearchForUserInTeam(userId string, teamId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) SearchMore(userId string, teamId string, term string) (*model.ChannelList, *model.AppError) SearchGroupChannels(userId, term string) (*model.ChannelList, *model.AppError) diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 29b88be307..0829335afc 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -687,13 +687,15 @@ func testChannelStoreGetDeleted(t *testing.T, ss store.Store) { o1.Name = "zz" + model.NewId() + "b" o1.Type = model.CHANNEL_OPEN + userId := model.NewId() + _, err := ss.Channel().Save(&o1, -1) require.Nil(t, err) err = ss.Channel().Delete(o1.Id, model.GetMillis()) require.Nil(t, err, "channel should have been deleted") - list, err := ss.Channel().GetDeleted(o1.TeamId, 0, 100) + list, err := ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId) require.Nil(t, err, err) require.Len(t, *list, 1, "wrong list") require.Equal(t, o1.Name, (*list)[0].Name, "missing channel") @@ -706,7 +708,7 @@ func testChannelStoreGetDeleted(t *testing.T, ss store.Store) { _, err = ss.Channel().Save(&o2, -1) require.Nil(t, err) - list, err = ss.Channel().GetDeleted(o1.TeamId, 0, 100) + list, err = ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId) require.Nil(t, err, err) require.Len(t, *list, 1, "wrong list") @@ -722,15 +724,15 @@ func testChannelStoreGetDeleted(t *testing.T, ss store.Store) { err = ss.Channel().Delete(o3.Id, model.GetMillis()) require.Nil(t, err, "channel should have been deleted") - list, err = ss.Channel().GetDeleted(o1.TeamId, 0, 100) + list, err = ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId) require.Nil(t, err, err) require.Len(t, *list, 2, "wrong list length") - list, err = ss.Channel().GetDeleted(o1.TeamId, 0, 1) + list, err = ss.Channel().GetDeleted(o1.TeamId, 0, 1, userId) require.Nil(t, err, err) require.Len(t, *list, 1, "wrong list length") - list, err = ss.Channel().GetDeleted(o1.TeamId, 1, 1) + list, err = ss.Channel().GetDeleted(o1.TeamId, 1, 1, userId) require.Nil(t, err, err) require.Len(t, *list, 1, "wrong list length") diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 8eb871ced0..5b3a58048e 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -647,7 +647,7 @@ func (_m *ChannelStore) GetChannelsByScheme(schemeId string, offset int, limit i } // GetDeleted provides a mock function with given fields: team_id, offset, limit -func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int) (*model.ChannelList, *model.AppError) { +func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int, userId string) (*model.ChannelList, *model.AppError) { ret := _m.Called(team_id, offset, limit) var r0 *model.ChannelList @@ -1539,6 +1539,31 @@ func (_m *ChannelStore) SearchInTeam(teamId string, term string, includeDeleted return r0, r1 } +// SearchArchivedInTeam provides a mock function with given fields: teamId, term, userId +func (_m *ChannelStore) SearchArchivedInTeam(teamId string, term string, userId string) (*model.ChannelList, *model.AppError) { + ret := _m.Called(teamId, term, userId) + + var r0 *model.ChannelList + if rf, ok := ret.Get(0).(func(string, string, string) *model.ChannelList); ok { + r0 = rf(teamId, term, userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.ChannelList) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, string, string) *model.AppError); ok { + r1 = rf(teamId, term, userId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // SearchMore provides a mock function with given fields: userId, teamId, term func (_m *ChannelStore) SearchMore(userId string, teamId string, term string) (*model.ChannelList, *model.AppError) { ret := _m.Called(userId, teamId, term) diff --git a/store/timer_layer.go b/store/timer_layer.go index 366150b79a..4ee55ba206 100644 --- a/store/timer_layer.go +++ b/store/timer_layer.go @@ -904,10 +904,10 @@ func (s *TimerLayerChannelStore) GetChannelsByScheme(schemeId string, offset int return resultVar0, resultVar1 } -func (s *TimerLayerChannelStore) GetDeleted(team_id string, offset int, limit int) (*model.ChannelList, *model.AppError) { +func (s *TimerLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userId string) (*model.ChannelList, *model.AppError) { start := timemodule.Now() - resultVar0, resultVar1 := s.ChannelStore.GetDeleted(team_id, offset, limit) + resultVar0, resultVar1 := s.ChannelStore.GetDeleted(team_id, offset, limit, userId) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil {