[MM-45272] Fix MM-45272 (#24701)

* Fix MM-45272

* Properly handle permalinks

* Fix

* Fix tests

* Handle only not found case for team member

* Fix lint

* Use proper config value

* Separate permission in several statements

* Add tests

* Fix lint

* Revert changes on utils

* Address feedback and more fixes

* Address feedback

* Fix test

* Fix test and related bug

* Fix and reorder test

* Address feedback

* Address feedback

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Daniel Espino García 2023-11-30 11:43:51 +01:00 committed by GitHub
parent c395ec6245
commit 2ff0fe343e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 381 additions and 178 deletions

View File

@ -3795,7 +3795,7 @@ func TestPostGetInfo(t *testing.T) {
dmPost, _, err := client.CreatePost(context.Background(), &model.Post{ChannelId: dmChannel.Id})
require.NoError(t, err)
openTeam, _, err := sysadminClient.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "open-team", DisplayName: "Open Team"})
openTeam, _, err := sysadminClient.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "open-team", DisplayName: "Open Team", AllowOpenInvite: true})
require.NoError(t, err)
openTeamOpenChannel, _, err := sysadminClient.CreateChannel(context.Background(), &model.Channel{TeamId: openTeam.Id, Type: model.ChannelTypeOpen, Name: "open-team-open-channel", DisplayName: "Open Team - Open Channel"})
require.NoError(t, err)
@ -3803,7 +3803,7 @@ func TestPostGetInfo(t *testing.T) {
require.NoError(t, err)
// Alt team is a team without the sysadmin in it.
altOpenTeam, _, err := client.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "alt-open-team", DisplayName: "Alt Open Team"})
altOpenTeam, _, err := client.CreateTeam(context.Background(), &model.Team{Type: model.TeamOpen, Name: "alt-open-team", DisplayName: "Alt Open Team", AllowOpenInvite: true})
require.NoError(t, err)
altOpenTeamOpenChannel, _, err := client.CreateChannel(context.Background(), &model.Channel{TeamId: altOpenTeam.Id, Type: model.ChannelTypeOpen, Name: "alt-open-team-open-channel", DisplayName: "Open Team - Open Channel"})
require.NoError(t, err)
@ -4008,8 +4008,12 @@ func TestPostGetInfo(t *testing.T) {
require.Equal(t, tc.channel.DisplayName, info.ChannelDisplayName)
require.Equal(t, tc.hasJoinedChannel, info.HasJoinedChannel)
if tc.team != nil {
teamType := "I"
if tc.team.AllowOpenInvite {
teamType = "O"
}
require.Equal(t, tc.team.Id, info.TeamId)
require.Equal(t, tc.team.Type, info.TeamType)
require.Equal(t, teamType, info.TeamType)
require.Equal(t, tc.team.DisplayName, info.TeamDisplayName)
require.Equal(t, tc.hasJoinedTeam, info.HasJoinedTeam)
}

View File

@ -380,5 +380,13 @@ func (a *App) HasPermissionToReadChannel(c request.CTX, userID string, channel *
if !*a.Config().TeamSettings.ExperimentalViewArchivedChannels && channel.DeleteAt != 0 {
return false
}
return a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannelContent) || (channel.Type == model.ChannelTypeOpen && a.HasPermissionToTeam(c, userID, channel.TeamId, model.PermissionReadPublicChannel))
if a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannelContent) {
return true
}
if channel.Type == model.ChannelTypeOpen && !*a.Config().ComplianceSettings.Enable {
return a.HasPermissionToTeam(c, userID, channel.TeamId, model.PermissionReadPublicChannel)
}
return false
}

View File

@ -553,3 +553,117 @@ func TestSessionHasPermissionToGroup(t *testing.T) {
}
}
}
func TestHasPermissionToReadChannel(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
ttcc := []struct {
name string
configViewArchived bool
configComplianceEnabled bool
channelDeleted bool
canReadChannel bool
channelIsOpen bool
canReadPublicChannel bool
expected bool
}{
{
name: "Cannot read archived channels if the config doesn't allow it",
configViewArchived: false,
configComplianceEnabled: true,
channelDeleted: true,
canReadChannel: true,
channelIsOpen: true,
canReadPublicChannel: true,
expected: false,
},
{
name: "Can read if it has permissions to read",
configViewArchived: false,
configComplianceEnabled: true,
channelDeleted: false,
canReadChannel: true,
channelIsOpen: false,
canReadPublicChannel: true,
expected: true,
},
{
name: "Cannot read private channels if it has no permission",
configViewArchived: false,
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelIsOpen: false,
canReadPublicChannel: true,
expected: false,
},
{
name: "Cannot read open channels if compliance is enabled",
configViewArchived: false,
configComplianceEnabled: true,
channelDeleted: false,
canReadChannel: false,
channelIsOpen: true,
canReadPublicChannel: true,
expected: false,
},
{
name: "Cannot read open channels if it has no team permissions",
configViewArchived: false,
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelIsOpen: true,
canReadPublicChannel: false,
expected: false,
},
{
name: "Can read open channels if it has team permissions and compliance is not enabled",
configViewArchived: false,
configComplianceEnabled: false,
channelDeleted: false,
canReadChannel: false,
channelIsOpen: true,
canReadPublicChannel: true,
expected: true,
},
}
for _, tc := range ttcc {
t.Run(tc.name, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
configViewArchived := tc.configViewArchived
configComplianceEnabled := tc.configComplianceEnabled
cfg.TeamSettings.ExperimentalViewArchivedChannels = &configViewArchived
cfg.ComplianceSettings.Enable = &configComplianceEnabled
})
team := th.CreateTeam()
if tc.canReadPublicChannel {
th.LinkUserToTeam(th.BasicUser2, team)
}
var channel *model.Channel
if tc.channelIsOpen {
channel = th.CreateChannel(th.Context, team)
} else {
channel = th.CreatePrivateChannel(th.Context, team)
}
if tc.canReadChannel {
_, err := th.App.AddUserToChannel(th.Context, th.BasicUser2, channel, false)
require.Nil(t, err)
}
if tc.channelDeleted {
err := th.App.DeleteChannel(th.Context, channel, th.SystemAdminUser.Id)
require.Nil(t, err)
channel, err = th.App.GetChannel(th.Context, channel.Id)
require.Nil(t, err)
}
result := th.App.HasPermissionToReadChannel(th.Context, th.BasicUser2.Id, channel)
require.Equal(t, tc.expected, result)
})
}
}

View File

@ -799,10 +799,13 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
return false, err
}
originalEmbeds := post.Metadata.Embeds
originalProps := post.GetProps()
permalinkPreviewedPost := post.GetPreviewPost()
for _, userID := range userIDs {
if permalinkPreviewedPost != nil {
post.Metadata.Embeds[0].Data = permalinkPreviewedPost
post.Metadata.Embeds = originalEmbeds
post.SetProps(originalProps)
}
postForUser := a.sanitizePostMetadataForUserAndChannel(c, post, permalinkPreviewedPost, permalinkPreviewedChannel, userID)
@ -822,6 +825,12 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
a.Publish(messageCopy)
}
// Restore the metadata that may have been removed in the sanitization
if permalinkPreviewedPost != nil {
post.Metadata.Embeds = originalEmbeds
post.SetProps(originalProps)
}
return true, nil
}
@ -2033,7 +2042,7 @@ func (a *App) GetPostIfAuthorized(c request.CTX, postID string, session *model.S
}
if !a.SessionHasPermissionToChannel(c, *session, channel.Id, model.PermissionReadChannelContent) {
if channel.Type == model.ChannelTypeOpen {
if channel.Type == model.ChannelTypeOpen && !*a.Config().ComplianceSettings.Enable {
if !a.SessionHasPermissionToTeam(*session, channel.TeamId, model.PermissionReadPublicChannel) {
return nil, a.MakePermissionError(session, []*model.Permission{model.PermissionReadPublicChannel})
}
@ -2233,10 +2242,23 @@ func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model
return nil, appErr
}
if team.Type == model.TeamOpen {
hasPermissionToAccessTeam = a.HasPermissionToTeam(c, userID, team.Id, model.PermissionJoinPublicTeams)
} else if team.Type == model.TeamInvite {
hasPermissionToAccessTeam = a.HasPermissionToTeam(c, userID, team.Id, model.PermissionJoinPrivateTeams)
teamMember, appErr := a.GetTeamMember(c, channel.TeamId, userID)
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
return nil, appErr
}
if appErr == nil {
if teamMember.DeleteAt == 0 {
hasPermissionToAccessTeam = true
}
}
if !hasPermissionToAccessTeam {
if team.AllowOpenInvite {
hasPermissionToAccessTeam = a.HasPermissionToTeam(c, userID, team.Id, model.PermissionJoinPublicTeams)
} else {
hasPermissionToAccessTeam = a.HasPermissionToTeam(c, userID, team.Id, model.PermissionJoinPrivateTeams)
}
}
} else {
// This happens in case of DMs and GMs.
@ -2269,12 +2291,16 @@ func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model
HasJoinedChannel: channelMemberErr == nil,
}
if team != nil {
_, teamMemberErr := a.GetTeamMember(c, team.Id, userID)
teamMember, teamMemberErr := a.GetTeamMember(c, team.Id, userID)
teamType := model.TeamInvite
if team.AllowOpenInvite {
teamType = model.TeamOpen
}
info.TeamId = team.Id
info.TeamType = team.Type
info.TeamType = teamType
info.TeamDisplayName = team.DisplayName
info.HasJoinedTeam = teamMemberErr == nil
info.HasJoinedTeam = teamMemberErr == nil && teamMember.DeleteAt == 0
}
return &info, nil
}

View File

@ -196,7 +196,19 @@ func (a *App) sanitizePostMetadataForUserAndChannel(c request.CTX, post *model.P
}
if previewedChannel != nil && !a.HasPermissionToReadChannel(c, userID, previewedChannel) {
post.Metadata.Embeds[0].Data = nil
// Remove all permalink embeds and only keep non-permalink embeds.
// We always have only one permalink embed even if the post
// contains multiple permalinks.
var newEmbeds []*model.PostEmbed
for _, embed := range post.Metadata.Embeds {
if embed.Type != model.PostEmbedPermalink {
newEmbeds = append(newEmbeds, embed)
}
}
post.Metadata.Embeds = newEmbeds
post.DelProp(model.PostPropsPreviewedPost)
}
return post
@ -229,6 +241,8 @@ func (a *App) SanitizePostMetadataForUser(c request.CTX, post *model.Post, userI
}
post.Metadata.Embeds = newEmbeds
post.DelProp(model.PostPropsPreviewedPost)
}
return post, nil

View File

@ -2820,7 +2820,7 @@ func TestSanitizePostMetadataForUserAndChannel(t *testing.T) {
require.Nil(t, appErr)
actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, directChannel, guest.Id)
assert.Nil(t, actual.Metadata.Embeds[0].Data)
assert.Len(t, actual.Metadata.Embeds, 0)
})
t.Run("should not preview for archived channels", func(t *testing.T) {
@ -2882,7 +2882,7 @@ func TestSanitizePostMetadataForUserAndChannel(t *testing.T) {
})
actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, publicChannel, th.BasicUser.Id)
assert.Nil(t, actual.Metadata.Embeds[0].Data)
assert.Len(t, actual.Metadata.Embeds, 0)
})
}

View File

@ -1001,51 +1001,49 @@ func TestCreatePost(t *testing.T) {
directChannel, err := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
require.Nil(t, err)
referencedPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "hello world",
UserId: th.BasicUser.Id,
}
th.Context.Session().UserId = th.BasicUser.Id
referencedPost, err = th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, false, false)
require.Nil(t, err)
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
testCases := []struct {
Description string
Channel *model.Channel
Author string
Assert func(t assert.TestingT, object any, msgAndArgs ...any) bool
Length int
}{
{
Description: "removes metadata from post for members who cannot read channel",
Channel: directChannel,
Author: user1.Id,
Assert: assert.Nil,
Length: 0,
},
{
Description: "does not remove metadata from post for members who can read channel",
Channel: th.BasicChannel,
Author: th.BasicUser.Id,
Assert: assert.NotNil,
Length: 1,
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
previewPost := &model.Post{
referencedPost := &model.Post{
ChannelId: testCase.Channel.Id,
Message: permalink,
Message: "hello world",
UserId: testCase.Author,
}
previewPost, err = th.App.CreatePost(th.Context, previewPost, testCase.Channel, false, false)
referencedPost, err = th.App.CreatePost(th.Context, referencedPost, testCase.Channel, false, false)
require.Nil(t, err)
testCase.Assert(t, previewPost.Metadata.Embeds[0].Data)
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
previewPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: permalink,
UserId: th.BasicUser.Id,
}
previewPost, err = th.App.CreatePost(th.Context, previewPost, th.BasicChannel, false, false)
require.Nil(t, err)
require.Len(t, previewPost.Metadata.Embeds, testCase.Length)
})
}
})
@ -1451,73 +1449,6 @@ func TestUpdatePost(t *testing.T) {
require.Nil(t, err)
assert.Equal(t, testPost.GetProps(), model.StringInterface{"previewed_post": referencedPost.Id})
})
t.Run("sanitizes post metadata appropriately", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
})
th.AddUserToChannel(th.BasicUser, th.BasicChannel)
user1 := th.CreateUser()
user2 := th.CreateUser()
directChannel, err := th.App.createDirectChannel(th.Context, user1.Id, user2.Id)
require.Nil(t, err)
referencedPost := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "hello world",
UserId: th.BasicUser.Id,
}
th.Context.Session().UserId = th.BasicUser.Id
referencedPost, err = th.App.CreatePost(th.Context, referencedPost, th.BasicChannel, false, false)
require.Nil(t, err)
permalink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
testCases := []struct {
Description string
Channel *model.Channel
Author string
Assert func(t assert.TestingT, object any, msgAndArgs ...any) bool
}{
{
Description: "removes metadata from post for members who cannot read channel",
Channel: directChannel,
Author: user1.Id,
Assert: assert.Nil,
},
{
Description: "does not remove metadata from post for members who can read channel",
Channel: th.BasicChannel,
Author: th.BasicUser.Id,
Assert: assert.NotNil,
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
previewPost := &model.Post{
ChannelId: testCase.Channel.Id,
UserId: testCase.Author,
}
previewPost, err = th.App.CreatePost(th.Context, previewPost, testCase.Channel, false, false)
require.Nil(t, err)
previewPost.Message = permalink
previewPost, err = th.App.UpdatePost(th.Context, previewPost, false)
require.Nil(t, err)
testCase.Assert(t, previewPost.Metadata.Embeds[0].Data)
})
}
})
}
func TestSearchPostsForUser(t *testing.T) {
@ -3043,26 +2974,64 @@ func TestGetPostIfAuthorized(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
privateChannel := th.CreatePrivateChannel(th.Context, th.BasicTeam)
post, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: privateChannel.Id, Message: "Hello"}, privateChannel, false, false)
require.Nil(t, err)
require.NotNil(t, post)
t.Run("Private channel", func(t *testing.T) {
privateChannel := th.CreatePrivateChannel(th.Context, th.BasicTeam)
post, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: privateChannel.Id, Message: "Hello"}, privateChannel, false, false)
require.Nil(t, err)
require.NotNil(t, post)
session1, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
require.Nil(t, err)
require.NotNil(t, session1)
session1, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
require.Nil(t, err)
require.NotNil(t, session1)
session2, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
require.Nil(t, err)
require.NotNil(t, session2)
session2, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
require.Nil(t, err)
require.NotNil(t, session2)
// User is not authorized to get post
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
require.NotNil(t, err)
// User is not authorized to get post
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
require.NotNil(t, err)
// User is authorized to get post
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
require.Nil(t, err)
// User is authorized to get post
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
require.Nil(t, err)
})
t.Run("Public channel", func(t *testing.T) {
publicChannel := th.CreateChannel(th.Context, th.BasicTeam)
post, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: publicChannel.Id, Message: "Hello"}, publicChannel, false, false)
require.Nil(t, err)
require.NotNil(t, post)
session1, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser.Id, Props: model.StringMap{}})
require.Nil(t, err)
require.NotNil(t, session1)
session2, err := th.App.CreateSession(th.Context, &model.Session{UserId: th.BasicUser2.Id, Props: model.StringMap{}})
require.Nil(t, err)
require.NotNil(t, session2)
// User is authorized to get post
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
require.Nil(t, err)
// User is authorized to get post
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
require.Nil(t, err)
th.App.UpdateConfig(func(c *model.Config) {
b := true
c.ComplianceSettings.Enable = &b
})
// User is not authorized to get post
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session2, false)
require.NotNil(t, err)
// User is authorized to get post
_, err = th.App.GetPostIfAuthorized(th.Context, post.Id, session1, false)
require.Nil(t, err)
})
}
func TestShouldNotRefollowOnOthersReply(t *testing.T) {

View File

@ -190,7 +190,7 @@ export function goToChannelByChannelName(match: Match, history: History): Action
const isSystemAdmin = UserUtils.isSystemAdmin(user?.roles);
if (isSystemAdmin) {
if (channel?.type === Constants.PRIVATE_CHANNEL) {
const joinPromptResult = await dispatch(joinPrivateChannelPrompt(teamObj, channel));
const joinPromptResult = await dispatch(joinPrivateChannelPrompt(teamObj, channel.display_name));
if ('data' in joinPromptResult && !joinPromptResult.data.join) {
return {data: undefined};
}

View File

@ -7,7 +7,8 @@ import type {Post} from '@mattermost/types/posts';
import {getChannel, getChannelMember, selectChannel, joinChannel, getChannelStats} from 'mattermost-redux/actions/channels';
import {getPostThread} from 'mattermost-redux/actions/posts';
import {getMissingProfilesByIds} from 'mattermost-redux/actions/users';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {Client4} from 'mattermost-redux/client';
import {getCurrentChannel, getChannel as getChannelFromRedux} from 'mattermost-redux/selectors/entities/channels';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam, getTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
@ -88,25 +89,48 @@ export function focusPost(postId: string, returnTo = '', currentUserId: string,
if (privateChannelJoinPromptVisible) {
return;
}
const {data} = await dispatch(getPostThread(postId));
if (!data) {
let postInfo;
try {
postInfo = await Client4.getPostInfo(postId);
} catch (e) {
getHistory().replace(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=${returnTo}`);
return;
}
if (data.first_inaccessible_post_time) {
const state = getState();
const currentTeam = getCurrentTeam(state);
if (!postInfo.has_joined_channel) {
// Prompt system admin before joining the private channel
const user = getCurrentUser(state);
if (postInfo.channel_type === Constants.PRIVATE_CHANNEL && isSystemAdmin(user.roles)) {
privateChannelJoinPromptVisible = true;
const joinPromptResult = await dispatch(joinPrivateChannelPrompt(currentTeam, postInfo.channel_display_name));
privateChannelJoinPromptVisible = false;
if ('data' in joinPromptResult && !joinPromptResult.data.join) {
return;
}
}
await dispatch(joinChannel(currentUserId, '', postInfo.channel_id));
}
const {data: threadData} = await dispatch(getPostThread(postId));
if (!threadData) {
getHistory().replace(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=${returnTo}`);
return;
}
if (threadData.first_inaccessible_post_time) {
getHistory().replace(`/error?type=${ErrorPageTypes.CLOUD_ARCHIVED}&returnTo=${returnTo}`);
return;
}
const state = getState();
const isCollapsed = isCollapsedThreadsEnabled(state);
const channelId = data.posts[data.order[0]].channel_id;
let channel = state.entities.channels.channels[channelId];
const currentTeam = getCurrentTeam(state);
const teamId = currentTeam.id;
const channelId = threadData.posts[threadData.order[0]].channel_id;
let channel = getChannelFromRedux(state, channelId);
if (!channel) {
const {data: channelData} = await dispatch(getChannel(channelId));
@ -119,6 +143,7 @@ export function focusPost(postId: string, returnTo = '', currentUserId: string,
channel = channelData;
}
const teamId = channel.team_id || currentTeam.id;
let myMember = state.entities.channels.myMembers[channelId];
if (!myMember) {
@ -134,17 +159,8 @@ export function focusPost(postId: string, returnTo = '', currentUserId: string,
}
if (!myMember) {
// Prompt system admin before joining the private channel
const user = getCurrentUser(state);
if (channel.type === Constants.PRIVATE_CHANNEL && isSystemAdmin(user.roles)) {
privateChannelJoinPromptVisible = true;
const joinPromptResult = await dispatch(joinPrivateChannelPrompt(currentTeam, channel));
privateChannelJoinPromptVisible = false;
if ('data' in joinPromptResult && !joinPromptResult.data.join) {
return;
}
}
await dispatch(joinChannel(currentUserId, '', channelId, channel.name));
getHistory().replace(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=${returnTo}`);
return;
}
}
@ -161,7 +177,7 @@ export function focusPost(postId: string, returnTo = '', currentUserId: string,
dispatch(loadNewGMIfNeeded(channel.id));
}
const post = data.posts[postId];
const post = threadData.posts[postId];
if (isCollapsed && isComment(post)) {
const {data} = await dispatch(focusReplyPost(post, channel, teamId, returnTo, option));

View File

@ -174,24 +174,46 @@ describe('components/PermalinkView', () => {
};
describe('focusPost', () => {
test('should redirect to error page for DM channel not a member of', async () => {
const testStore = await mockStore(initialState);
await testStore.dispatch(focusPost('dmpostid1', undefined, baseProps.currentUserId));
beforeEach(() => {
TestHelper.initBasic(Client4);
});
expect(getPostThread).toHaveBeenCalledWith('dmpostid1');
afterEach(() => {
TestHelper.tearDown();
});
function nockInfoForPost(postId: string) {
nock(Client4.getPostRoute(postId)).
get('/info').
reply(200, {
has_joined_channel: true,
});
}
test('should redirect to error page for DM channel not a member of', async () => {
const postId = 'dmpostid1';
TestHelper.initBasic(Client4);
nockInfoForPost(postId);
const testStore = await mockStore(initialState);
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
expect(getPostThread).toHaveBeenCalledWith(postId);
expect(testStore.getActions()).toEqual([
{type: 'MOCK_GET_POST_THREAD', data: {posts: {dmpostid1: {id: 'dmpostid1', message: 'some message', channel_id: 'dmchannelid'}}, order: ['dmpostid1']}},
{type: 'MOCK_GET_POST_THREAD', data: {posts: {dmpostid1: {id: postId, message: 'some message', channel_id: 'dmchannelid'}}, order: ['dmpostid1']}},
]);
expect(getHistory().replace).toHaveBeenCalledWith(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=`);
});
test('should redirect to error page for GM channel not a member of', async () => {
const testStore = await mockStore(initialState);
await testStore.dispatch(focusPost('gmpostid1', undefined, baseProps.currentUserId));
const postId = 'gmpostid1';
nockInfoForPost(postId);
expect(getPostThread).toHaveBeenCalledWith('gmpostid1');
const testStore = await mockStore(initialState);
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
expect(getPostThread).toHaveBeenCalledWith(postId);
expect(testStore.getActions()).toEqual([
{type: 'MOCK_GET_POST_THREAD', data: {posts: {gmpostid1: {id: 'gmpostid1', message: 'some message', channel_id: 'gmchannelid'}}, order: ['gmpostid1']}},
{type: 'MOCK_GET_POST_THREAD', data: {posts: {gmpostid1: {id: postId, message: 'some message', channel_id: 'gmchannelid'}}, order: ['gmpostid1']}},
]);
expect(getHistory().replace).toHaveBeenCalledWith(`/error?type=${ErrorPageTypes.PERMALINK_NOT_FOUND}&returnTo=`);
});
@ -199,7 +221,9 @@ describe('components/PermalinkView', () => {
test('should redirect to DM link with postId for permalink', async () => {
const dateNowOrig = Date.now;
Date.now = () => new Date(0).getMilliseconds();
const nextTick = () => new Promise((res) => process.nextTick(res));
const postId = 'dmpostid1';
nockInfoForPost(postId);
TestHelper.initBasic(Client4);
nock(Client4.getUsersRoute()).
@ -220,13 +244,12 @@ describe('components/PermalinkView', () => {
};
const testStore = mockStore(modifiedState);
testStore.dispatch(focusPost('dmpostid1', undefined, baseProps.currentUserId));
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
await nextTick();
expect.assertions(3);
expect(getPostThread).toHaveBeenCalledWith('dmpostid1');
expect(getPostThread).toHaveBeenCalledWith(postId);
expect(testStore.getActions()).toEqual([
{type: 'MOCK_GET_POST_THREAD', data: {posts: {dmpostid1: {id: 'dmpostid1', message: 'some message', channel_id: 'dmchannelid'}}, order: ['dmpostid1']}},
{type: 'MOCK_GET_POST_THREAD', data: {posts: {dmpostid1: {id: postId, message: 'some message', channel_id: 'dmchannelid'}}, order: [postId]}},
{type: 'MOCK_GET_MISSING_PROFILES', userIds: ['dmchannel']},
{
type: 'RECEIVED_PREFERENCES',
@ -236,17 +259,19 @@ describe('components/PermalinkView', () => {
],
},
{type: 'MOCK_SELECT_CHANNEL', args: ['dmchannelid']},
{type: 'RECEIVED_FOCUSED_POST', channelId: 'dmchannelid', data: 'dmpostid1'},
{type: 'RECEIVED_FOCUSED_POST', channelId: 'dmchannelid', data: postId},
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
{type: 'MOCK_GET_CHANNEL_STATS', args: ['dmchannelid']},
]);
expect(getHistory().replace).toHaveBeenCalledWith('/currentteam/messages/@otherUser/dmpostid1');
Date.now = dateNowOrig;
TestHelper.tearDown();
});
test('should redirect to GM link with postId for permalink', async () => {
const postId = 'gmpostid1';
nockInfoForPost(postId);
const modifiedState = {
entities: {
...initialState.entities,
@ -261,13 +286,13 @@ describe('components/PermalinkView', () => {
};
const testStore = await mockStore(modifiedState);
await testStore.dispatch(focusPost('gmpostid1', undefined, baseProps.currentUserId));
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
expect(getPostThread).toHaveBeenCalledWith('gmpostid1');
expect(getPostThread).toHaveBeenCalledWith(postId);
expect(testStore.getActions()).toEqual([
{type: 'MOCK_GET_POST_THREAD', data: {posts: {gmpostid1: {id: 'gmpostid1', message: 'some message', channel_id: 'gmchannelid'}}, order: ['gmpostid1']}},
{type: 'MOCK_GET_POST_THREAD', data: {posts: {gmpostid1: {id: postId, message: 'some message', channel_id: 'gmchannelid'}}, order: [postId]}},
{type: 'MOCK_SELECT_CHANNEL', args: ['gmchannelid']},
{type: 'RECEIVED_FOCUSED_POST', channelId: 'gmchannelid', data: 'gmpostid1'},
{type: 'RECEIVED_FOCUSED_POST', channelId: 'gmchannelid', data: postId},
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
{type: 'MOCK_GET_CHANNEL_STATS', args: ['gmchannelid']},
]);
@ -275,23 +300,26 @@ describe('components/PermalinkView', () => {
});
test('should redirect to channel link with postId for permalink', async () => {
const testStore = await mockStore(initialState);
await testStore.dispatch(focusPost('postid1', undefined, baseProps.currentUserId));
const postId = 'postid1';
nockInfoForPost(postId);
expect(getPostThread).toHaveBeenCalledWith('postid1');
const testStore = await mockStore(initialState);
await testStore.dispatch(focusPost(postId, undefined, baseProps.currentUserId));
expect(getPostThread).toHaveBeenCalledWith(postId);
expect(testStore.getActions()).toEqual([
{
type: 'MOCK_GET_POST_THREAD',
data: {
posts: {
replypostid1: {id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
postid1: {id: 'postid1', message: 'some message', channel_id: 'channelid1'},
replypostid1: {id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: postId},
postid1: {id: postId, message: 'some message', channel_id: 'channelid1'},
},
order: ['postid1', 'replypostid1'],
order: [postId, 'replypostid1'],
},
},
{type: 'MOCK_SELECT_CHANNEL', args: ['channelid1']},
{type: 'RECEIVED_FOCUSED_POST', channelId: 'channelid1', data: 'postid1'},
{type: 'RECEIVED_FOCUSED_POST', channelId: 'channelid1', data: postId},
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
{type: 'MOCK_GET_CHANNEL_STATS', args: ['channelid1']},
]);
@ -299,6 +327,9 @@ describe('components/PermalinkView', () => {
});
test('should not redirect to channel link with postId for a reply permalink when collapsedThreads enabled and option is set true', async () => {
const postId = 'replypostid1';
nockInfoForPost(postId);
const newState = {
entities: {
...initialState.entities,
@ -313,34 +344,34 @@ describe('components/PermalinkView', () => {
jest.spyOn<typeof Channels, keyof typeof Channels>(Channels, 'getCurrentChannel').mockReturnValue({id: 'channelid1', name: 'channel1', type: 'O', team_id: 'current_team_id'});
const testStore = await mockStore(newState);
await testStore.dispatch(focusPost('replypostid1', '#', initialState.entities.users.currentUserId, {skipRedirectReplyPermalink: true}));
await testStore.dispatch(focusPost(postId, '#', initialState.entities.users.currentUserId, {skipRedirectReplyPermalink: true}));
expect(getPostThread).toHaveBeenCalledWith('replypostid1');
expect(getPostThread).toHaveBeenCalledWith(postId);
expect(testStore.getActions()).toEqual([
{
type: 'MOCK_GET_POST_THREAD',
data: {
posts: {
replypostid1: {id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
replypostid1: {id: postId, message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
postid1: {id: 'postid1', message: 'some message', channel_id: 'channelid1'},
},
order: ['postid1', 'replypostid1'],
order: ['postid1', postId],
},
},
{
type: 'MOCK_GET_POST_THREAD',
data: {
posts: {
replypostid1: {id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
replypostid1: {id: postId, message: 'some message', channel_id: 'channelid1', root_id: 'postid1'},
postid1: {id: 'postid1', message: 'some message', channel_id: 'channelid1'},
},
order: ['postid1', 'replypostid1'],
order: ['postid1', postId],
},
},
{type: 'MOCK_SELECT_POST_AND_HIGHLIGHT', args: [{id: 'replypostid1', message: 'some message', channel_id: 'channelid1', root_id: 'postid1'}]},
{type: 'MOCK_SELECT_POST_AND_HIGHLIGHT', args: [{id: postId, message: 'some message', channel_id: 'channelid1', root_id: 'postid1'}]},
{type: 'MOCK_LOAD_CHANNELS_FOR_CURRENT_USER'},
{type: 'MOCK_GET_CHANNEL_STATS', args: ['channelid1']},
]);

View File

@ -206,6 +206,8 @@ function TeamController(props: Props) {
return null;
}
const teamLoaded = team?.name.toLowerCase() === teamNameParam?.toLowerCase();
return (
<Switch>
<Route
@ -229,7 +231,7 @@ function TeamController(props: Props) {
)}
/>
))}
<ChannelController shouldRenderCenterChannel={initialChannelsLoaded}/>
<ChannelController shouldRenderCenterChannel={initialChannelsLoaded && teamLoaded}/>
</Switch>
);
}

View File

@ -71,14 +71,14 @@ type JoinPrivateChannelPromptResult = {
};
};
export function joinPrivateChannelPrompt(team: Team, channel: Channel, handleOnCancel = true): ActionFunc {
export function joinPrivateChannelPrompt(team: Team, channelDisplayName: string, handleOnCancel = true): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const result: JoinPrivateChannelPromptResult = await new Promise((resolve) => {
const modalData = {
modalId: ModalIdentifiers.JOIN_CHANNEL_PROMPT,
dialogType: JoinPrivateChannelModal,
dialogProps: {
channelName: channel.display_name,
channelName: channelDisplayName,
onJoin: () => {
LocalStorageStore.setTeamIdJoinedOnLoad(null);
resolve({

View File

@ -106,7 +106,7 @@ import type {
MarketplaceApp,
MarketplacePlugin,
} from '@mattermost/types/marketplace';
import {Post, PostList, PostSearchResults, OpenGraphMetadata, PostsUsageResponse, TeamsUsageResponse, PaginatedPostList, FilesUsageResponse, PostAcknowledgement, PostAnalytics} from '@mattermost/types/posts';
import {Post, PostList, PostSearchResults, PostsUsageResponse, TeamsUsageResponse, PaginatedPostList, FilesUsageResponse, PostAcknowledgement, PostAnalytics, PostInfo} from '@mattermost/types/posts';
import {Draft} from '@mattermost/types/drafts';
import {Reaction} from '@mattermost/types/reactions';
import {Role} from '@mattermost/types/roles';
@ -2160,6 +2160,13 @@ export default class Client4 {
);
};
getPostInfo = (postId: string) => {
return this.doFetch<PostInfo>(
`${this.getPostRoute(postId)}/info`,
{method: 'get'},
)
}
getPostsByIds = (postIds: string[]) => {
return this.doFetch<Post[]>(
`${this.getPostsRoute()}/ids`,

View File

@ -5,6 +5,7 @@ import {Channel, ChannelType} from './channels';
import {CustomEmoji} from './emojis';
import {FileInfo} from './files';
import {Reaction} from './reactions';
import { TeamType } from './teams';
import {UserProfile} from './users';
import {
RelationOneToOne,
@ -208,4 +209,15 @@ export type ActivityEntry = {
actorId: string[];
userIds: string[];
usernames: string[];
}
export type PostInfo = {
channel_id: string;
channel_type: ChannelType;
channel_display_name: string;
has_joined_channel: boolean;
team_id: string;
team_type: TeamType;
team_display_name: string;
has_joined_team: boolean;
}