WebShare: Prevent external formatting insertion (#3921)
This commit is contained in:
parent
d893ca6cfe
commit
3e59a07012
@ -93,6 +93,7 @@ import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/windowEnvironme
|
|||||||
import windowSize from '../../util/windowSize';
|
import windowSize from '../../util/windowSize';
|
||||||
import applyIosAutoCapitalizationFix from '../middle/composer/helpers/applyIosAutoCapitalizationFix';
|
import applyIosAutoCapitalizationFix from '../middle/composer/helpers/applyIosAutoCapitalizationFix';
|
||||||
import buildAttachment, { prepareAttachmentsToSend } from '../middle/composer/helpers/buildAttachment';
|
import buildAttachment, { prepareAttachmentsToSend } from '../middle/composer/helpers/buildAttachment';
|
||||||
|
import { escapeHtml } from '../middle/composer/helpers/cleanHtml';
|
||||||
import { buildCustomEmojiHtml } from '../middle/composer/helpers/customEmoji';
|
import { buildCustomEmojiHtml } from '../middle/composer/helpers/customEmoji';
|
||||||
import { isSelectionInsideInput } from '../middle/composer/helpers/selection';
|
import { isSelectionInsideInput } from '../middle/composer/helpers/selection';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
@ -1038,7 +1039,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (requestedDraftText) {
|
if (requestedDraftText) {
|
||||||
setHtml(requestedDraftText);
|
setHtml(escapeHtml(requestedDraftText));
|
||||||
resetOpenChatWithDraft();
|
resetOpenChatWithDraft();
|
||||||
|
|
||||||
requestNextMutation(() => {
|
requestNextMutation(() => {
|
||||||
|
|||||||
65
src/components/middle/composer/helpers/cleanHtml.ts
Normal file
65
src/components/middle/composer/helpers/cleanHtml.ts
Normal 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   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;
|
||||||
|
}
|
||||||
@ -2,72 +2,22 @@ import type { StateHookSetter } from '../../../../lib/teact/teact';
|
|||||||
import { useEffect } from '../../../../lib/teact/teact';
|
import { useEffect } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
import type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../api/types';
|
import type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../../../../api/types';
|
|
||||||
|
|
||||||
import {
|
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';
|
} from '../../../../config';
|
||||||
import cleanDocsHtml from '../../../../lib/cleanDocsHtml';
|
|
||||||
import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols';
|
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 buildAttachment from '../helpers/buildAttachment';
|
||||||
|
import { preparePastedHtml } from '../helpers/cleanHtml';
|
||||||
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
|
||||||
|
|
||||||
const MAX_MESSAGE_LENGTH = 4096;
|
const MAX_MESSAGE_LENGTH = 4096;
|
||||||
|
|
||||||
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;
|
|
||||||
const TYPE_HTML = 'text/html';
|
const TYPE_HTML = 'text/html';
|
||||||
const DOCUMENT_TYPE_WORD = 'urn:schemas-microsoft-com:office:word';
|
const DOCUMENT_TYPE_WORD = 'urn:schemas-microsoft-com:office:word';
|
||||||
const NAMESPACE_PREFIX_WORD = 'xmlns:w';
|
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   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 = (
|
const useClipboardPaste = (
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
insertTextAndUpdateCursor: (text: ApiFormattedText, inputId?: string) => void,
|
insertTextAndUpdateCursor: (text: ApiFormattedText, inputId?: string) => void,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user