File: Show warning before downloading HTML (#6145)

This commit is contained in:
zubiden 2025-08-29 08:57:50 +02:00 committed by Alexander Zinchuk
parent 17dd4d47b0
commit 452929ad35
12 changed files with 89 additions and 63 deletions

View File

@ -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<HTMLDivElement>();
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}
/>
<ConfirmDialog
isOpen={isSvgDialogOpen}
onClose={closeSvgDialog}
confirmHandler={handleSvgConfirm}
isOpen={isFileIpDialogOpen}
onClose={closeFileIpDialog}
confirmHandler={handleFileIpConfirm}
>
{lang('lng_launch_svg_warning')}
<Checkbox
className="dialog-checkbox"
checked={shouldNotWarnAboutSvg}
checked={shouldNotWarnAboutFiles}
label={lang('lng_launch_exe_dont_ask')}
onCheck={setShouldNotWarnAboutSvg}
onCheck={setShouldNotWarnAboutFiles}
/>
</ConfirmDialog>
</>

View File

@ -44,7 +44,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
globalMessagesByChatId,
foundIds,
activeDownloads,
shouldWarnAboutSvg,
shouldWarnAboutFiles,
}) => {
const {
searchMessagesGlobal,
@ -117,7 +117,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
sender={getSenderName(lang, message, chatsById, usersById)}
className="scroll-item"
isDownloading={getIsDownloading(activeDownloads, message.content.document!)}
shouldWarnAboutSvg={shouldWarnAboutSvg}
shouldWarnAboutFiles={shouldWarnAboutFiles}
observeIntersection={observeIntersectionForMedia}
onDateClick={handleMessageFocus}
/>

View File

@ -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,
};
};
}

View File

@ -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<OwnProps & StateProps> = ({
@ -68,8 +68,8 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
hasProtectedMessage,
isAnyModalOpen,
selectedMessageIds,
shouldWarnAboutSvg,
hasSvgs,
shouldWarnAboutFiles,
hasIpRevealingMedia,
}) => {
const {
exitMessageSelectMode,
@ -85,8 +85,8 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
});
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<OwnProps & StateProps> = ({
</div>
</div>
<ConfirmDialog
isOpen={isSvgDialogOpen}
onClose={closeSvgDialog}
confirmHandler={handleSvgConfirm}
isOpen={isFileIpDialogOpen}
onClose={closeFileIpDialog}
confirmHandler={handleFileIpConfirm}
>
{lang('lng_launch_svg_warning')}
<Checkbox
className="dialog-checkbox"
checked={shouldNotWarnAboutSvg}
checked={shouldNotWarnAboutFiles}
label={lang('lng_launch_exe_dont_ask')}
onCheck={setShouldNotWarnAboutSvg}
onCheck={setShouldNotWarnAboutFiles}
/>
</ConfirmDialog>
</>
@ -242,7 +242,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
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<OwnProps>(
selectedMessageIds,
hasProtectedMessage,
isAnyModalOpen,
shouldWarnAboutSvg,
hasSvgs,
shouldWarnAboutFiles,
hasIpRevealingMedia,
};
},
)(MessageSelectToolbar));

View File

@ -310,7 +310,7 @@ type StateProps = {
webPageStory?: ApiTypeStory;
isConnected: boolean;
isLoadingComments?: boolean;
shouldWarnAboutSvg?: boolean;
shouldWarnAboutFiles?: boolean;
senderBoosts?: number;
tags?: Record<ApiReactionKey, ApiSavedReactionTag>;
canTranscribeVoice?: boolean;
@ -438,7 +438,7 @@ const Message: FC<OwnProps & StateProps> = ({
webPageStory,
isConnected,
getIsMessageListReady,
shouldWarnAboutSvg,
shouldWarnAboutFiles,
senderBoosts,
tags,
canTranscribeVoice,
@ -1261,7 +1261,7 @@ const Message: FC<OwnProps & StateProps> = ({
onMediaClick={handleDocumentClick}
onCancelUpload={handleCancelUpload}
isDownloading={isDownloading}
shouldWarnAboutSvg={shouldWarnAboutSvg}
shouldWarnAboutFiles={shouldWarnAboutFiles}
/>
)}
{storyData && !isStoryMention && (
@ -1430,7 +1430,7 @@ const Message: FC<OwnProps & StateProps> = ({
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<OwnProps>(
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<OwnProps>(
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 && {

View File

@ -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<OwnProps & StateProps> = ({
story,
theme,
backgroundEmojiId,
shouldWarnAboutSvg,
shouldWarnAboutFiles,
autoLoadFileMaxSizeMb,
lastPlaybackTimestamp,
observeIntersectionForLoading,
@ -286,7 +286,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
onMediaClick={onDocumentClick}
onCancelUpload={onCancelMediaTransfer}
isDownloading={isDownloading}
shouldWarnAboutSvg={shouldWarnAboutSvg}
shouldWarnAboutFiles={shouldWarnAboutFiles}
/>
)}
{stickers && (

View File

@ -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<OwnProps & StateProps> = ({
isChatProtected,
nextProfileTab,
animationLevel,
shouldWarnAboutSvg,
shouldWarnAboutFiles,
similarChannels,
similarBots,
isCurrentUserPremium,
@ -693,7 +693,7 @@ const Profile: FC<OwnProps & StateProps> = ({
observeIntersection={observeIntersectionForMedia}
onDateClick={handleMessageFocus}
message={messagesById[id]}
shouldWarnAboutSvg={shouldWarnAboutSvg}
shouldWarnAboutFiles={shouldWarnAboutFiles}
/>
))
) : resultType === 'links' ? (
@ -955,7 +955,7 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
nextProfileTab: selectTabState(global).nextProfileTab,
forceScrollProfileTab: selectTabState(global).forceScrollProfileTab,
animationLevel,
shouldWarnAboutSvg,
shouldWarnAboutFiles,
similarChannels: similarChannelIds,
similarBots: similarBotsIds,
botPreviewMedia,

View File

@ -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) {

View File

@ -88,7 +88,7 @@ export const INITIAL_SHARED_STATE: SharedState = {
isConnectionStatusMinimized: true,
canDisplayChatInTitle: true,
shouldAllowHttpTransport: true,
shouldWarnAboutSvg: true,
shouldWarnAboutFiles: true,
},
isInitial: true,
};

View File

@ -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<T extends GlobalState>(global: T, chatI
&& (message.isForwardingAllowed || isServiceNotificationMessage(message)));
}
export function selectHasSvg<T extends GlobalState>(global: T, chatId: string, messageIds: number[]) {
export function selectHasIpRevealingMedia<T extends GlobalState>(global: T, chatId: string, messageIds: number[]) {
const messages = selectChatMessages(global, chatId);
return messageIds
@ -1319,7 +1320,7 @@ export function selectHasSvg<T extends GlobalState>(global: T, chatId: string, m
const extension = getDocumentExtension(document);
if (!extension) return false;
return SVG_EXTENSIONS.has(extension);
return isIpRevealingMedia({ mimeType: document.mimeType, extension });
});
}

View File

@ -27,6 +27,6 @@ export interface SharedSettings {
shouldAllowHttpTransport?: boolean;
shouldCollectDebugLogs?: boolean;
shouldDebugExportedSenders?: boolean;
shouldWarnAboutSvg?: boolean;
shouldWarnAboutFiles?: boolean;
shouldSkipWebAppCloseConfirmation: boolean;
}

View File

@ -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;
}