[FIX] website: prevent duplicate code in embed snippet
Since the introduction of the Embed Code snippet with [1], in case some
content was created dynamically through a `<script>` tag, it would be
duplicated upon editing the snippet again after it had been displayed a
1st time. This is because each time you open the snippet's ace editor,
the current state of the snippet (including dynamically created
elements) is saved in the view.
This commit removes the `<script>`s inside embed code snippets from the
view in edit mode, and then saves them on the server upon save.
We also take the opportunity to add a message in edit mode if the
snippet doesn't display anything (e.g. if it only contains a script tag,
or an empty element), so that it is easily focusable to edit its
content.
Finally, we add a message upon editing an embed code snippet to inform
the user that they should not use it unless they know what they're doing
as well as tell them they may inject code in the `<head>` or `<body>`
elements through the Theme panel.
Note: this fix is only valid for code injected inside the embed code
snippet. For code injected outside of the snippet, we have no way of
controlling / sanitizing the DOM after the fact.
Steps to reproduce:
1. Drag and drop an Embed Code Snippet
2. Copy the following code:
```
<script>
document.addEventListener('DOMContentLoaded', function () {
const alertEl = document.createElement('div');
alertEl.classList.add('alert', 'alert-primary');
alertEl.textContent = "Hello";
document.getElementById('some-stuff').appendChild(alertEl);
});
</script>
<div id="some-stuff"></div>
```
3. Save and exit the editor. The injected div should appear.
4. Go back to the editor, click to edit the snippet and either save or
discard.
5. Exit the editor
=> The div is duplicated.
[1]: https://github.com/odoo/odoo/commit/2cc481d1a62202ade4c1ca8f846c962f9f2cc34d
opw-3513760
closes odoo/odoo#152502
X-original-commit: e6fe14dc90af6782693fd5e9dca6f18ecfe7c89a
Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
@@ -149,6 +149,9 @@
|
||||
'website/static/src/xml/website.background.video.xml',
|
||||
'website/static/src/xml/website.share.xml',
|
||||
'website/static/src/js/text_processing.js',
|
||||
# Stable fix, will be replaced by an `ir.asset` in master to be able
|
||||
# to clean `<script>` tags in embed code snippets in edit mode.
|
||||
'website/static/src/snippets/s_embed_code/000.js',
|
||||
],
|
||||
'web.assets_frontend_minimal': [
|
||||
'website/static/src/js/content/inject_dom.js',
|
||||
@@ -255,7 +258,6 @@
|
||||
'website/static/src/snippets/s_map/options.js',
|
||||
'website/static/src/snippets/s_dynamic_snippet/options.js',
|
||||
'website/static/src/snippets/s_dynamic_snippet_carousel/options.js',
|
||||
'website/static/src/snippets/s_embed_code/options.js',
|
||||
'website/static/src/snippets/s_website_controller_page_listing_layout/options.js',
|
||||
'website/static/src/snippets/s_website_form/options.js',
|
||||
'website/static/src/js/form_editor_registry.js',
|
||||
@@ -282,6 +284,7 @@
|
||||
('include', 'web_editor.assets_legacy_wysiwyg'),
|
||||
('include', 'website.assets_wysiwyg'),
|
||||
'website/static/src/components/wysiwyg_adapter/wysiwyg_adapter.js',
|
||||
'website/static/src/snippets/s_embed_code/options.js',
|
||||
],
|
||||
'web_editor.assets_media_dialog': [
|
||||
'website/static/src/components/media_dialog/image_selector.js',
|
||||
|
||||
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: Odoo Server 17.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-01-29 10:44+0000\n"
|
||||
"PO-Revision-Date: 2024-01-29 10:44+0000\n"
|
||||
"PO-Revision-Date: 2024-02-05 12:08+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -4109,6 +4109,16 @@ msgstr ""
|
||||
msgid "Display this website when users visit this domain"
|
||||
msgstr ""
|
||||
|
||||
#. module: website
|
||||
#. odoo-javascript
|
||||
#: code:addons/website/static/src/components/edit_head_body_dialog/edit_head_body_dialog.xml:0
|
||||
#: code:addons/website/static/src/xml/website.editor.xml:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Do not copy/paste code you do not understand, this could put your data at "
|
||||
"risk."
|
||||
msgstr ""
|
||||
|
||||
#. module: website
|
||||
#: model_terms:ir.ui.view,arch_db:website.s_mega_menu_cards
|
||||
msgid ""
|
||||
@@ -5902,6 +5912,16 @@ msgid ""
|
||||
"cancel to return to edit mode."
|
||||
msgstr ""
|
||||
|
||||
#. module: website
|
||||
#. odoo-javascript
|
||||
#: code:addons/website/static/src/components/resource_editor/resource_editor_warning.xml:0
|
||||
#: code:addons/website/static/src/xml/website.editor.xml:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"If you need to add analytics or marketing tags, inject code in your <head> "
|
||||
"or <body> instead."
|
||||
msgstr ""
|
||||
|
||||
#. module: website
|
||||
#. odoo-javascript
|
||||
#: code:addons/website/static/src/components/resource_editor/resource_editor_warning.xml:0
|
||||
@@ -12652,6 +12672,15 @@ msgstr ""
|
||||
msgid "Your Email"
|
||||
msgstr ""
|
||||
|
||||
#. module: website
|
||||
#. odoo-javascript
|
||||
#: code:addons/website/static/src/snippets/s_embed_code/000.js:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Your Embed Code snippet doesn't have anything to display. Click on Edit to "
|
||||
"modify it."
|
||||
msgstr ""
|
||||
|
||||
#. module: website
|
||||
#. odoo-javascript
|
||||
#: code:addons/website/static/src/js/send_mail_form.js:0
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<t t-name="website.EditHeadBodyDialog">
|
||||
<Dialog title="'Edit Head and Body Code'">
|
||||
<p class="h5">Do not copy/paste code you do not understand, this could put your data at risk.</p>
|
||||
<div>Enter code that will be added into the <head> of every page of your site.</div>
|
||||
<CodeEditor mode="'xml'" theme="'monokai'"
|
||||
class="'my-2 rounded border o_website_code_editor_field'"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<i class="fa fa-warning mb-4 fs-1 text-danger"/>
|
||||
<div>
|
||||
<h1 class="fs-2 text-dark">Your changes might be lost during future Odoo upgrade.</h1>
|
||||
<p>If you need to add analytics or marketing tags, inject code in your header or body instead.</p>
|
||||
<p>If you need to add analytics or marketing tags, inject code in your <head> or <body> instead.</p>
|
||||
<em>Note: To embed code in this specific page, use the "Embed Code" building block</em>
|
||||
<div class="d-flex flex-row gap-3 pt-4">
|
||||
<button type="button" class="btn btn-primary" t-on-click="onInjectCode">Inject code in header or body</button>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useHotkey } from '@web/core/hotkeys/hotkey_hook';
|
||||
import { Wysiwyg } from "@web_editor/js/wysiwyg/wysiwyg";
|
||||
import weUtils from '@web_editor/js/common/utils';
|
||||
import { isMediaElement } from '@web_editor/js/editor/odoo-editor/src/utils/utils';
|
||||
import { cloneContentEls } from "@website/js/utils";
|
||||
|
||||
import { EditMenuDialog, MenuDialog } from "../dialog/edit_menu";
|
||||
import { WebsiteDialog } from '../dialog/dialog';
|
||||
@@ -904,11 +905,27 @@ export class WysiwygAdapterComponent extends Wysiwyg {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _saveElement($el, context, withLang) {
|
||||
async _saveElement($el, context, withLang, ...rest) {
|
||||
var promises = [];
|
||||
|
||||
// Saving a view content
|
||||
await super._saveElement(...arguments);
|
||||
// Saving Embed Code snippets with <script> in the database, as these
|
||||
// elements are removed in edit mode.
|
||||
if ($el[0].querySelector(".s_embed_code")) {
|
||||
// Copied so as not to impact the actual DOM and prevent scripts
|
||||
// from loading.
|
||||
const $clonedEl = $el.clone(true, true);
|
||||
for (const embedCodeEl of $clonedEl[0].querySelectorAll(".s_embed_code")) {
|
||||
const embedTemplateEl = embedCodeEl.querySelector(".s_embed_code_saved");
|
||||
if (embedTemplateEl) {
|
||||
embedCodeEl.querySelector(".s_embed_code_embedded")
|
||||
.replaceChildren(cloneContentEls(embedTemplateEl.content, true));
|
||||
}
|
||||
}
|
||||
await super._saveElement($clonedEl, context, withLang, ...rest);
|
||||
} else {
|
||||
// Saving a view content
|
||||
await super._saveElement(...arguments);
|
||||
}
|
||||
|
||||
// Saving mega menu options
|
||||
if ($el.data('oe-field') === 'mega_menu_content') {
|
||||
|
||||
@@ -438,6 +438,29 @@ function getParsedDataFor(formId, parentEl) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clones children or parses a string into elements, with or without
|
||||
* <script> elements.
|
||||
*
|
||||
* @param {DocumentFragment|HTMLElement|String} content
|
||||
* @param {Boolean} [keepScripts=false] - whether to keep script tags or not.
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
export function cloneContentEls(content, keepScripts = false) {
|
||||
let copyFragment;
|
||||
if (typeof content === "string") {
|
||||
copyFragment = new Range().createContextualFragment(content);
|
||||
} else {
|
||||
copyFragment = new DocumentFragment();
|
||||
const els = [...content.children].map(el => el.cloneNode(true));
|
||||
copyFragment.append(...els);
|
||||
}
|
||||
if (!keepScripts) {
|
||||
copyFragment.querySelectorAll("script").forEach(scriptEl => scriptEl.remove());
|
||||
}
|
||||
return copyFragment;
|
||||
}
|
||||
|
||||
export default {
|
||||
loadAnchors: loadAnchors,
|
||||
autocompleteWithPages: autocompleteWithPages,
|
||||
@@ -452,4 +475,5 @@ export default {
|
||||
generateGMapLink: generateGMapLink,
|
||||
isMobile: isMobile,
|
||||
getParsedDataFor: getParsedDataFor,
|
||||
cloneContentEls: cloneContentEls,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { cloneContentEls } from "@website/js/utils";
|
||||
|
||||
const EmbedCodeWidget = publicWidget.Widget.extend({
|
||||
selector: ".s_embed_code",
|
||||
disabledInEditableMode: false,
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async start() {
|
||||
this.embedCodeEl = this.el.querySelector(".s_embed_code_embedded");
|
||||
|
||||
// TODO this should not be the widget's job to create this element but
|
||||
// the job of the related editor option. This was done as a stable fix
|
||||
// and we should make an upgrade script in master to be able to get rid
|
||||
// of this code.
|
||||
if (!this.el.querySelector("template.s_embed_code_saved")) {
|
||||
const templateEl = document.createElement("template");
|
||||
templateEl.classList.add("s_embed_code_saved");
|
||||
templateEl.content.append(cloneContentEls(this.embedCodeEl, true));
|
||||
this.el.prepend(templateEl);
|
||||
}
|
||||
|
||||
if (this.editableMode && this.embedCodeEl.offsetHeight === 0) {
|
||||
// Shows a placeholder message in edit mode to be able to select
|
||||
// the snippet if it's visually empty.
|
||||
const placeholderEl = document.createElement("div");
|
||||
placeholderEl.classList
|
||||
.add("s_embed_code_placeholder", "alert", "alert-info", "pt16", "pb16");
|
||||
placeholderEl.textContent = _t("Your Embed Code snippet doesn't have anything to display. Click on Edit to modify it.");
|
||||
this.el.querySelector(".s_embed_code_embedded").appendChild(placeholderEl);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
this._super(...arguments);
|
||||
|
||||
// Just before entering edit mode, reinitialize the snippet's content,
|
||||
// without <script> elements. This is both done so that scripts don't
|
||||
// affect the DOM in edit mode, and to remove elements that would have
|
||||
// been introduced by a script.
|
||||
if (!this.editableMode) {
|
||||
const templateContent = this.el.querySelector("template.s_embed_code_saved").content;
|
||||
this.embedCodeEl.replaceChildren(cloneContentEls(templateContent));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
publicWidget.registry.EmbedCode = EmbedCodeWidget;
|
||||
|
||||
export default EmbedCodeWidget;
|
||||
@@ -1,8 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { CodeEditor } from "@web/core/code_editor/code_editor";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import options from '@web_editor/js/editor/snippets.options';
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { EditHeadBodyDialog } from "@website/components/edit_head_body_dialog/edit_head_body_dialog";
|
||||
import { cloneContentEls } from "@website/js/utils";
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
@@ -10,6 +13,7 @@ class CodeEditorDialog extends Component {
|
||||
static template = "website.s_embed_code_dialog";
|
||||
static components = { Dialog, CodeEditor };
|
||||
setup() {
|
||||
this.dialog = useService("dialog");
|
||||
this.state = useState({ value: this.props.value });
|
||||
}
|
||||
onCodeChange(newValue) {
|
||||
@@ -19,6 +23,10 @@ class CodeEditorDialog extends Component {
|
||||
this.props.confirm(this.state.value);
|
||||
this.props.close();
|
||||
}
|
||||
onInjectHeadOrBody() {
|
||||
this.dialog.add(EditHeadBodyDialog);
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
|
||||
options.registry.EmbedCode = options.Class.extend({
|
||||
@@ -32,15 +40,20 @@ options.registry.EmbedCode = options.Class.extend({
|
||||
|
||||
async editCode() {
|
||||
const $container = this.$target.find('.s_embed_code_embedded');
|
||||
const code = $container.html().trim();
|
||||
const templateEl = this.$target[0].querySelector("template.s_embed_code_saved");
|
||||
const embedContent = templateEl.innerHTML.trim();
|
||||
|
||||
await new Promise(resolve => {
|
||||
this.dialog.add(CodeEditorDialog, {
|
||||
title: _t("Edit embedded code"),
|
||||
value: code,
|
||||
value: embedContent,
|
||||
mode: "xml",
|
||||
confirm: (newValue) => {
|
||||
$container[0].innerHTML = newValue;
|
||||
// Removes scripts tags from the DOM as we don't want them
|
||||
// to interfere during edition, but keeps them in a
|
||||
// `<template>` that will be saved to the database.
|
||||
templateEl.content.replaceChildren(cloneContentEls(newValue, true));
|
||||
$container[0].replaceChildren(cloneContentEls(newValue));
|
||||
}
|
||||
}, {
|
||||
onClose: resolve,
|
||||
|
||||
@@ -148,6 +148,12 @@
|
||||
<!-- Theme - custom code -->
|
||||
<t t-name="website.s_embed_code_dialog">
|
||||
<Dialog title="props.title">
|
||||
<p class="h5">Do not copy/paste code you do not understand, this could put your data at risk.</p>
|
||||
<p>
|
||||
<button class="btn btn-link ps-0" t-on-click="onInjectHeadOrBody">
|
||||
If you need to add analytics or marketing tags, inject code in your <head> or <body> instead.
|
||||
</button>
|
||||
</p>
|
||||
<CodeEditor mode="props.mode"
|
||||
class="'o_website_code_editor_field'"
|
||||
theme="'monokai'"
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Snippet assets -->
|
||||
<!-- TODO: create a new ir.asset for s_embed_code_000_js in master. -->
|
||||
|
||||
<record id="website.s_embed_code_000_scss" model="ir.asset">
|
||||
<field name="name">Embed Code 000 SCSS</field>
|
||||
<field name="bundle">web.assets_frontend</field>
|
||||
|
||||
Reference in New Issue
Block a user