WebShare: Prevent external formatting insertion (#3921)

This commit is contained in:
Alexander Zinchuk 2023-10-27 12:50:00 +02:00
parent d893ca6cfe
commit 3e59a07012
3 changed files with 70 additions and 54 deletions

View File

@ -93,6 +93,7 @@ import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/windowEnvironme
import windowSize from '../../util/windowSize';
import applyIosAutoCapitalizationFix from '../middle/composer/helpers/applyIosAutoCapitalizationFix';
import buildAttachment, { prepareAttachmentsToSend } from '../middle/composer/helpers/buildAttachment';
import { escapeHtml } from '../middle/composer/helpers/cleanHtml';
import { buildCustomEmojiHtml } from '../middle/composer/helpers/customEmoji';
import { isSelectionInsideInput } from '../middle/composer/helpers/selection';
import renderText from './helpers/renderText';
@ -1038,7 +1039,7 @@ const Composer: FC<OwnProps & StateProps> = ({
useEffect(() => {
if (requestedDraftText) {
setHtml(requestedDraftText);
setHtml(escapeHtml(requestedDraftText));
resetOpenChatWithDraft();
requestNextMutation(() => {

View File

@ -0,0 +1,65 @@
import { ApiMessageEntityTypes } from '../../../../api/types';
import { DEBUG } from '../../../../config';
import cleanDocsHtml from '../../../../lib/cleanDocsHtml';
import { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput';
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;
export function preparePastedHtml(html: string) {
let fragment = document.createElement('div');
try {
html = cleanDocsHtml(html);
} catch (err) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.error(err);
}
}
fragment.innerHTML = html.replace(/\u00a0/g, ' ').replace(STYLE_TAG_REGEX, ''); // Strip &nbsp and styles
const textContents = fragment.querySelectorAll<HTMLDivElement>('.text-content');
if (textContents.length) {
fragment = textContents[textContents.length - 1]; // Replace with the last copied message
}
Array.from(fragment.getElementsByTagName('*')).forEach((node) => {
if (!(node instanceof HTMLElement)) {
node.remove();
return;
}
node.removeAttribute('style');
// Fix newlines
if (node.tagName === 'BR') node.replaceWith('\n');
if (node.tagName === 'P') node.appendChild(document.createTextNode('\n'));
if (node.tagName === 'IMG' && !node.dataset.entityType) node.replaceWith(node.getAttribute('alt') || '');
// We do not intercept copy logic, so we remove some nodes here
if (node.dataset.ignoreOnPaste) node.remove();
if (ENTITY_CLASS_BY_NODE_NAME[node.tagName]) {
node.setAttribute('data-entity-type', ENTITY_CLASS_BY_NODE_NAME[node.tagName]);
}
// Strip non-entity tags
if (!node.dataset.entityType && node.textContent === node.innerText) node.replaceWith(node.textContent);
// Append entity parameters for parsing
if (node.dataset.alt) node.setAttribute('alt', node.dataset.alt);
switch (node.dataset.entityType) {
case ApiMessageEntityTypes.MentionName:
node.replaceWith(node.textContent || '');
break;
case ApiMessageEntityTypes.CustomEmoji:
node.textContent = node.dataset.alt || '';
break;
}
});
return fragment.innerHTML.trimEnd();
}
export function escapeHtml(html: string) {
const fragment = document.createElement('div');
const text = document.createTextNode(html);
fragment.appendChild(text);
return fragment.innerHTML;
}

View File

@ -2,72 +2,22 @@ import type { StateHookSetter } from '../../../../lib/teact/teact';
import { useEffect } from '../../../../lib/teact/teact';
import type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../api/types';
import { ApiMessageEntityTypes } from '../../../../api/types';
import {
DEBUG, EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID,
EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID,
} from '../../../../config';
import cleanDocsHtml from '../../../../lib/cleanDocsHtml';
import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols';
import parseMessageInput, { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput';
import parseMessageInput from '../../../../util/parseMessageInput';
import buildAttachment from '../helpers/buildAttachment';
import { preparePastedHtml } from '../helpers/cleanHtml';
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
const MAX_MESSAGE_LENGTH = 4096;
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;
const TYPE_HTML = 'text/html';
const DOCUMENT_TYPE_WORD = 'urn:schemas-microsoft-com:office:word';
const NAMESPACE_PREFIX_WORD = 'xmlns:w';
function preparePastedHtml(html: string) {
let fragment = document.createElement('div');
try {
html = cleanDocsHtml(html);
} catch (err) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.error(err);
}
}
fragment.innerHTML = html.replace(/\u00a0/g, ' ').replace(STYLE_TAG_REGEX, ''); // Strip &nbsp and styles
const textContents = fragment.querySelectorAll<HTMLDivElement>('.text-content');
if (textContents.length) {
fragment = textContents[textContents.length - 1]; // Replace with the last copied message
}
Array.from(fragment.getElementsByTagName('*')).forEach((node) => {
if (!(node instanceof HTMLElement)) return;
node.removeAttribute('style');
// Fix newlines
if (node.tagName === 'BR') node.replaceWith('\n');
if (node.tagName === 'P') node.appendChild(document.createTextNode('\n'));
if (node.tagName === 'IMG' && !node.dataset.entityType) node.replaceWith(node.getAttribute('alt') || '');
// We do not intercept copy logic, so we remove some nodes here
if (node.dataset.ignoreOnPaste) node.remove();
if (ENTITY_CLASS_BY_NODE_NAME[node.tagName]) {
node.setAttribute('data-entity-type', ENTITY_CLASS_BY_NODE_NAME[node.tagName]);
}
// Strip non-entity tags
if (!node.dataset.entityType && node.textContent === node.innerText) node.replaceWith(node.textContent);
// Append entity parameters for parsing
if (node.dataset.alt) node.setAttribute('alt', node.dataset.alt);
switch (node.dataset.entityType) {
case ApiMessageEntityTypes.MentionName:
node.replaceWith(node.textContent || '');
break;
case ApiMessageEntityTypes.CustomEmoji:
node.textContent = node.dataset.alt || '';
break;
}
});
return fragment.innerHTML.trimEnd();
}
const useClipboardPaste = (
isActive: boolean,
insertTextAndUpdateCursor: (text: ApiFormattedText, inputId?: string) => void,