From 51b5ee8d5f245479ffbd7a76dc574a8ef5362573 Mon Sep 17 00:00:00 2001 From: abd-msyukyu-odoo Date: Mon, 24 Oct 2022 13:59:03 +0000 Subject: [PATCH] [FIX] web_editor, knowledge: prevent modification of data-oe-protected Change class="oe-blackbox" to data-oe-protected="true" (same purpose), because the name was decided in a rush before 16.0 When the editor history management and sanitization (sanitize.js) handle nodes of an embedded view, OWL can become corrupted, which will lead to a traceback. This commit uses the attribute data-oe-protected="true" to prevent historyRollbacks or sanitization, and ensure that protected elements children are removed during the cleanForSave operation. The collaborative DOMPurify sanitization is untouched, for security reasons. To properly use `data-oe-protected` in collaborative mode, a more complex mechanism should be put in place to ensure that the protected element children are properly reconstructed for each client, and that it is not part of the collaboration. (i.e.: embedded view in knowledge do not support the collaborative mode currently). This commit also blocks the selection handling in tables which would be in a data-oe-protected="true" node. Task-3012552 closes odoo/odoo#106255 X-original-commit: 394e4fe567bb2eecbd0865de82914aa7396fde63 Related: odoo/enterprise#34245 Signed-off-by: David Beguin (dbe) Signed-off-by: Abeloos Damien (abd) --- .../js/editor/odoo-editor/src/OdooEditor.js | 26 +++++++-- .../editor/odoo-editor/src/utils/sanitize.js | 4 ++ .../odoo-editor/test/spec/editor.test.js | 58 ++++++++++++++++++- .../static/src/js/wysiwyg/wysiwyg.js | 4 +- odoo/tools/mail.py | 1 + 5 files changed, 86 insertions(+), 7 deletions(-) diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js index 0fb5e4d1048..05b2822d1ef 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js @@ -929,6 +929,13 @@ export class OdooEditor extends EventTarget { continue; } } + if (record.target && [Node.TEXT_NODE, Node.ELEMENT_NODE].includes(record.target.nodeType)) { + const closestProtected = closestElement(record.target, '[data-oe-protected="true"]'); + if (closestProtected && closestProtected.nodeType === Node.ELEMENT_NODE && + record.target !== closestProtected) { + continue; + } + } filteredRecords.push(record); } return this.options.filterMutationRecords(filteredRecords); @@ -2164,12 +2171,16 @@ export class OdooEditor extends EventTarget { * @returns {boolean} */ _handleSelectionInTable(ev=undefined) { + const selection = this.document.getSelection(); + const anchorNode = selection.anchorNode; + if (anchorNode && closestElement(anchorNode, '[data-oe-protected="true"]')) { + return false; + } this.deselectTable(); const traversedNodes = getTraversedNodes(this.editable); if (this._isResizingTable || !traversedNodes.some(node => !!closestElement(node, 'td'))) { return false; } - const selection = this.document.getSelection(); let range; if (selection.rangeCount > 1) { // Firefox selection in table works with multiple ranges. @@ -3362,7 +3373,7 @@ export class OdooEditor extends EventTarget { _onSelectionChange() { const selection = this.document.getSelection(); const anchorNode = selection.anchorNode; - if (anchorNode && closestElement(anchorNode, '.oe-blackbox')) { + if (anchorNode && closestElement(anchorNode, '[data-oe-protected="true"]')) { return; } @@ -3547,6 +3558,13 @@ export class OdooEditor extends EventTarget { emptyElement.removeAttribute('data-oe-zws-empty-inline'); } } + + // Clean all protected nodes because they are not sanitized + const protectedNodes = element.querySelectorAll('[data-oe-protected="true"]'); + for (const node of protectedNodes) { + node.replaceChildren(); + } + sanitize(element); // Remove contenteditable=false on elements @@ -3578,7 +3596,7 @@ export class OdooEditor extends EventTarget { _handleCommandHint() { const selection = this.document.getSelection(); const anchorNode = selection.anchorNode; - if (anchorNode && closestElement(anchorNode, '.oe-blackbox')) { + if (anchorNode && closestElement(anchorNode, '[data-oe-protected="true"]')) { return; } @@ -3652,7 +3670,7 @@ export class OdooEditor extends EventTarget { _fixSelectionOnContenteditableFalse() { const selection = this.document.getSelection(); const anchorNode = selection.anchorNode; - if (anchorNode && closestElement(anchorNode, '.oe-blackbox')) { + if (anchorNode && closestElement(anchorNode, '[data-oe-protected="true"]')) { return; } // When the browser set the selection inside a node that is diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js index 77307dec78d..c9c468f9807 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js @@ -109,6 +109,10 @@ class Sanitize { _parse(node) { while (node) { + const closestProtected = closestElement(node, '[data-oe-protected="true"]'); + if (closestProtected && node !== closestProtected) { + return; + } // Merge identical elements together. while ( areSimilarElements(node, node.previousSibling) && diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js index 30de9bc5135..deee3fd67c5 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js @@ -1,5 +1,8 @@ import { OdooEditor } from '../../src/OdooEditor.js'; -import { getTraversedNodes } from '../../src/utils/utils.js'; +import { + getTraversedNodes, + setSelection, +} from '../../src/utils/utils.js'; import { BasicEditor, deleteBackward, @@ -5688,4 +5691,57 @@ X[] }); }); }); + + describe('oe-protected', () => { + it('should ignore protected elements children mutations', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(` +

a[]

+

a

+ `), + stepFunction: async editor => { + await insertText(editor, 'bc'); + const protectedParagraph = editor.editable.querySelector('[data-oe-protected="true"] > p'); + setSelection(protectedParagraph, 1); + await insertText(editor, 'b'); + editor.historyUndo(); + }, + contentAfterEdit: unformat(` +

ab[]

+

ab

+ `), + }); + }); + it('should not sanitize protected elements children', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(` +
+

+

+
+
+

+

+
+ `), + stepFunction: async editor => editor.sanitize(), + contentAfterEdit: unformat(` +
+

\u200B

+

+
+
+

+

+
+ `), + }); + }); + it('should remove protected elements children during cleaning', async () => { + await testEditor(BasicEditor, { + contentBefore: '

a[]

a

', + contentAfter: '

a[]

', + }); + }); + }); }); diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index bdbadfe581e..4cf7ad14907 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -302,7 +302,7 @@ const Wysiwyg = Widget.extend({ const selection = self.odooEditor.document.getSelection(); const anchorNode = selection.anchorNode; - if (anchorNode && closestElement(anchorNode, '.oe-blackbox')) { + if (anchorNode && closestElement(anchorNode, '[data-oe-protected="true"]')) { return; } @@ -1777,7 +1777,7 @@ const Wysiwyg = Widget.extend({ _updateEditorUI: function (e) { let selection = this.odooEditor.document.getSelection(); const anchorNode = selection.anchorNode; - if (anchorNode && closestElement(anchorNode, '.oe-blackbox')) { + if (anchorNode && closestElement(anchorNode, '[data-oe-protected="true"]')) { return; } diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py index b27b6e707a3..71fccea4957 100644 --- a/odoo/tools/mail.py +++ b/odoo/tools/mail.py @@ -35,6 +35,7 @@ safe_attrs = clean.defs.safe_attrs | frozenset( 'data-publish', 'data-id', 'data-res_id', 'data-interval', 'data-member_id', 'data-scroll-background-ratio', 'data-view-id', 'data-class', 'data-mimetype', 'data-original-src', 'data-original-id', 'data-gl-filter', 'data-quality', 'data-resize-width', 'data-shape', 'data-shape-colors', 'data-file-name', 'data-original-mimetype', + 'data-oe-protected', # editor 'data-behavior-props', 'data-prop-name', # knowledge commands ]) SANITIZE_TAGS = {