From 68aa01692191679de281e26df862d2cd3e26d9ec Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Thu, 28 Nov 2024 07:52:28 +0100 Subject: [PATCH] Fix MM-61710 (#8368) (#8382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit f911a29e4c9aac1800b8eacea6b97e40421c3fdc) Co-authored-by: Daniel Espino GarcĂ­a --- app/components/autocomplete_selector/index.tsx | 3 ++- app/components/formatted_text/index.tsx | 4 +--- .../markdown/channel_mention/channel_mention.tsx | 3 ++- app/components/markdown/markdown_image/index.tsx | 3 ++- .../markdown/markdown_table_image/index.tsx | 3 ++- .../combined_user_activity.tsx | 12 +++++++----- .../combined_user_activity/last_users.tsx | 15 ++++++++++----- .../body/content/image_preview/image_preview.tsx | 3 ++- .../message_attachments/action_button/index.tsx | 3 ++- .../message_attachments/message_attachment.tsx | 3 ++- .../content/opengraph/opengraph_image/index.tsx | 6 ++---- .../post/system_message/system_message.tsx | 3 ++- app/components/selected_users/index.tsx | 6 +++--- app/database/components/index.tsx | 3 ++- app/screens/apps_form/apps_form_component.tsx | 11 ++++++++--- .../header/skintone_selector/skin_selector.tsx | 3 +-- .../skintone_selector/skintone_selector.tsx | 3 +-- app/screens/in_app_notification/index.tsx | 3 ++- .../integration_selector/integration_selector.tsx | 12 ++++++++---- app/screens/interactive_dialog/index.tsx | 11 ++++++++--- app/screens/invite/invite.tsx | 8 +++++--- app/screens/invite/summary_report.tsx | 3 ++- app/screens/permalink/permalink.tsx | 3 ++- app/utils/types.ts | 6 ++++++ 24 files changed, 84 insertions(+), 49 deletions(-) create mode 100644 app/utils/types.ts diff --git a/app/components/autocomplete_selector/index.tsx b/app/components/autocomplete_selector/index.tsx index 64f0842bb..9412a3436 100644 --- a/app/components/autocomplete_selector/index.tsx +++ b/app/components/autocomplete_selector/index.tsx @@ -19,6 +19,7 @@ import {getUserById, observeTeammateNameDisplay} from '@queries/servers/user'; import {goToScreen} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import {displayUsername} from '@utils/user'; import type {WithDatabaseArgs} from '@typings/database/database'; @@ -93,7 +94,7 @@ async function getItemName(serverUrl: string, selected: string, teammateNameDisp return ''; } - const database = DatabaseManager.serverDatabases[serverUrl]?.database; + const database = secureGetFromRecord(DatabaseManager.serverDatabases, serverUrl)?.database; switch (dataSource) { case ViewConstants.DATA_SOURCE_USERS: { diff --git a/app/components/formatted_text/index.tsx b/app/components/formatted_text/index.tsx index fd8f335aa..a9b591ace 100644 --- a/app/components/formatted_text/index.tsx +++ b/app/components/formatted_text/index.tsx @@ -46,9 +46,7 @@ const FormattedText = (props: FormattedTextProps) => { // when the `message` is formatted. This allows the formatted // message to then be broken-up into parts with references to the // React Elements inserted back in. - Object.keys(values).forEach((name) => { - const value = values[name]; - + Object.entries(values).forEach(([name, value]) => { if (isValidElement(value)) { const token = generateToken(); tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter; diff --git a/app/components/markdown/channel_mention/channel_mention.tsx b/app/components/markdown/channel_mention/channel_mention.tsx index 1377e891d..5dceb752f 100644 --- a/app/components/markdown/channel_mention/channel_mention.tsx +++ b/app/components/markdown/channel_mention/channel_mention.tsx @@ -10,6 +10,7 @@ import {useServerUrl} from '@context/server'; import {t} from '@i18n'; import {alertErrorWithFallback} from '@utils/draft'; import {preventDoubleTap} from '@utils/tap'; +import {secureGetFromRecord} from '@utils/types'; import type ChannelModel from '@typings/database/models/servers/channel'; import type TeamModel from '@typings/database/models/servers/team'; @@ -40,7 +41,7 @@ function getChannelFromChannelName(name: string, channels: ChannelModel[], chann }); while (channelName.length > 0) { - if (channelsByName[channelName]) { + if (secureGetFromRecord(channelsByName, channelName)) { return channelsByName[channelName]; } diff --git a/app/components/markdown/markdown_image/index.tsx b/app/components/markdown/markdown_image/index.tsx index db4a67992..23bead8ca 100644 --- a/app/components/markdown/markdown_image/index.tsx +++ b/app/components/markdown/markdown_image/index.tsx @@ -29,6 +29,7 @@ import {bottomSheetSnapPoint} from '@utils/helpers'; import {calculateDimensions, getViewPortWidth, isGifTooLarge} from '@utils/images'; import {getMarkdownImageSize} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import {normalizeProtocol, tryOpenURL} from '@utils/url'; import type {GalleryItemType} from '@typings/screens/gallery'; @@ -80,7 +81,7 @@ const MarkdownImage = ({ const style = getStyleSheet(theme); const managedConfig = useManagedConfig(); const genericFileId = useRef(generateId('uid')).current; - const metadata = imagesMetadata?.[source] || Object.values(imagesMetadata || {})[0]; + const metadata = secureGetFromRecord(imagesMetadata, source) || Object.values(imagesMetadata || {})[0]; const [failed, setFailed] = useState(isGifTooLarge(metadata)); const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth, layoutHeight); const serverUrl = useServerUrl(); diff --git a/app/components/markdown/markdown_table_image/index.tsx b/app/components/markdown/markdown_table_image/index.tsx index 042887bef..a285b141f 100644 --- a/app/components/markdown/markdown_table_image/index.tsx +++ b/app/components/markdown/markdown_table_image/index.tsx @@ -13,6 +13,7 @@ import {useGalleryItem} from '@hooks/gallery'; import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery'; import {generateId} from '@utils/general'; import {calculateDimensions, isGifTooLarge} from '@utils/images'; +import {secureGetFromRecord} from '@utils/types'; import type {GalleryItemType} from '@typings/screens/gallery'; @@ -33,7 +34,7 @@ const style = StyleSheet.create({ }); const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, source}: MarkdownTableImageProps) => { - const metadata = imagesMetadata[source]; + const metadata = secureGetFromRecord(imagesMetadata, source); const fileId = useRef(generateId('uid')).current; const [failed, setFailed] = useState(isGifTooLarge(metadata)); const currentServerUrl = useServerUrl(); diff --git a/app/components/post_list/combined_user_activity/combined_user_activity.tsx b/app/components/post_list/combined_user_activity/combined_user_activity.tsx index 845688c04..eafea6dd2 100644 --- a/app/components/post_list/combined_user_activity/combined_user_activity.tsx +++ b/app/components/post_list/combined_user_activity/combined_user_activity.tsx @@ -16,6 +16,7 @@ import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@ import {emptyFunction} from '@utils/general'; import {getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import LastUsers from './last_users'; import {postTypeMessages} from './messages'; @@ -76,7 +77,7 @@ const CombinedUserActivity = ({ const usernamesValues = Object.values(usernamesById); const usernames = userIds.reduce((acc: string[], id: string) => { if (id !== currentUserId && id !== currentUsername) { - const name = usernamesById[id] ?? usernamesValues.find((n) => n === id); + const name = secureGetFromRecord(usernamesById, id) ?? usernamesValues.find((n) => n === id); acc.push(name ? `@${name}` : someone); } return acc; @@ -112,7 +113,7 @@ const CombinedUserActivity = ({ return null; } let actor = ''; - if (usernamesById[actorId]) { + if (secureGetFromRecord(usernamesById, actorId)) { actor = `@${usernamesById[actorId]}`; } @@ -141,11 +142,11 @@ const CombinedUserActivity = ({ const secondUser = usernames[1]; let localeHolder; if (numOthers === 0) { - localeHolder = postTypeMessages[postType].one; + localeHolder = secureGetFromRecord(postTypeMessages, postType)?.one; if ( (userIds[0] === currentUserId || userIds[0] === currentUsername) && - postTypeMessages[postType].one_you + secureGetFromRecord(postTypeMessages, postType)?.one_you ) { localeHolder = postTypeMessages[postType].one_you; } @@ -153,7 +154,8 @@ const CombinedUserActivity = ({ localeHolder = postTypeMessages[postType].two; } - const formattedMessage = intl.formatMessage(localeHolder, {firstUser, secondUser, actor}); + // We default to empty string, but this should never happen + const formattedMessage = localeHolder ? intl.formatMessage(localeHolder, {firstUser, secondUser, actor}) : ''; return ( { diff --git a/app/components/post_list/post/body/content/message_attachments/action_button/index.tsx b/app/components/post_list/post/body/content/message_attachments/action_button/index.tsx index 4778aea3e..328c85a50 100644 --- a/app/components/post_list/post/body/content/message_attachments/action_button/index.tsx +++ b/app/components/post_list/post/body/content/message_attachments/action_button/index.tsx @@ -9,6 +9,7 @@ import {useServerUrl} from '@context/server'; import {getStatusColors} from '@utils/message_attachment_colors'; import {preventDoubleTap} from '@utils/tap'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import ActionButtonText from './action_button_text'; @@ -65,7 +66,7 @@ const ActionButton = ({buttonColor, cookie, disabled, id, name, postId, theme}: if (buttonColor) { const STATUS_COLORS = getStatusColors(theme); - const hexColor = STATUS_COLORS[buttonColor] || theme[buttonColor] || buttonColor; + const hexColor = secureGetFromRecord(STATUS_COLORS, buttonColor) || secureGetFromRecord(theme, buttonColor) || buttonColor; customButtonStyle = {borderColor: changeOpacity(hexColor, 0.25), backgroundColor: '#ffffff'}; customButtonTextStyle = {color: hexColor}; } diff --git a/app/components/post_list/post/body/content/message_attachments/message_attachment.tsx b/app/components/post_list/post/body/content/message_attachments/message_attachment.tsx index ed557d6df..97f1afd27 100644 --- a/app/components/post_list/post/body/content/message_attachments/message_attachment.tsx +++ b/app/components/post_list/post/body/content/message_attachments/message_attachment.tsx @@ -7,6 +7,7 @@ import {View} from 'react-native'; import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {getStatusColors} from '@utils/message_attachment_colors'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import {isValidUrl} from '@utils/url'; import AttachmentActions from './attachment_actions'; @@ -62,7 +63,7 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l if (attachment.color) { if (attachment.color[0] === '#') { borderStyle = {borderLeftColor: attachment.color}; - } else if (STATUS_COLORS[attachment.color]) { + } else if (secureGetFromRecord(STATUS_COLORS, attachment.color)) { borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]}; } } diff --git a/app/components/post_list/post/body/content/opengraph/opengraph_image/index.tsx b/app/components/post_list/post/body/content/opengraph/opengraph_image/index.tsx index c6ee80a68..e86153877 100644 --- a/app/components/post_list/post/body/content/opengraph/opengraph_image/index.tsx +++ b/app/components/post_list/post/body/content/opengraph/opengraph_image/index.tsx @@ -16,6 +16,7 @@ import {isTablet} from '@utils/helpers'; import {calculateDimensions} from '@utils/images'; import {type BestImage, getNearestPoint} from '@utils/opengraph'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import {extractFilenameFromUrl, isValidUrl} from '@utils/url'; import type {GalleryItemType} from '@typings/screens/gallery'; @@ -71,10 +72,7 @@ const OpengraphImage = ({isReplyPost, layoutWidth, location, metadata, openGraph const imageUrl = (bestImage.secure_url || bestImage.url)!; const imagesMetadata = metadata?.images; - let ogImage; - if (imagesMetadata && imagesMetadata[imageUrl]) { - ogImage = imagesMetadata[imageUrl]; - } + let ogImage = secureGetFromRecord(imagesMetadata, imageUrl); if (!ogImage) { ogImage = openGraphImages.find((i: BestImage) => i.url === imageUrl || i.secure_url === imageUrl); diff --git a/app/components/post_list/post/system_message/system_message.tsx b/app/components/post_list/post/system_message/system_message.tsx index 479253c39..f7c885404 100644 --- a/app/components/post_list/post/system_message/system_message.tsx +++ b/app/components/post_list/post/system_message/system_message.tsx @@ -12,6 +12,7 @@ import {useTheme} from '@context/theme'; import {t} from '@i18n'; import {getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import {typography} from '@utils/typography'; import type PostModel from '@typings/database/models/servers/post'; @@ -276,7 +277,7 @@ export const SystemMessage = ({post, location, author, hideGuestTags}: SystemMes return renderAddGuestToChannelMessage({post, author, location, styles, intl, theme}, hideGuestTags); } - const renderer = systemMessageRenderers[post.type]; + const renderer = secureGetFromRecord(systemMessageRenderers, post.type); if (!renderer) { return ( { const u = []; - for (const id of Object.keys(selectedIds)) { - if (!selectedIds[id]) { + for (const [id, user] of Object.entries(selectedIds)) { + if (!user) { continue; } u.push( (Component: if (server) { const database = - DatabaseManager.serverDatabases[server.url]?.database; + secureGetFromRecord(DatabaseManager.serverDatabases, server.url)?.database; if (database) { setState({ diff --git a/app/screens/apps_form/apps_form_component.tsx b/app/screens/apps_form/apps_form_component.tsx index 5ca04c7e0..bf438c3c9 100644 --- a/app/screens/apps_form/apps_form_component.tsx +++ b/app/screens/apps_form/apps_form_component.tsx @@ -20,6 +20,7 @@ import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations'; import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import DialogIntroductionText from '../interactive_dialog/dialog_introduction_text'; import {buildNavigationButton, dismissModal, setButtons} from '../navigation'; @@ -263,7 +264,7 @@ function AppsFormComponent({ elements?.forEach((element) => { const newError = checkDialogElementForError( element, - element.name === form.submit_buttons ? button : values[element.name], + element.name === form.submit_buttons ? button : secureGetFromRecord(values, element.name), ); if (newError) { hasErrors = true; @@ -408,13 +409,17 @@ function AppsFormComponent({ /> } {form.fields && form.fields.filter((f) => f.name !== form.submit_buttons).map((field) => { + const value = secureGetFromRecord(values, field.name); + if (!value) { + return null; + } return ( diff --git a/app/screens/emoji_picker/picker/header/skintone_selector/skin_selector.tsx b/app/screens/emoji_picker/picker/header/skintone_selector/skin_selector.tsx index 41bf06fd4..2ef00eb9f 100644 --- a/app/screens/emoji_picker/picker/header/skintone_selector/skin_selector.tsx +++ b/app/screens/emoji_picker/picker/header/skintone_selector/skin_selector.tsx @@ -68,8 +68,7 @@ const SkinSelector = ({onSelectSkin, selected, skins}: Props) => { /> - {Object.keys(skins).map((key) => { - const name = skins[key]; + {Object.entries(skins).map(([key, name]) => { return ( >((result, value) => { - const skin = skinCodes[value]; +const skins = Object.entries(skinCodes).reduce>((result, [value, skin]) => { if (value === 'default') { result[value] = 'hand'; } else { diff --git a/app/screens/in_app_notification/index.tsx b/app/screens/in_app_notification/index.tsx index 787f0190c..54bb67d65 100644 --- a/app/screens/in_app_notification/index.tsx +++ b/app/screens/in_app_notification/index.tsx @@ -15,6 +15,7 @@ import {useIsTablet} from '@hooks/device'; import {dismissOverlay} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import Icon from './icon'; import Server from './server'; @@ -148,7 +149,7 @@ const InAppNotification = ({componentId, serverName, serverUrl, notification}: I // eslint-disable-next-line new-cap const gesture = Gesture.Pan().activeOffsetY(-20).onStart(() => runOnJS(animateDismissOverlay)()); - const database = DatabaseManager.serverDatabases[serverUrl]?.database; + const database = secureGetFromRecord(DatabaseManager.serverDatabases, serverUrl)?.database; return ( diff --git a/app/screens/integration_selector/integration_selector.tsx b/app/screens/integration_selector/integration_selector.tsx index 434bf0d89..8cc324f2c 100644 --- a/app/screens/integration_selector/integration_selector.tsx +++ b/app/screens/integration_selector/integration_selector.tsx @@ -24,6 +24,7 @@ import { } from '@screens/navigation'; import {filterChannelsMatchingTerm} from '@utils/channel'; import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import {typography} from '@utils/typography'; import ChannelListRow from './channel_list_row'; @@ -68,7 +69,8 @@ const extractItemKey = (dataSource: string, item: DataType): string => { const toggleFromMap = (current: MultiselectSelectedMap, key: string, item: T): MultiselectSelectedMap => { const newMap = {...current}; - if (current[key]) { + const hasValue = Boolean(secureGetFromRecord(current, key)); + if (hasValue) { delete newMap[key]; } else { newMap[key] = item; @@ -96,7 +98,7 @@ const filterSearchData = (source: string, searchData: DataTypeList, searchTerm: const handleIdSelection = (dataSource: string, currentIds: {[id: string]: DataType}, item: DataType) => { const newSelectedIds = {...currentIds}; const key = extractItemKey(dataSource, item); - const wasSelected = currentIds[key]; + const wasSelected = secureGetFromRecord(currentIds, key); if (wasSelected) { Reflect.deleteProperty(newSelectedIds, key); @@ -239,7 +241,9 @@ function IntegrationSelector( } else { setMultiselectSelected((current) => { const multiselectSelectedItems = {...current}; - delete multiselectSelectedItems[itemKey]; + if (secureGetFromRecord(multiselectSelectedItems, itemKey) !== undefined) { + delete multiselectSelectedItems[itemKey]; + } return multiselectSelectedItems; }); } @@ -470,7 +474,7 @@ function IntegrationSelector( }, [multiselectSelected, theme, isMultiselect]); const renderOptionItem = useCallback((itemProps: any) => { - const itemSelected = Boolean(multiselectSelected[itemProps.item.value]); + const itemSelected = Boolean(secureGetFromRecord(multiselectSelected, itemProps.item.value)); return ( { - const newError = checkDialogElementForError(elem, values[elem.name]); + const newError = checkDialogElementForError(elem, secureGetFromRecord(values, elem.name)); if (newError) { newErrors[elem.name] = intl.formatMessage({id: newError.id, defaultMessage: newError.defaultMessage}, newError.values); hasErrors = true; @@ -244,6 +245,10 @@ function InteractiveDialog({ /> } {Boolean(elements) && elements.map((e) => { + const value = secureGetFromRecord(values, e.name); + if (value === undefined) { + return null; + } return ( ); diff --git a/app/screens/invite/invite.tsx b/app/screens/invite/invite.tsx index fd81ccea4..de6f1ea0a 100644 --- a/app/screens/invite/invite.tsx +++ b/app/screens/invite/invite.tsx @@ -19,6 +19,7 @@ import {dismissModal, setButtons} from '@screens/navigation'; import {isEmail} from '@utils/helpers'; import {mergeNavigationOptions} from '@utils/navigation'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import {isGuest} from '@utils/user'; import Selection from './selection'; @@ -183,7 +184,7 @@ export default function Invite({ const id = email ? item : (item as UserProfile).id; const newSelectedIds = Object.assign({}, selectedIds); - if (!selectedIds[id]) { + if (!secureGetFromRecord(selectedIds, id)) { newSelectedIds[id] = item; } @@ -301,8 +302,9 @@ export default function Invite({ } for (const email of emails) { - if (membersWithError[email]) { - notSent.push({userId: email, reason: membersWithError[email]}); + const error = secureGetFromRecord(membersWithError, email); + if (error) { + notSent.push({userId: email, reason: error}); } else { sent.push({userId: email, reason: formatMessage({id: 'invite.summary.email_invite', defaultMessage: 'An invitation email has been sent'})}); } diff --git a/app/screens/invite/summary_report.tsx b/app/screens/invite/summary_report.tsx index 8819ca6b2..35ec7f41b 100644 --- a/app/screens/invite/summary_report.tsx +++ b/app/screens/invite/summary_report.tsx @@ -9,6 +9,7 @@ import CompassIcon from '@components/compass_icon'; import UserItem from '@components/user_item'; import {useTheme} from '@context/theme'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; +import {secureGetFromRecord} from '@utils/types'; import {typography} from '@utils/typography'; import TextItem, {TextItemType} from './text_item'; @@ -114,7 +115,7 @@ export default function SummaryReport({ {invites.map(({userId, reason}) => { - const item = selectedIds[userId]; + const item = secureGetFromRecord(selectedIds, userId); return ( (v: Record | undefined, key: string) { + return typeof v === 'object' && v && Object.prototype.hasOwnProperty.call(v, key) ? v[key] : undefined; +}