Ensure full validation of Calls post props (#8279)

This commit is contained in:
Claudio Costa 2024-10-24 15:10:49 -07:00 committed by GitHub
parent e4dc8dbbfe
commit 751bb2dfb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 135 additions and 8 deletions

View file

@ -9,6 +9,7 @@ import {Text, TouchableOpacity, View} from 'react-native';
import {leaveCallConfirmation} from '@calls/actions/calls';
import {leaveAndJoinWithAlert, showLimitRestrictedAlert} from '@calls/alerts';
import {setJoiningChannelId} from '@calls/state';
import {getCallPropsFromPost} from '@calls/utils';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
import FormattedText from '@components/formatted_text';
@ -164,13 +165,15 @@ export const CallsCustomMessage = ({
leaveCallConfirmation(intl, otherParticipants, isAdmin, isHost, serverUrl, post.channelId);
}, [intl, otherParticipants, isAdmin, isHost, serverUrl, post.channelId]);
const title = post.props.title ? (
const callProps = getCallPropsFromPost(post);
const title = callProps.title ? (
<Text style={style.title}>
{post.props.title}
{callProps.title}
</Text>
) : null;
if (post.props.end_at) {
if (callProps.start_at > 0 && callProps.end_at > 0) {
return (
<>
{title}
@ -195,7 +198,7 @@ export const CallsCustomMessage = ({
<Text>{' '}</Text>
<FormattedTime
style={style.timeText}
value={post.props.end_at}
value={callProps.end_at}
isMilitaryTime={isMilitaryTime}
timezone={timezone}
/>
@ -204,7 +207,7 @@ export const CallsCustomMessage = ({
id={'mobile.calls_lasted'}
style={style.timeText}
defaultMessage={'Lasted {duration}'}
values={{duration: moment.duration(post.props.end_at - post.props.start_at).humanize(false)}}
values={{duration: moment.duration(callProps.end_at - callProps.start_at).humanize(false)}}
/>
</View>
</View>
@ -273,7 +276,7 @@ export const CallsCustomMessage = ({
style={style.text}
/>
<FormattedRelativeTime
value={post.props.start_at}
value={callProps.start_at}
updateIntervalInSeconds={1}
style={style.timeText}
/>

View file

@ -6,7 +6,7 @@ import assert from 'assert';
import {type CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
import {License} from '@constants';
import {getICEServersConfigs} from './utils';
import {getICEServersConfigs, getCallPropsFromPost} from './utils';
describe('getICEServersConfigs', () => {
it('backwards compatible case, no ICEServersConfigs present', () => {
@ -97,3 +97,109 @@ describe('getICEServersConfigs', () => {
]);
});
});
describe('getCallPropsFromPost', () => {
test('undefined props', () => {
const post = {} as Post;
const props = getCallPropsFromPost(post);
expect(props.title).toBe('');
expect(props.start_at).toBe(0);
expect(props.end_at).toBe(0);
expect(props.recordings).toStrictEqual({});
expect(props.recording_files.length).toBe(0);
expect(props.transcriptions).toStrictEqual({});
expect(props.participants.length).toBe(0);
});
test('missing props', () => {
const post = {
props: {},
} as Post;
const props = getCallPropsFromPost(post);
expect(props.title).toBe('');
expect(props.start_at).toBe(0);
expect(props.end_at).toBe(0);
expect(props.recordings).toStrictEqual({});
expect(props.recording_files.length).toBe(0);
expect(props.transcriptions).toStrictEqual({});
expect(props.participants.length).toBe(0);
});
test('invalid props', () => {
const callProps = {
title: {},
start_at: 'invalid',
end_at: [],
recordings: null,
transcriptions: 45,
participants: 'invalid',
recording_files: 45,
};
const post = {
props: callProps as unknown,
} as Post;
const props = getCallPropsFromPost(post);
expect(props.title).toBe('');
expect(props.start_at).toBe(0);
expect(props.end_at).toBe(0);
expect(props.recordings).toStrictEqual({});
expect(props.recording_files.length).toBe(0);
expect(props.transcriptions).toStrictEqual({});
expect(props.participants.length).toBe(0);
});
test('full props', () => {
const callProps = {
title: 'call title',
start_at: 1000,
end_at: 1045,
recordings: {
recA: {
file_id: 'recAFileID',
post_id: 'recAPostID',
tr_id: 'trA',
},
recB: {
file_id: 'recBFileID',
post_id: 'recBPostID',
tr_id: 'trB',
},
},
recording_files: ['recAFileID', 'recBFileID'],
transcriptions: {
trA: {
file_id: 'trAFileID',
post_id: 'trAPostID',
rec_id: 'recA',
},
trB: {
file_id: 'trBFileID',
post_id: 'trBPostID',
rec_id: 'recB',
},
},
participants: ['userA', 'userB'],
};
const post = {
props: callProps as unknown,
} as Post;
const props = getCallPropsFromPost(post);
expect(props.title).toBe(post.props.title);
expect(props.start_at).toBe(post.props.start_at);
expect(props.end_at).toBe(post.props.end_at);
expect(props.recordings).toBe(post.props.recordings);
expect(props.recording_files).toBe(post.props.recording_files);
expect(props.transcriptions).toBe(post.props.transcriptions);
expect(props.participants).toBe(post.props.participants);
});
});

View file

@ -17,7 +17,7 @@ import type {
CallsTheme,
CallsVersion,
} from '@calls/types/calls';
import type {CallsConfig, Caption} from '@mattermost/calls/lib/types';
import type {CallsConfig, Caption, CallPostProps} from '@mattermost/calls/lib/types';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {IntlShape} from 'react-intl';
@ -246,3 +246,21 @@ export const getTranscriptionUri = (serverUrl: string, postProps?: Record<string
selected: {type: SelectedTrackType.INDEX, value: 0},
};
};
function isValidObject(v: any) {
return typeof v === 'object' && !Array.isArray(v) && v !== null;
}
export function getCallPropsFromPost(post: PostModel | Post): CallPostProps {
return {
title: typeof post.props?.title === 'string' ? post.props.title : '',
start_at: typeof post.props?.start_at === 'number' ? post.props.start_at : 0,
end_at: typeof post.props?.end_at === 'number' ? post.props.end_at : 0,
recordings: isValidObject(post.props?.recordings) ? post.props.recordings : {},
transcriptions: isValidObject(post.props?.transcriptions) ? post.props.transcriptions : {},
participants: Array.isArray(post.props?.participants) ? post.props.participants : [],
// DEPRECATED
recording_files: Array.isArray(post.props?.recording_files) ? post.props.recording_files : [],
};
}