From 452929ad35152df1441c25af023fea07d4e354f1 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Fri, 29 Aug 2025 08:57:50 +0200 Subject: [PATCH] File: Show warning before downloading HTML (#6145) --- src/components/common/Document.tsx | 32 ++++++------- src/components/left/search/FileResults.tsx | 4 +- .../search/helpers/createMapStateToProps.ts | 6 +-- .../middle/MessageSelectToolbar.tsx | 45 ++++++++++--------- src/components/middle/message/Message.tsx | 12 ++--- src/components/middle/message/WebPage.tsx | 6 +-- src/components/right/Profile.tsx | 10 ++--- src/global/cache.ts | 7 ++- src/global/initialState.ts | 2 +- src/global/selectors/messages.ts | 7 +-- src/global/types/sharedState.ts | 2 +- src/util/media/ipRevealingMedia.ts | 19 ++++++++ 12 files changed, 89 insertions(+), 63 deletions(-) create mode 100644 src/util/media/ipRevealingMedia.ts diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index 7c91102cf..b9f0d2c2b 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -6,7 +6,6 @@ import { getActions } from '../../global'; import type { ApiDocument, ApiMessage } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; -import { SVG_EXTENSIONS } from '../../config'; import { getDocumentMediaHash, getMediaFormat, @@ -14,6 +13,7 @@ import { getMediaTransferState, isDocumentVideo, } from '../../global/helpers'; +import { isIpRevealingMedia } from '../../util/media/ipRevealingMedia'; import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo'; import useFlag from '../../hooks/useFlag'; @@ -41,7 +41,7 @@ type OwnProps = { sender?: string; autoLoadFileMaxSizeMb?: number; isDownloading?: boolean; - shouldWarnAboutSvg?: boolean; + shouldWarnAboutFiles?: boolean; onCancelUpload?: () => void; onMediaClick?: () => void; } & ({ @@ -67,7 +67,7 @@ const Document = ({ sender, isSelected, isSelectable, - shouldWarnAboutSvg, + shouldWarnAboutFiles, isDownloading, message, onCancelUpload, @@ -79,10 +79,10 @@ const Document = ({ const ref = useRef(); const lang = useOldLang(); - const [isSvgDialogOpen, openSvgDialog, closeSvgDialog] = useFlag(); - const [shouldNotWarnAboutSvg, setShouldNotWarnAboutSvg] = useState(false); + const [isFileIpDialogOpen, openFileIpDialog, closeFileIpDialog] = useFlag(); + const [shouldNotWarnAboutFiles, setShouldNotWarnAboutFiles] = useState(false); - const { fileName, size, timestamp } = document; + const { fileName, size, timestamp, mimeType } = document; const extension = getDocumentExtension(document) || ''; const isIntersecting = useIsIntersecting(ref, observeIntersection); @@ -150,17 +150,17 @@ const Document = ({ return; } - if (SVG_EXTENSIONS.has(extension) && shouldWarnAboutSvg) { - openSvgDialog(); + if (isIpRevealingMedia({ mimeType, extension }) && shouldWarnAboutFiles) { + openFileIpDialog(); return; } handleDownload(); }); - const handleSvgConfirm = useLastCallback(() => { - setSharedSettingOption({ shouldWarnAboutSvg: !shouldNotWarnAboutSvg }); - closeSvgDialog(); + const handleFileIpConfirm = useLastCallback(() => { + setSharedSettingOption({ shouldWarnAboutFiles: !shouldNotWarnAboutFiles }); + closeFileIpDialog(); handleDownload(); }); @@ -191,16 +191,16 @@ const Document = ({ onDateClick={onDateClick ? handleDateClick : undefined} /> {lang('lng_launch_svg_warning')} diff --git a/src/components/left/search/FileResults.tsx b/src/components/left/search/FileResults.tsx index 6dc10f0b3..6da5f9243 100644 --- a/src/components/left/search/FileResults.tsx +++ b/src/components/left/search/FileResults.tsx @@ -44,7 +44,7 @@ const FileResults: FC = ({ globalMessagesByChatId, foundIds, activeDownloads, - shouldWarnAboutSvg, + shouldWarnAboutFiles, }) => { const { searchMessagesGlobal, @@ -117,7 +117,7 @@ const FileResults: FC = ({ sender={getSenderName(lang, message, chatsById, usersById)} className="scroll-item" isDownloading={getIsDownloading(activeDownloads, message.content.document!)} - shouldWarnAboutSvg={shouldWarnAboutSvg} + shouldWarnAboutFiles={shouldWarnAboutFiles} observeIntersection={observeIntersectionForMedia} onDateClick={handleMessageFocus} /> diff --git a/src/components/left/search/helpers/createMapStateToProps.ts b/src/components/left/search/helpers/createMapStateToProps.ts index 84e022967..d52bdbff3 100644 --- a/src/components/left/search/helpers/createMapStateToProps.ts +++ b/src/components/left/search/helpers/createMapStateToProps.ts @@ -18,7 +18,7 @@ export type StateProps = { searchChatId?: string; activeDownloads: TabState['activeDownloads']; isChatProtected?: boolean; - shouldWarnAboutSvg?: boolean; + shouldWarnAboutFiles?: boolean; }; export function createMapStateToProps(type: ApiGlobalMessageSearchType) { @@ -30,7 +30,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) { fetchingStatus, resultsByType, chatId, } = tabState.globalSearch; - const { shouldWarnAboutSvg } = selectSharedSettings(global); + const { shouldWarnAboutFiles } = selectSharedSettings(global); // One component is used for two different types of results. // The differences between them are only in the isVoice property. @@ -53,7 +53,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) { searchChatId: chatId, activeDownloads, isChatProtected: chatId ? selectChat(global, chatId)?.isProtected : undefined, - shouldWarnAboutSvg, + shouldWarnAboutFiles, }; }; } diff --git a/src/components/middle/MessageSelectToolbar.tsx b/src/components/middle/MessageSelectToolbar.tsx index 3fba82e27..23d4bcadf 100644 --- a/src/components/middle/MessageSelectToolbar.tsx +++ b/src/components/middle/MessageSelectToolbar.tsx @@ -11,8 +11,8 @@ import { selectCanDownloadSelectedMessages, selectCanForwardMessages, selectCanReportSelectedMessages, selectCurrentChat, - selectCurrentMessageList, selectHasProtectedMessage, - selectHasSvg, + selectCurrentMessageList, selectHasIpRevealingMedia, + selectHasProtectedMessage, selectSelectedMessagesCount, selectTabState, } from '../../global/selectors'; @@ -50,8 +50,8 @@ type StateProps = { hasProtectedMessage?: boolean; isAnyModalOpen?: boolean; selectedMessageIds?: number[]; - shouldWarnAboutSvg?: boolean; - hasSvgs?: boolean; + shouldWarnAboutFiles?: boolean; + hasIpRevealingMedia?: boolean; }; const MessageSelectToolbar: FC = ({ @@ -68,8 +68,8 @@ const MessageSelectToolbar: FC = ({ hasProtectedMessage, isAnyModalOpen, selectedMessageIds, - shouldWarnAboutSvg, - hasSvgs, + shouldWarnAboutFiles, + hasIpRevealingMedia, }) => { const { exitMessageSelectMode, @@ -85,8 +85,8 @@ const MessageSelectToolbar: FC = ({ useCopySelectedMessages(isActive); - const [isSvgDialogOpen, openSvgDialog, closeSvgDialog] = useFlag(); - const [shouldNotWarnAboutSvg, setShouldNotWarnAboutSvg] = useState(false); + const [isFileIpDialogOpen, openFileIpDialog, closeFileIpDialog] = useFlag(); + const [shouldNotWarnAboutFiles, setShouldNotWarnAboutFiles] = useState(false); const handleExitMessageSelectMode = useLastCallback(() => { exitMessageSelectMode(); @@ -128,17 +128,17 @@ const MessageSelectToolbar: FC = ({ }); const handleMessageDownload = useLastCallback(() => { - if (shouldWarnAboutSvg && hasSvgs) { - openSvgDialog(); + if (shouldWarnAboutFiles && hasIpRevealingMedia) { + openFileIpDialog(); return; } handleDownload(); }); - const handleSvgConfirm = useLastCallback(() => { - setSharedSettingOption({ shouldWarnAboutSvg: !shouldNotWarnAboutSvg }); - closeSvgDialog(); + const handleFileIpConfirm = useLastCallback(() => { + setSharedSettingOption({ shouldWarnAboutFiles: !shouldNotWarnAboutFiles }); + closeFileIpDialog(); handleDownload(); }); @@ -223,16 +223,16 @@ const MessageSelectToolbar: FC = ({ {lang('lng_launch_svg_warning')} @@ -242,7 +242,7 @@ const MessageSelectToolbar: FC = ({ export default memo(withGlobal( (global): StateProps => { const tabState = selectTabState(global); - const { shouldWarnAboutSvg } = selectSharedSettings(global); + const { shouldWarnAboutFiles } = selectSharedSettings(global); const chat = selectCurrentChat(global); const { type: messageListType, chatId } = selectCurrentMessageList(global) || {}; @@ -253,7 +253,8 @@ export default memo(withGlobal( const { messageIds: selectedMessageIds } = tabState.selectedMessages || {}; const hasProtectedMessage = chatId ? selectHasProtectedMessage(global, chatId, selectedMessageIds) : false; const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false; - const hasSvgs = selectedMessageIds && chatId ? selectHasSvg(global, chatId, selectedMessageIds) : false; + const hasIpRevealingMedia = selectedMessageIds && chatId + ? selectHasIpRevealingMedia(global, chatId, selectedMessageIds) : false; const isShareMessageModalOpen = tabState.isShareMessageModalShown; const isAnyModalOpen = Boolean(isShareMessageModalOpen || tabState.requestedDraft || tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal @@ -270,8 +271,8 @@ export default memo(withGlobal( selectedMessageIds, hasProtectedMessage, isAnyModalOpen, - shouldWarnAboutSvg, - hasSvgs, + shouldWarnAboutFiles, + hasIpRevealingMedia, }; }, )(MessageSelectToolbar)); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index cd4411291..b2f13df99 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -310,7 +310,7 @@ type StateProps = { webPageStory?: ApiTypeStory; isConnected: boolean; isLoadingComments?: boolean; - shouldWarnAboutSvg?: boolean; + shouldWarnAboutFiles?: boolean; senderBoosts?: number; tags?: Record; canTranscribeVoice?: boolean; @@ -438,7 +438,7 @@ const Message: FC = ({ webPageStory, isConnected, getIsMessageListReady, - shouldWarnAboutSvg, + shouldWarnAboutFiles, senderBoosts, tags, canTranscribeVoice, @@ -1261,7 +1261,7 @@ const Message: FC = ({ onMediaClick={handleDocumentClick} onCancelUpload={handleCancelUpload} isDownloading={isDownloading} - shouldWarnAboutSvg={shouldWarnAboutSvg} + shouldWarnAboutFiles={shouldWarnAboutFiles} /> )} {storyData && !isStoryMention && ( @@ -1430,7 +1430,7 @@ const Message: FC = ({ isConnected={isConnected} lastPlaybackTimestamp={lastPlaybackTimestamp} backgroundEmojiId={messageColorPeer?.color?.backgroundEmojiId} - shouldWarnAboutSvg={shouldWarnAboutSvg} + shouldWarnAboutFiles={shouldWarnAboutFiles} autoLoadFileMaxSizeMb={autoLoadFileMaxSizeMb} onAudioPlay={handleAudioPlay} onMediaClick={handleMediaClick} @@ -1884,7 +1884,7 @@ export default memo(withGlobal( const webPage = selectFullWebPageFromMessage(global, message); - const { shouldWarnAboutSvg } = selectSharedSettings(global); + const { shouldWarnAboutFiles } = selectSharedSettings(global); const isChatWithUser = isUserId(chatId); const chat = selectChat(global, chatId); @@ -2085,7 +2085,7 @@ export default memo(withGlobal( isLoadingComments: repliesThreadInfo?.isCommentsInfo && loadingThread?.loadingChatId === repliesThreadInfo?.originChannelId && loadingThread?.loadingMessageId === repliesThreadInfo?.originMessageId, - shouldWarnAboutSvg, + shouldWarnAboutFiles, ...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }), ...(typeof uploadProgress === 'number' && { uploadProgress }), ...(isFocused && { diff --git a/src/components/middle/message/WebPage.tsx b/src/components/middle/message/WebPage.tsx index d1623dcd5..f95bf0ebb 100644 --- a/src/components/middle/message/WebPage.tsx +++ b/src/components/middle/message/WebPage.tsx @@ -55,7 +55,7 @@ type OwnProps = { backgroundEmojiId?: string; theme: ThemeKey; story?: ApiTypeStory; - shouldWarnAboutSvg?: boolean; + shouldWarnAboutFiles?: boolean; autoLoadFileMaxSizeMb?: number; lastPlaybackTimestamp?: number; observeIntersectionForLoading?: ObserveFn; @@ -84,7 +84,7 @@ const WebPage: FC = ({ story, theme, backgroundEmojiId, - shouldWarnAboutSvg, + shouldWarnAboutFiles, autoLoadFileMaxSizeMb, lastPlaybackTimestamp, observeIntersectionForLoading, @@ -286,7 +286,7 @@ const WebPage: FC = ({ onMediaClick={onDocumentClick} onCancelUpload={onCancelMediaTransfer} isDownloading={isDownloading} - shouldWarnAboutSvg={shouldWarnAboutSvg} + shouldWarnAboutFiles={shouldWarnAboutFiles} /> )} {stickers && ( diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index f2ed43e18..01435a7fb 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -146,7 +146,7 @@ type StateProps = { isChatProtected?: boolean; nextProfileTab?: ProfileTabType; animationLevel: AnimationLevel; - shouldWarnAboutSvg?: boolean; + shouldWarnAboutFiles?: boolean; similarChannels?: string[]; similarBots?: string[]; botPreviewMedia?: ApiBotPreviewMedia[]; @@ -212,7 +212,7 @@ const Profile: FC = ({ isChatProtected, nextProfileTab, animationLevel, - shouldWarnAboutSvg, + shouldWarnAboutFiles, similarChannels, similarBots, isCurrentUserPremium, @@ -693,7 +693,7 @@ const Profile: FC = ({ observeIntersection={observeIntersectionForMedia} onDateClick={handleMessageFocus} message={messagesById[id]} - shouldWarnAboutSvg={shouldWarnAboutSvg} + shouldWarnAboutFiles={shouldWarnAboutFiles} /> )) ) : resultType === 'links' ? ( @@ -955,7 +955,7 @@ export default memo(withGlobal( const userFullInfo = selectUserFullInfo(global, chatId); const messagesById = selectChatMessages(global, chatId); - const { animationLevel, shouldWarnAboutSvg } = selectSharedSettings(global); + const { animationLevel, shouldWarnAboutFiles } = selectSharedSettings(global); const { currentType: mediaSearchType, resultsByType } = selectCurrentSharedMediaSearch(global) || {}; const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {}; @@ -1040,7 +1040,7 @@ export default memo(withGlobal( nextProfileTab: selectTabState(global).nextProfileTab, forceScrollProfileTab: selectTabState(global).forceScrollProfileTab, animationLevel, - shouldWarnAboutSvg, + shouldWarnAboutFiles, similarChannels: similarChannelIds, similarBots: similarBotsIds, botPreviewMedia, diff --git a/src/global/cache.ts b/src/global/cache.ts index 871f45254..17d0f2825 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -330,7 +330,7 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { shouldAllowHttpTransport: untypedCached.settings.byKey.shouldAllowHttpTransport, shouldCollectDebugLogs: untypedCached.settings.byKey.shouldCollectDebugLogs, shouldDebugExportedSenders: untypedCached.settings.byKey.shouldDebugExportedSenders, - shouldWarnAboutSvg: untypedCached.settings.byKey.shouldWarnAboutSvg, + shouldWarnAboutFiles: untypedCached.settings.byKey.shouldWarnAboutFiles, }; } @@ -351,6 +351,11 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { if (!cached.appConfig) { cached.appConfig = initialState.appConfig; } + + if (untypedCached.sharedState?.settings?.shouldWarnAboutSvg) { + cached.sharedState.settings.shouldWarnAboutFiles = true; + untypedCached.sharedState.settings.shouldWarnAboutSvg = undefined; + } } function updateCache(force?: boolean) { diff --git a/src/global/initialState.ts b/src/global/initialState.ts index d0900098d..8ed3d30ec 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -88,7 +88,7 @@ export const INITIAL_SHARED_STATE: SharedState = { isConnectionStatusMinimized: true, canDisplayChatInTitle: true, shouldAllowHttpTransport: true, - shouldWarnAboutSvg: true, + shouldWarnAboutFiles: true, }, isInitial: true, }; diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 74c7a2ea6..8a3aa23d3 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -24,13 +24,14 @@ import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { ANONYMOUS_USER_ID, GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, - SVG_EXTENSIONS, WEB_APP_PLATFORM, + WEB_APP_PLATFORM, } from '../../config'; import { IS_TRANSLATION_SUPPORTED } from '../../util/browser/windowEnvironment'; import { isUserId } from '../../util/entities/ids'; import { getCurrentTabId } from '../../util/establishMultitabRole'; import { findLast } from '../../util/iteratees'; import { getMessageKey, isLocalMessageId } from '../../util/keys/messageKey'; +import { isIpRevealingMedia } from '../../util/media/ipRevealingMedia'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { getServerTime } from '../../util/serverTime'; import { getDocumentExtension } from '../../components/common/helpers/documentInfo'; @@ -1305,7 +1306,7 @@ export function selectCanForwardMessages(global: T, chatI && (message.isForwardingAllowed || isServiceNotificationMessage(message))); } -export function selectHasSvg(global: T, chatId: string, messageIds: number[]) { +export function selectHasIpRevealingMedia(global: T, chatId: string, messageIds: number[]) { const messages = selectChatMessages(global, chatId); return messageIds @@ -1319,7 +1320,7 @@ export function selectHasSvg(global: T, chatId: string, m const extension = getDocumentExtension(document); if (!extension) return false; - return SVG_EXTENSIONS.has(extension); + return isIpRevealingMedia({ mimeType: document.mimeType, extension }); }); } diff --git a/src/global/types/sharedState.ts b/src/global/types/sharedState.ts index 84662cf50..414736cea 100644 --- a/src/global/types/sharedState.ts +++ b/src/global/types/sharedState.ts @@ -27,6 +27,6 @@ export interface SharedSettings { shouldAllowHttpTransport?: boolean; shouldCollectDebugLogs?: boolean; shouldDebugExportedSenders?: boolean; - shouldWarnAboutSvg?: boolean; + shouldWarnAboutFiles?: boolean; shouldSkipWebAppCloseConfirmation: boolean; } diff --git a/src/util/media/ipRevealingMedia.ts b/src/util/media/ipRevealingMedia.ts new file mode 100644 index 000000000..33da19c82 --- /dev/null +++ b/src/util/media/ipRevealingMedia.ts @@ -0,0 +1,19 @@ +import { SVG_EXTENSIONS } from '../../config'; + +const UNSAFE_MIME_TYPES = new Set(['text/html', 'image/svg+xml']); +const UNSAFE_EXTENSIONS = new Set([ + ...SVG_EXTENSIONS, + 'htm', 'html', 'svg', 'm4v', 'm3u', 'm3u8', 'xhtml', 'xml', +]); + +export function isIpRevealingMedia({ mimeType, extension }: { mimeType?: string; extension: string }) { + if (mimeType && UNSAFE_MIME_TYPES.has(mimeType)) { + return true; + } + + if (UNSAFE_EXTENSIONS.has(extension)) { + return true; + } + + return false; +}