[IMP] web_editor: do not alter code pasted from editor

Purpose

Allow users to copy/paste specific elements that are generated by Odoo instead
of sanitizing it on paste. It allows to keep structured data in DOM.

Prepares Task-2796156
Prepares odoo/enterprise#29423

X-original-commit: 36ed4db2b378543d51e6e3eb03962707dbf15b1d
Part-of: odoo/odoo#101694
This commit is contained in:
Nicolas Bayet
2022-09-30 10:33:01 +02:00
committed by Thibault Delavallée
parent 1f6005070f
commit d8cf028714
2 changed files with 50 additions and 1 deletions
@@ -589,6 +589,7 @@ export class OdooEditor extends EventTarget {
this.addDomListener(this.editable, 'paste', this._onPaste);
this.addDomListener(this.editable, 'drop', this._onDrop);
this.addDomListener(this.document, 'copy', this._onClipboardCopy);
this.addDomListener(this.document, 'selectionchange', this._onSelectionChange);
this.addDomListener(this.document, 'selectionchange', this._handleCommandHint);
this.addDomListener(this.document, 'keydown', this._onDocumentKeydown);
@@ -3081,6 +3082,26 @@ export class OdooEditor extends EventTarget {
}
}
_onClipboardCopy(clipboardEvent) {
if (this.isSelectionInEditable()) {
clipboardEvent.preventDefault();
const selection = this.document.getSelection();
const range = selection.getRangeAt(0);
const rangeContent = range.cloneContents();
const dataHtmlElement = document.createElement('data');
dataHtmlElement.append(rangeContent);
const odooHtml = dataHtmlElement.innerHTML;
const odooText = dataHtmlElement.innerText;
if (!clipboardEvent.clipboardData.getData('text/plain')) {
clipboardEvent.clipboardData.setData('text/plain', odooText);
}
if (!clipboardEvent.clipboardData.getData('text/html')) {
clipboardEvent.clipboardData.setData('text/html', odooHtml);
}
clipboardEvent.clipboardData.setData('text/odoo-editor', odooHtml);
}
}
/**
* @private
*/
@@ -3882,8 +3903,15 @@ export class OdooEditor extends EventTarget {
ev.preventDefault();
const sel = this.document.getSelection();
const files = getImageFiles(ev.clipboardData);
const odooEditorHtml = ev.clipboardData.getData('text/odoo-editor');
const clipboardHtml = ev.clipboardData.getData('text/html');
if (clipboardHtml) {
if (odooEditorHtml) {
const fragment = parseHTML(odooEditorHtml);
DOMPurify.sanitize(fragment, { IN_PLACE: true });
if (fragment.hasChildNodes()) {
this.execCommand('insert', fragment);
}
} else if (clipboardHtml) {
this.execCommand('insert', this._prepareClipboardData(clipboardHtml));
} else if (files.length) {
this.addImagesFiles(files);
@@ -21,6 +21,7 @@ const pasteData = async function (editor, text, type) {
const pasteText = async (editor, text) => pasteData(editor, text, 'text/plain');
const pasteHtml = async (editor, html) => pasteData(editor, html, 'text/html');
const pasteOdooEditorHtml = async (editor, html) => pasteData(editor, html, 'text/odoo-editor');
describe('Copy and paste', () => {
describe('Html Paste cleaning', () => {
@@ -1479,4 +1480,24 @@ describe('Copy and paste', () => {
});
});
});
describe('Odoo editor own html', () => {
it('should paste html as is', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>a[]b</p>',
stepFunction: async editor => {
await pasteOdooEditorHtml(editor, '<div class="custom-paste">b</div>');
},
contentAfter: '<p>a</p><div class="custom-paste">b</div>[]<p>b</p>',
});
});
it('should not paste unsafe content', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>a[]b</p>',
stepFunction: async editor => {
await pasteOdooEditorHtml(editor, `<script>console.log('xss attack')</script>`);
},
contentAfter: '<p>a[]b</p>',
});
});
});
});