[FIX] web_editor: prevent editing of header
The way the live HTML editor for html_field works means the header (and other nodes that should only appear once) are removed and the content inserted inside the body. This results in values being edited without the user being aware. For emails, this results in any email using 'style' in the header to break on GMail and other clients that do not support 'style' outside of the header. When an html_field is passed a value containing a header the html_field appears readonly and the user is able to toggle the code view. Which enables them to edit the 'raw' HTML and see changes while preventing the presentation view from affecting the database value task - 3035163 closes odoo/odoo#118920 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com> Signed-off-by: Thiry Renaud (reth) <reth@odoo.com>
This commit is contained in:
@@ -74,6 +74,12 @@ export class HtmlFieldWysiwygAdapterComponent extends ComponentAdapter {
|
||||
|
||||
export class HtmlField extends Component {
|
||||
setup() {
|
||||
// readonly if nodes in <head> as the head will be removed on insertion in the DOM
|
||||
// this prevents the field from overwriting the value with invalid HTML when relevant
|
||||
const domParser = new DOMParser();
|
||||
const parsedOriginal = domParser.parseFromString(this.props.record.data[this.props.name] || '', 'text/html');
|
||||
this.containsComplexHTML = !!parsedOriginal.head.innerHTML.trim();
|
||||
|
||||
this.readonlyElementRef = useRef("readonlyElement");
|
||||
this.codeViewRef = useRef("codeView");
|
||||
this.iframeRef = useRef("iframe");
|
||||
@@ -326,19 +332,21 @@ export class HtmlField extends Component {
|
||||
}
|
||||
/**
|
||||
* Toggle the code view and update the UI.
|
||||
*
|
||||
* @param {JQuery} $codeview
|
||||
*/
|
||||
toggleCodeView() {
|
||||
this.state.showCodeView = !this.state.showCodeView;
|
||||
|
||||
this.wysiwyg.odooEditor.observerUnactive('toggleCodeView');
|
||||
if (this.state.showCodeView) {
|
||||
this.wysiwyg.odooEditor.toolbarHide();
|
||||
const value = this.wysiwyg.getValue();
|
||||
this.props.record.update({ [this.props.name]: value });
|
||||
} else {
|
||||
this.wysiwyg.odooEditor.observerActive('toggleCodeView');
|
||||
if (this.wysiwyg) {
|
||||
this.wysiwyg.odooEditor.observerUnactive('toggleCodeView');
|
||||
if (this.state.showCodeView) {
|
||||
this.wysiwyg.odooEditor.toolbarHide();
|
||||
const value = this.wysiwyg.getValue();
|
||||
this.props.record.update({ [this.props.name]: value });
|
||||
} else {
|
||||
this.wysiwyg.odooEditor.observerActive('toggleCodeView');
|
||||
}
|
||||
}
|
||||
if (!this.state.showCodeView) {
|
||||
const $codeview = $(this.codeViewRef.el);
|
||||
const value = $codeview.val();
|
||||
this.props.record.update({ [this.props.name]: value });
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="web_editor.HtmlField" owl="1">
|
||||
<t t-if="props.readonly || props.notEditable">
|
||||
<t t-if="props.readonly || props.notEditable || (containsComplexHTML and !state.showCodeView)">
|
||||
<t t-if="this.showIframe">
|
||||
<iframe t-ref="iframe" t-att-class="{'d-none': !this.state.iframeVisible, 'o_readonly': true}"></iframe>
|
||||
</t>
|
||||
@@ -13,11 +13,6 @@
|
||||
<div t-else="" class="h-100" t-ref="spellcheck">
|
||||
<t t-if="state.showCodeView">
|
||||
<textarea t-ref="codeView" class="o_codeview" t-att-value="markupValue"/>
|
||||
<div t-ref="codeViewButton" id="codeview-btn-group" class="btn-group" t-on-click="toggleCodeView">
|
||||
<button class="o_codeview_btn btn btn-primary">
|
||||
<i class="fa fa-code" />
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<HtmlFieldWysiwygAdapterComponent Component="this.Wysiwyg"
|
||||
@@ -34,6 +29,11 @@
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="state.showCodeView || containsComplexHTML" t-ref="codeViewButton" id="codeview-btn-group" class="btn-group" t-on-click="toggleCodeView">
|
||||
<button class="o_codeview_btn btn btn-primary">
|
||||
<i class="fa fa-code" />
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { click, editInput, getFixture, makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { HtmlField } from "@web_editor/js/backend/html_field";
|
||||
import { onRendered } from "@odoo/owl";
|
||||
|
||||
QUnit.module("WebEditor.HtmlField", ({ beforeEach }) => {
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
txt: { string: "txt", type: "html", trim: true },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
/**
|
||||
* Check that documents with data in a <head> node are set to readonly
|
||||
* with a codeview option.
|
||||
*/
|
||||
QUnit.test("html fields with complete HTML document", async (assert) => {
|
||||
assert.timeout(2000);
|
||||
assert.expect(12);
|
||||
let codeViewState = false;
|
||||
let togglePromiseId = 0;
|
||||
const togglePromises = [makeDeferred(), makeDeferred()];
|
||||
patchWithCleanup(HtmlField.prototype, {
|
||||
setup: function () {
|
||||
this._super(...arguments);
|
||||
onRendered(() => {
|
||||
if (codeViewState !== this.state.showCodeView) {
|
||||
togglePromises[togglePromiseId].resolve();
|
||||
}
|
||||
codeViewState = this.state.showCodeView;
|
||||
});
|
||||
},
|
||||
});
|
||||
const htmlDocumentTextTemplate = (text, color) => `
|
||||
<!DOCTYPE HTML>
|
||||
<html xml:lang="en" lang="en">
|
||||
<head>
|
||||
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="format-detection" content="telephone=no"/>
|
||||
<style type="text/css">
|
||||
body {
|
||||
color: ${color};
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${text}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
txt: htmlDocumentTextTemplate('Hello', 'red'),
|
||||
}];
|
||||
const writePromise = makeDeferred();
|
||||
await makeView({
|
||||
type: "form",
|
||||
resId: 1,
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page string="Body" name="body">
|
||||
<field name="txt" widget="html"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write" && args.model === 'partner') {
|
||||
assert.equal(args.args[1].txt, htmlDocumentTextTemplate('Hi', 'black'));
|
||||
writePromise.resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fieldHtml = target.querySelector('.o_field_html');
|
||||
let readonlyNode = fieldHtml.querySelector('.o_readonly');
|
||||
assert.ok(readonlyNode);
|
||||
assert.equal(readonlyNode.innerText, 'Hello');
|
||||
assert.equal(window.getComputedStyle(readonlyNode).color, 'rgb(255, 0, 0)');
|
||||
|
||||
const codeViewButton = fieldHtml.querySelector('.o_codeview_btn');
|
||||
assert.ok(codeViewButton);
|
||||
|
||||
await click(codeViewButton);
|
||||
await togglePromises[togglePromiseId];
|
||||
const codeView = fieldHtml.querySelector('textarea.o_codeview');
|
||||
assert.ok(codeView);
|
||||
assert.equal(codeView.value, htmlDocumentTextTemplate('Hello', 'red'));
|
||||
|
||||
await editInput(codeView, null, htmlDocumentTextTemplate('Hi', 'black'));
|
||||
|
||||
assert.ok(codeViewButton);
|
||||
togglePromiseId++;
|
||||
await click(codeViewButton);
|
||||
await togglePromises[togglePromiseId];
|
||||
readonlyNode = fieldHtml.querySelector('.o_readonly');
|
||||
assert.ok(readonlyNode);
|
||||
assert.equal(readonlyNode.innerText, 'Hi');
|
||||
assert.equal(window.getComputedStyle(readonlyNode).color, 'rgb(0, 0, 0)');
|
||||
|
||||
const saveButton = target.querySelector('.o_form_button_save');
|
||||
assert.ok(saveButton);
|
||||
await click(saveButton);
|
||||
await writePromise;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user