diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index e16e65db888..a7069fbabe0 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -61,7 +61,8 @@ class AuthSignupHome(Home): qcontext['error'] = _("Could not create a new account.") response = request.render('auth_signup.signup', qcontext) - response.headers['X-Frame-Options'] = 'DENY' + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['Content-Security-Policy'] = "frame-ancestors 'self'" return response @http.route('/web/reset_password', type='http', auth='public', website=True, sitemap=False) @@ -93,7 +94,8 @@ class AuthSignupHome(Home): qcontext['error'] = str(e) response = request.render('auth_signup.reset_password', qcontext) - response.headers['X-Frame-Options'] = 'DENY' + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['Content-Security-Policy'] = "frame-ancestors 'self'" return response def get_auth_signup_config(self): diff --git a/addons/knowledge/static/src/components/media_dialog/knowledge_documents_selector.js b/addons/knowledge/static/src/components/media_dialog/knowledge_documents_selector.js new file mode 100644 index 00000000000..13be1ac4d6f --- /dev/null +++ b/addons/knowledge/static/src/components/media_dialog/knowledge_documents_selector.js @@ -0,0 +1,70 @@ +/** @odoo-module */ + +import { qweb as QWeb } from 'web.core'; +import { DocumentSelector } from '@web_editor/components/media_dialog/document_selector'; +import { TABS, MediaDialog } from '@web_editor/components/media_dialog/media_dialog'; +import { patch } from '@web/core/utils/patch'; + +/** + * Override the @see DocumentSelector to manage files in a @see MediaDialog used + * by the /file command. The purpose of this override is to redefine the + * rendering of the media (/file block), and to merge images in the documents + * tab of the MediaDialog, since the /file block displays a default mimetype for + * every files. + */ +export class KnowledgeDocumentSelector extends DocumentSelector { + /** + * Filter files for the documents tab of the MediaDialog. Any file with a + * mimetype is valid. (images and documents are displayed together) + * + * @override + */ + get attachmentsDomain() { + const domain = super.attachmentsDomain; + return domain.map(d => { + if (d[0] === 'mimetype') { + return ['mimetype', '!=', false]; + } + return d; + }); + } + + static async createElements(...args) { + const files = await DocumentSelector.createElements(...args); + return files.map(file => { + const extension = (file.title && file.title.split('.').pop()) || file.dataset.mimetype; + const fileEl = $(QWeb.render('knowledge.file_block', { + img: { + name: file.title, + extension: extension, + }, + }))[0]; + file.classList.add(...DocumentSelector.mediaSpecificClasses); + fileEl.querySelector('.o_knowledge_file_image').replaceChildren(file); + return fileEl; + }); + } +} +KnowledgeDocumentSelector.mediaSpecificClasses = []; +KnowledgeDocumentSelector.tagNames = DocumentSelector.tagNames; + +patch(TABS, 'knowledge_media_dialog_tabs', { + KNOWLEDGE_DOCUMENTS: { + ...TABS.DOCUMENTS, + id: 'KNOWLEDGE_DOCUMENTS', + Component: KnowledgeDocumentSelector, + } +}); + +patch(MediaDialog.prototype, 'knowledge_media_dialog', { + /** + * @override + */ + addTabs() { + this._super(...arguments); + + if (this.props.knowledgeDocuments) { + this.addTab(TABS.KNOWLEDGE_DOCUMENTS); + } + } +}); diff --git a/addons/knowledge/static/src/js/wysiwyg.js b/addons/knowledge/static/src/js/wysiwyg.js index cae48694fd8..f2f582e4c3b 100644 --- a/addons/knowledge/static/src/js/wysiwyg.js +++ b/addons/knowledge/static/src/js/wysiwyg.js @@ -1,73 +1,10 @@ /** @odoo-module **/ import { qweb as QWeb } from 'web.core'; -import { DocumentWidget } from 'wysiwyg.widgets.media'; -import MediaDialog from 'wysiwyg.widgets.MediaDialog'; import Wysiwyg from 'web_editor.wysiwyg'; import { KnowledgeArticleLinkModal } from './wysiwyg/knowledge_article_link.js'; import { preserveCursor, setCursorStart } from '@web_editor/../lib/odoo-editor/src/OdooEditor'; -/** - * Override the @see DocumentWidget to manage files in a @see MediaDialog used - * by the /file command. The purpose of this override is to redefine the - * rendering of the media (/file block), and to merge images in the documents - * tab of the MediaDialog, since the /file block displays a default mimetype for - * every files. - */ -const KnowledgeDocumentWidget = DocumentWidget.extend({ - /** - * Filter files for the documents tab of the MediaDialog. Any file with a - * mimetype is valid. (images and documents are displayed together) - * - * @override - */ - init: function (parent, media, options) { - options = _.extend({ - accept: '*/*', - mimetypeDomain: [['mimetype', '!=', false]], - }, options || {}); - this._super(parent, media, options); - }, - /** - * Custom rendering for the /file command - * - * @override - * @param {Object} img - * @returns {Element} - */ - _renderMedia: function (img) { - if (img.image_src) { - delete img.image_src; - } - const file = this._super(...arguments); - const extension = (img.name && img.name.split('.').pop()) || img.mimetype; - this.$media = $(QWeb.render('knowledge.file_block', { - img: { - name: img.name, - extension: extension, - }, - })); - this.media = this.$media[0]; - this.media.querySelector('.o_knowledge_file_image').replaceChildren(file); - return this.media; - } -}); - -/** - * Override the @see MediaDialog to manage documents with the - * @see KnowledgeDocumentWidget for the /file command - */ -const KnowledgeMediaDialog = MediaDialog.extend({ - /** - * @override - * @param {Object} media - * @param {Object} options - * @returns {Widget} - */ - getDocumentWidget: function (media, options) { - return new KnowledgeDocumentWidget(this, media, options); - } -}); Wysiwyg.include({ /** * @override @@ -131,8 +68,9 @@ Wysiwyg.include({ noVideos: true, noImages: true, noIcons: true, - noDocuments: false - }, KnowledgeMediaDialog); + noDocuments: true, + knowledgeDocuments: true, + }); } }, { groupName: 'Knowledge', diff --git a/addons/mass_mailing/static/src/scss/mass_mailing.ui.scss b/addons/mass_mailing/static/src/scss/mass_mailing.ui.scss index 008a8631c25..c67f47e21f7 100644 --- a/addons/mass_mailing/static/src/scss/mass_mailing.ui.scss +++ b/addons/mass_mailing/static/src/scss/mass_mailing.ui.scss @@ -336,6 +336,9 @@ body.editor_enable.o_basic_theme.o_in_iframe { display: block; } } + #oe_snippets.o_loaded { + transform: translateX(0); + } } diff --git a/addons/mass_mailing/static/src/snippets/s_rating/options.js b/addons/mass_mailing/static/src/snippets/s_rating/options.js index ed6e493bccc..fc798052a5c 100644 --- a/addons/mass_mailing/static/src/snippets/s_rating/options.js +++ b/addons/mass_mailing/static/src/snippets/s_rating/options.js @@ -1,7 +1,8 @@ odoo.define('mass_mailing.s_rating_options', function (require) { 'use strict'; -const weWidgets = require('wysiwyg.widgets'); +const { ComponentWrapper } = require('web.OwlCompatibility'); +const { MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog'); const options = require('web_editor.snippets.options'); options.registry.Rating = options.Class.extend({ @@ -37,16 +38,15 @@ options.registry.Rating = options.Class.extend({ * @see this.selectClass for parameters */ customIcon: async function (previewMode, widgetValue, params) { - return new Promise(resolve => { - const dialog = new weWidgets.MediaDialog( - this, - {noImages: true, noDocuments: true, noVideos: true, mediaWidth: 1920}, - $('') - ); - this._saving = false; - dialog.on('save', this, function (attachments) { - this._saving = true; - const customClass = 'fa ' + attachments.className; + const media = document.createElement('i'); + media.className = params.customActiveIcon === 'true' ? this.faClassActiveCustomIcons : this.faClassInactiveCustomIcons; + const dialog = new ComponentWrapper(this, MediaDialogWrapper, { + noImages: true, + noDocuments: true, + noVideos: true, + media, + save: icon => { + const customClass = icon.className; const $activeIcons = this.$target.find('.s_rating_active_icons > i'); const $inactiveIcons = this.$target.find('.s_rating_inactive_icons > i'); const $icons = params.customActiveIcon === 'true' ? $activeIcons : $inactiveIcons; @@ -57,15 +57,9 @@ options.registry.Rating = options.Class.extend({ this.$target[0].dataset.inactiveCustomIcon = this.faClassInactiveCustomIcons; this.$target[0].dataset.icon = 'custom'; this.iconType = 'custom'; - resolve(); - }); - dialog.on('closed', this, function () { - if (!this._saving) { - resolve(); - } - }); - dialog.open(); + } }); + dialog.mount(document.body); }, /** * Sets the number of active icons. diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py index 880afa26318..07505196952 100644 --- a/addons/portal/controllers/portal.py +++ b/addons/portal/controllers/portal.py @@ -213,7 +213,8 @@ class CustomerPortal(Controller): }) response = request.render("portal.portal_my_details", values) - response.headers['X-Frame-Options'] = 'DENY' + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['Content-Security-Policy'] = "frame-ancestors 'self'" return response def on_account_update(self, values, partner): @@ -233,7 +234,8 @@ class CustomerPortal(Controller): )) return request.render('portal.portal_my_security', values, headers={ - 'X-Frame-Options': 'DENY' + 'X-Frame-Options': 'SAMEORIGIN', + 'Content-Security-Policy': "frame-ancestors 'self'" }) def _update_password(self, old, new1, new2): diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index eb920ce3717..e998d926ec8 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -93,6 +93,9 @@
+ + Apps + diff --git a/addons/survey/static/src/scss/survey_templates_form.scss b/addons/survey/static/src/scss/survey_templates_form.scss index 8257fbb0342..3e2c5a976d9 100644 --- a/addons/survey/static/src/scss/survey_templates_form.scss +++ b/addons/survey/static/src/scss/survey_templates_form.scss @@ -1,13 +1,10 @@ /********************************************************** - Remove website navbar : Should be done in website survey - but we won't do a bridge module only for this. + Remove website backend redirection button : Should be + done in website survey but we won't do a bridge module + only for this. TODO: SmartPeople Fixme - cleaner solution? be my guest! **********************************************************/ -body.o_connected_user { - padding-top: 0px !important; -} - -nav#oe_main_menu_navbar { +nav.o_frontend_to_backend_nav { display: none !important; } diff --git a/addons/test_website/data/test_website_data.xml b/addons/test_website/data/test_website_data.xml index 13458ee61ae..3217fcf42f1 100644 --- a/addons/test_website/data/test_website_data.xml +++ b/addons/test_website/data/test_website_data.xml @@ -122,9 +122,6 @@ test_website.test_image_progress - - -
diff --git a/addons/test_website/static/src/js/mock_image_widget.js b/addons/test_website/static/src/js/mock_image_widget.js deleted file mode 100644 index 9e2afd1c2a0..00000000000 --- a/addons/test_website/static/src/js/mock_image_widget.js +++ /dev/null @@ -1,41 +0,0 @@ -odoo.define('test_website.mock_image_widgets', function (require) { -'use strict'; - -const widgetsMedia = require('wysiwyg.widgets.media'); - -widgetsMedia.FileWidget.include({ - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - _onFileInputChange: function () { - function getFileFromB64(fileData) { - const binary = atob(fileData[2]); - let len = binary.length; - const arr = new Uint8Array(len); - while (len--) { - arr[len] = binary.charCodeAt(len); - } - return new File([arr], fileData[1], {type: fileData[0]}); - } - - let files = [ - getFileFromB64(['image/png', 'image.png', "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAApElEQVR42u3RAQ0AAAjDMO5fNCCDkC5z0HTVrisFCBABASIgQAQEiIAAAQJEQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAQECBAgAgJEQIAIyPcGFY7HnV2aPXoAAAAASUVORK5CYII="]), - getFileFromB64(['image/jpeg', 'image.jpeg', "/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAA0NDQ0ODQ4QEA4UFhMWFB4bGRkbHi0gIiAiIC1EKjIqKjIqRDxJOzc7STxsVUtLVWx9aWNpfZeHh5e+tb75+f8BDQ0NDQ4NDhAQDhQWExYUHhsZGRseLSAiICIgLUQqMioqMipEPEk7NztJPGxVS0tVbH1pY2l9l4eHl761vvn5///CABEIAEsASwMBIgACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAABv/aAAgBAQAAAACHAAAAAAAAAAAAAAAAH//EABUBAQEAAAAAAAAAAAAAAAAAAAAH/9oACAECEAAAAKYAAAB//8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/2gAIAQMQAAAAngAAAf/EABQQAQAAAAAAAAAAAAAAAAAAAGD/2gAIAQEAAT8ASf/EABQRAQAAAAAAAAAAAAAAAAAAAED/2gAIAQIBAT8AT//EABQRAQAAAAAAAAAAAAAAAAAAAED/2gAIAQMBAT8AT//Z"]), - getFileFromB64(['image/vnd.microsoft.icon', 'icon.ico', "AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA=="]), - getFileFromB64(['image/webp', 'image.webp', "UklGRhwAAABXRUJQVlA4TBAAAAAvE8AEAAfQhuh//wMR0f8A"]), - ]; - - if (!this.options.multiImages) { - if (this.media.classList.contains('o_mock_show_error')) { - files = [files[2]]; - } else { - files = [files[0]]; - } - } - this.$fileInput = [{'files': files}]; - return this._super(...arguments); - } -}); -}); diff --git a/addons/test_website/static/tests/tours/custom_snippets.js b/addons/test_website/static/tests/tours/custom_snippets.js index 682776ce7c9..ebbce117ee4 100644 --- a/addons/test_website/static/tests/tours/custom_snippets.js +++ b/addons/test_website/static/tests/tours/custom_snippets.js @@ -1,7 +1,7 @@ odoo.define('test_website.custom_snippets', function (require) { 'use strict'; -var tour = require('web_tour.tour'); +const wTourUtils = require('website.tour_utils'); /** * The purpose of this tour is to check the custom snippets flow: @@ -22,24 +22,21 @@ var tour = require('web_tour.tour'); * -> ensure it was deleted */ -tour.register('test_custom_snippet', { +wTourUtils.registerEditionTour('test_custom_snippet', { url: '/', - test: true + edition: true, + test: true, }, [ - { - content: "enter edit mode", - trigger: "a[data-action=edit]" - }, { content: "drop a snippet", - trigger: "#oe_snippets .oe_snippet[name='Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", - extra_trigger: "body.editor_enable.editor_has_snippets", + trigger: "#oe_snippets.o_loaded .oe_snippet[name='Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", + extra_trigger: "iframe body.editor_enable", moveTrigger: ".oe_drop_zone", - run: "drag_and_drop #wrap", + run: "drag_and_drop iframe #wrap", }, { content: "customize snippet", - trigger: "#wrapwrap .s_banner h1", + trigger: "iframe #wrapwrap .s_banner h1", run: "text", consumeEvent: "input", }, @@ -53,7 +50,7 @@ tour.register('test_custom_snippet', { }, { content: "ensure custom snippet appeared", - trigger: "#oe_snippets .oe_snippet[name='Custom Banner']", + trigger: "#oe_snippets.o_loaded .oe_snippet[name='Custom Banner']", run: function () { $("#oe_snippets .oe_snippet[name='Custom Banner'] .o_rename_btn").attr("style", "display: block;"); // hover is needed for rename button to appear @@ -76,18 +73,18 @@ tour.register('test_custom_snippet', { { content: "drop custom snippet", trigger: ".oe_snippet[name='Bruce Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", - extra_trigger: "body.editor_enable.editor_has_snippets", + extra_trigger: "iframe body.editor_enable", moveTrigger: ".oe_drop_zone", - run: "drag_and_drop #wrap", + run: "drag_and_drop iframe #wrap", }, { content: "ensure banner section exists", - trigger: "#wrap section[data-name='Banner']", + trigger: "iframe #wrap section[data-name='Banner']", run: function () {}, // check }, { content: "ensure custom banner section exists", - trigger: "#wrap section[data-name='Bruce Banner']", + trigger: "iframe #wrap section[data-name='Bruce Banner']", run: function () { $("#oe_snippets .oe_snippet[name='Bruce Banner'] .o_delete_btn").attr("style", "display: block;"); // hover is needed for delete button to appear diff --git a/addons/test_website/static/tests/tours/image_upload_progress.js b/addons/test_website/static/tests/tours/image_upload_progress.js index 4976752aeb9..c5b31a31337 100644 --- a/addons/test_website/static/tests/tours/image_upload_progress.js +++ b/addons/test_website/static/tests/tours/image_upload_progress.js @@ -1,36 +1,75 @@ odoo.define('test_website.image_upload_progress', function (require) { 'use strict'; -const tour = require('web_tour.tour'); +const wTourUtils = require('website.tour_utils'); + +const { FileSelectorControlPanel } = require('@web_editor/components/media_dialog/file_selector'); +const { patch, unpatch } = require('web.utils'); + +let patchWithError = false; +const patchMediaDialog = () => patch(FileSelectorControlPanel.prototype, 'test_website.mock_image_widgets', { + async onChangeFileInput() { + const getFileFromB64 = (fileData) => { + const binary = atob(fileData[2]); + let len = binary.length; + const arr = new Uint8Array(len); + while (len--) { + arr[len] = binary.charCodeAt(len); + } + return new File([arr], fileData[1], {type: fileData[0]}); + }; + + let files = [ + getFileFromB64(['image/vnd.microsoft.icon', 'icon.ico', "AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA=="]), + getFileFromB64(['image/webp', 'image.webp', "UklGRhwAAABXRUJQVlA4TBAAAAAvE8AEAAfQhuh//wMR0f8A"]), + getFileFromB64(['image/png', 'image.png', "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAApElEQVR42u3RAQ0AAAjDMO5fNCCDkC5z0HTVrisFCBABASIgQAQEiIAAAQJEQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAREQIAICBABASIgQAQECBAgAgJEQIAIyPcGFY7HnV2aPXoAAAAASUVORK5CYII="]), + getFileFromB64(['image/jpeg', 'image.jpeg', "/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAA0NDQ0ODQ4QEA4UFhMWFB4bGRkbHi0gIiAiIC1EKjIqKjIqRDxJOzc7STxsVUtLVWx9aWNpfZeHh5e+tb75+f8BDQ0NDQ4NDhAQDhQWExYUHhsZGRseLSAiICIgLUQqMioqMipEPEk7NztJPGxVS0tVbH1pY2l9l4eHl761vvn5///CABEIAEsASwMBIgACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAABv/aAAgBAQAAAACHAAAAAAAAAAAAAAAAH//EABUBAQEAAAAAAAAAAAAAAAAAAAAH/9oACAECEAAAAKYAAAB//8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/2gAIAQMQAAAAngAAAf/EABQQAQAAAAAAAAAAAAAAAAAAAGD/2gAIAQEAAT8ASf/EABQRAQAAAAAAAAAAAAAAAAAAAED/2gAIAQIBAT8AT//EABQRAQAAAAAAAAAAAAAAAAAAAED/2gAIAQMBAT8AT//Z"]), + ]; + + if (!this.props.multiSelect) { + if (patchWithError) { + files = [files[0]]; + } else { + files = [files[2]]; + } + } + await this.props.uploadFiles(files); + } +}); + +const unpatchMediaDialog = () => unpatch(FileSelectorControlPanel.prototype, 'test_website.mock_image_widgets'); const setupSteps = [{ - content: "enter edit mode", - trigger: "a[data-action=edit]" + content: "reload to load patch", + trigger: ".o_website_preview", + run: () => { + patchMediaDialog(); + }, }, { content: "drop a snippet", trigger: "#oe_snippets .oe_snippet[name='Text - Image'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", - extra_trigger: "body.editor_enable.editor_has_snippets", - moveTrigger: ".oe_drop_zone", - run: "drag_and_drop #wrap", + moveTrigger: "iframe .oe_drop_zone", + run: "drag_and_drop iframe #wrap", }, { content: "drop a snippet", trigger: "#oe_snippets .oe_snippet[name='Image Gallery'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", - extra_trigger: "body.editor_enable.editor_has_snippets", + extra_trigger: "body.editor_has_snippets", moveTrigger: ".oe_drop_zone", - run: "drag_and_drop #wrap", + run: "drag_and_drop iframe #wrap", }]; const formatErrorMsg = "format is not supported. Try with: .gif, .jpe, .jpeg, .jpg, .png, .svg"; -tour.register('test_image_upload_progress', { +wTourUtils.registerEditionTour('test_image_upload_progress', { url: '/test_image_progress', - test: true + test: true, + edition: true, }, [ ...setupSteps, // 1. Check multi image upload { content: "click on dropped snippet", - trigger: "#wrap .s_image_gallery .img", + trigger: "iframe #wrap .s_image_gallery .img", }, { content: "click on add images to open image dialog (in multi mode)", trigger: 'we-customizeblock-option [data-add-images]', @@ -38,11 +77,9 @@ tour.register('test_image_upload_progress', { content: "manually trigger input change", trigger: ".o_select_media_dialog .o_upload_media_button", run: () => { - const fileInput = $('.o_select_media_dialog .o_file_input').first(); // This will trigger upload of dummy files for test purpose, as a // test can't select local files to upload into the input. - // See `mock_image_widgets`. - fileInput.change(); + document.body.querySelector('.o_select_media_dialog .o_file_input').dispatchEvent(new Event('change')); }, }, { content: "check upload progress bar is correctly shown (1)", @@ -85,20 +122,17 @@ tour.register('test_image_upload_progress', { // 2. Check success single image upload { content: "click on dropped snippet", - trigger: "#wrap .s_text_image .img", + trigger: "iframe #wrap .s_text_image .img", }, { content: "click on replace media to open image dialog", trigger: 'we-customizeblock-option [data-replace-media]', }, { content: "manually trigger input change", trigger: ".o_select_media_dialog .o_upload_media_button", - in_modal: false, run: () => { - const fileInput = $('.o_select_media_dialog .o_file_input').first(); // This will trigger upload of dummy files for test purpose, as a // test can't select local files to upload into the input. - // See `mock_image_widgets`. - fileInput.change(); + document.body.querySelector('.o_select_media_dialog .o_file_input').dispatchEvent(new Event('change')); }, }, { content: "check upload progress bar is correctly shown", @@ -126,7 +160,7 @@ tour.register('test_image_upload_progress', { // 3. Check error single image upload { content: "click on dropped snippet", - trigger: "#wrap .s_text_image .img", + trigger: "iframe #wrap .s_text_image .img", }, { content: "click on replace media to open image dialog", trigger: 'we-customizeblock-option [data-replace-media]', @@ -135,18 +169,19 @@ tour.register('test_image_upload_progress', { trigger: ".o_select_media_dialog .o_upload_media_button", in_modal: false, run: () => { - $("#wrap .s_text_image .img").addClass('o_mock_show_error'); - const fileInput = $('.o_select_media_dialog .o_file_input').first(); + patchWithError = true; // This will trigger upload of dummy files for test purpose, as a // test can't select local files to upload into the input. - // See `mock_image_widgets`. - fileInput.change(); + document.body.querySelector('.o_select_media_dialog .o_file_input').dispatchEvent(new Event('change')); + }, }, { content: "check upload progress bar is correctly shown", trigger: `.o_we_progressbar:contains('icon.ico'):contains('${formatErrorMsg}')`, in_modal: false, - run: function () {}, // it's a check + run: function () { + patchWithError = false; + }, }, { content: "there should only have one notification toaster", trigger: ".o_notification", @@ -156,20 +191,22 @@ tour.register('test_image_upload_progress', { if (notificationCount !== 1) { console.error("There should be one noficiation toaster opened, and only one."); } + unpatchMediaDialog(); } }, ]); -tour.register('test_image_upload_progress_unsplash', { +wTourUtils.registerEditionTour('test_image_upload_progress_unsplash', { url: '/test_image_progress', - test: true + test: true, + edition: true, }, [ ...setupSteps, // 1. Check multi image upload { content: "click on dropped snippet", - trigger: "#wrap .s_image_gallery .img", + trigger: "iframe #wrap .s_image_gallery .img", }, { content: "click on replace media to open image dialog", trigger: 'we-customizeblock-option [data-replace-media]', @@ -179,7 +216,7 @@ tour.register('test_image_upload_progress_unsplash', { run: "text fox", }, { content: "click on unsplash result", // note that unsplash is mocked - trigger: ".o_unsplash_attachment_cell" + trigger: "img[alt~=fox]" }, { content: "check that the upload progress bar is correctly shown", // ensure it is there so we are sure next step actually test something @@ -193,7 +230,10 @@ tour.register('test_image_upload_progress_unsplash', { in_modal: false, }, { content: "unsplash image (mocked to logo) should have been used", - trigger: "#wrap .s_image_gallery .img[data-original-src^='/unsplash/HQqIOc8oYro/fox']", + trigger: "iframe #wrap .s_image_gallery .img[data-original-src^='/unsplash/HQqIOc8oYro/fox']", + run: () => { + unpatchMediaDialog(); + }, }, ]); diff --git a/addons/test_website/static/tests/tours/reset_views.js b/addons/test_website/static/tests/tours/reset_views.js index d379c776908..cee8bc06498 100644 --- a/addons/test_website/static/tests/tours/reset_views.js +++ b/addons/test_website/static/tests/tours/reset_views.js @@ -2,7 +2,7 @@ odoo.define('test_website.reset_views', function (require) { 'use strict'; -var tour = require("web_tour.tour"); +const wTourUtils = require('website.tour_utils'); var BROKEN_STEP = { // because saving a broken template opens a recovery page with no assets @@ -13,36 +13,33 @@ var BROKEN_STEP = { trigger: 'body', run: function () {} }; -tour.register('test_reset_page_view_complete_flow_part1', { +wTourUtils.registerEditionTour('test_reset_page_view_complete_flow_part1', { test: true, url: '/test_page_view', + // 1. Edit the page through Edit Mode, it will COW the view + edition: true, }, [ - // 1. Edit the page through Edit Mode, it will COW the view - { - content: "enter edit mode", - trigger: "a[data-action=edit]" - }, { content: "drop a snippet", - trigger: "#oe_snippets .oe_snippet:has(.s_cover) .oe_snippet_thumbnail", + trigger: "#oe_snippets.o_loaded .oe_snippet:has(.s_cover) .oe_snippet_thumbnail", // id starting by 'oe_structure..' will actually create an inherited view - run: "drag_and_drop #oe_structure_test_website_page", + run: "drag_and_drop iframe #oe_structure_test_website_page", }, { content: "save the page", - extra_trigger: '#oe_structure_test_website_page.o_dirty', + extra_trigger: 'iframe #oe_structure_test_website_page.o_dirty', trigger: "button[data-action=save]", }, // 2. Edit that COW'd view in the HTML editor to break it. { - content: "open customize menu", - extra_trigger: "body:not(.editor_enable)", - trigger: '#customize-menu > a', + content: "open site menu", + extra_trigger: "iframe body:not(.editor_enable)", + trigger: 'button[data-menu-xmlid="website.menu_site"]', }, { content: "open html editor", - trigger: '#html_editor', + trigger: 'a[data-menu-xmlid="website.menu_ace_editor"]', }, { content: "add a broken t-field in page DOM", @@ -60,29 +57,29 @@ tour.register('test_reset_page_view_complete_flow_part1', { ] ); -tour.register('test_reset_page_view_complete_flow_part2', { +wTourUtils.registerEditionTour('test_reset_page_view_complete_flow_part2', { test: true, url: '/test_page_view', }, [ { content: "check that the view got fixed", - trigger: 'p:containsExact("Test Page View")', + trigger: 'iframe p:containsExact("Test Page View")', run: function () {}, // it's a check }, { content: "check that the inherited COW view is still there (created during edit mode)", - trigger: '#oe_structure_test_website_page .s_cover', + trigger: 'iframe #oe_structure_test_website_page .s_cover', run: function () {}, // it's a check }, //4. Now break the inherited view created when dropping a snippet { - content: "open customize menu", - trigger: '#customize-menu > a', + content: "open site menu", + trigger: 'button[data-menu-xmlid="website.menu_site"]', }, { content: "open html editor", - trigger: '#html_editor', + trigger: 'a[data-menu-xmlid="website.menu_ace_editor"]', }, { content: "select oe_structure view", diff --git a/addons/test_website/tests/test_custom_snippet.py b/addons/test_website/tests/test_custom_snippet.py index c79ed227dc0..d5adc0c9a65 100644 --- a/addons/test_website/tests/test_custom_snippet.py +++ b/addons/test_website/tests/test_custom_snippet.py @@ -10,4 +10,4 @@ class TestCustomSnippet(odoo.tests.HttpCase): @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http') def test_01_run_tour(self): - self.start_tour("/", 'test_custom_snippet', login="admin") + self.start_tour(self.env['website'].get_client_action_url('/'), 'test_custom_snippet', login="admin") diff --git a/addons/test_website/tests/test_image_upload_progress.py b/addons/test_website/tests/test_image_upload_progress.py index 4d03faa6a59..13beecc5422 100644 --- a/addons/test_website/tests/test_image_upload_progress.py +++ b/addons/test_website/tests/test_image_upload_progress.py @@ -13,7 +13,7 @@ from odoo import http class TestImageUploadProgress(odoo.tests.HttpCase): def test_01_image_upload_progress(self): - self.start_tour("/test_image_progress", 'test_image_upload_progress', login="admin") + self.start_tour(self.env['website'].get_client_action_url('/test_image_progress'), 'test_image_upload_progress', login="admin") def test_02_image_upload_progress_unsplash(self): BASE_URL = self.base_url() @@ -53,4 +53,4 @@ class TestImageUploadProgress(odoo.tests.HttpCase): media_library_search.routing_type = 'json' Web_Editor.media_library_search = http.route(['/web_editor/media_library_search'], type='json', auth="user", website=True)(media_library_search) - self.start_tour("/", 'test_image_upload_progress_unsplash', login="admin") + self.start_tour(self.env['website'].get_client_action_url('/test_image_progress'), 'test_image_upload_progress_unsplash', login="admin") diff --git a/addons/test_website/tests/test_reset_views.py b/addons/test_website/tests/test_reset_views.py index 2d8bf7fe656..34d33ef7453 100644 --- a/addons/test_website/tests/test_reset_views.py +++ b/addons/test_website/tests/test_reset_views.py @@ -90,9 +90,9 @@ class TestWebsiteResetViews(odoo.tests.HttpCase): @mute_logger('odoo.http') def test_07_reset_page_view_complete_flow(self): - self.start_tour("/", 'test_reset_page_view_complete_flow_part1', login="admin") + self.start_tour(self.env['website'].get_client_action_url('/test_page_view'), 'test_reset_page_view_complete_flow_part1', login="admin") self.fix_it('/test_page_view') - self.start_tour("/", 'test_reset_page_view_complete_flow_part2', login="admin") + self.start_tour(self.env['website'].get_client_action_url('/test_page_view'), 'test_reset_page_view_complete_flow_part2', login="admin") self.fix_it('/test_page_view') @mute_logger('odoo.http') diff --git a/addons/test_website_modules/static/tests/tours/configurator_flow.js b/addons/test_website_modules/static/tests/tours/configurator_flow.js index ea4529a0927..312e25db3a8 100644 --- a/addons/test_website_modules/static/tests/tours/configurator_flow.js +++ b/addons/test_website_modules/static/tests/tours/configurator_flow.js @@ -2,6 +2,7 @@ odoo.define('test_website_modules.tour.configurator_flow', function (require) { 'use strict'; const tour = require('web_tour.tour'); +const wTourUtils = require('website.tour_utils'); tour.register('configurator_flow', { test: true, @@ -74,27 +75,27 @@ tour.register('configurator_flow', { run: function () {}, // it's a check }, { content: "Wait untill the configurator is finished", - trigger: 'body.editor_started', + trigger: '#oe_snippets.o_loaded', timeout: 30000, - }, { - content: "exit edit mode", - trigger: '.o_we_website_top_actions button.btn-primary:contains("Save")', - }, { + }, + ...wTourUtils.clickOnSave(), + { content: "check menu and footer links are correct", - trigger: '#oe_main_menu_navbar', // edit mode left and landed on regular frontend + trigger: 'body:not(.editor_enable)', // edit mode left run: function () { + const $iframe = this.$anchor.find('iframe.o_iframe:not(.o_ignore_in_tour)'); for (const menu of ['Home', 'Events', 'Courses', 'Pricing', 'News', 'Success Stories', 'Contact us']) { - if (!$(`#top_menu a:contains(${menu})`).length) { + if (!$iframe.contents().find(`#top_menu a:contains(${menu})`).length) { console.error(`Missing ${menu} menu. It should have been created by the configurator.`); } } for (const url of ['/', '/event', '/slides', '/pricing', '/blog/', '/blog/', '/contactus']) { - if (!$(`#top_menu a[href^='${url}'`).length) { + if (!$iframe.contents().find(`#top_menu a[href^='${url}'`).length) { console.error(`Missing ${url} menu URL. It should have been created by the configurator.`); } } for (const link of ['Privacy Policy', 'Contact us']) { - if (!$(`#footer ul a:contains(${link})`).length) { + if (!$iframe.contents().find(`#footer ul a:contains(${link})`).length) { console.error(`Missing ${link} footer link. It should have been created by the configurator.`); } } diff --git a/addons/test_website_slides_full/tests/tours/slides_certification_member.js b/addons/test_website_slides_full/tests/tours/slides_certification_member.js index f74caa70922..65f19c0f066 100644 --- a/addons/test_website_slides_full/tests/tours/slides_certification_member.js +++ b/addons/test_website_slides_full/tests/tours/slides_certification_member.js @@ -41,7 +41,7 @@ var initTourSteps = [{ var buyCertificationSteps = [{ content: 'eLearning: try to buy course', trigger: 'a:contains("Add to Cart")' -}, +}, tourUtils.goToCart(), { content: 'eCommerce: Process Checkout', diff --git a/addons/web/controllers/home.py b/addons/web/controllers/home.py index 7c22ac3c3b2..f0f08d30f91 100644 --- a/addons/web/controllers/home.py +++ b/addons/web/controllers/home.py @@ -119,7 +119,8 @@ class Home(http.Controller): values['disable_database_manager'] = True response = request.render('web.login', values) - response.headers['X-Frame-Options'] = 'DENY' + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['Content-Security-Policy'] = "frame-ancestors 'self'" return response @http.route('/web/login_successful', type='http', auth='user', website=True, sitemap=False) diff --git a/addons/web/static/src/legacy/js/core/misc.js b/addons/web/static/src/legacy/js/core/misc.js index a457f61984e..9b1e1842886 100644 --- a/addons/web/static/src/legacy/js/core/misc.js +++ b/addons/web/static/src/legacy/js/core/misc.js @@ -130,6 +130,7 @@ function redirect (url, wait) { function Reload(parent, action) { var params = action.params || {}; var menu_id = params.menu_id || false; + var action_id = params.action_id || false; var l = window.location; var sobj = $.deparam(l.search.substr(1)); @@ -141,6 +142,11 @@ function Reload(parent, action) { var hash = l.hash; if (menu_id) { hash = "#menu_id=" + menu_id; + if (action_id) { + hash += "&action=" + action_id; + } + } else if (action_id) { + hash = "#action=" + action_id; } var url = l.protocol + "//" + l.host + l.pathname + search + hash; diff --git a/addons/web/static/src/legacy/js/core/smooth_scroll_on_drag.js b/addons/web/static/src/legacy/js/core/smooth_scroll_on_drag.js index 37aae5f1f44..1e9eeb20f6f 100644 --- a/addons/web/static/src/legacy/js/core/smooth_scroll_on_drag.js +++ b/addons/web/static/src/legacy/js/core/smooth_scroll_on_drag.js @@ -254,6 +254,14 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { - this.options.offsetElementsManager.left(), }; + if (this.iframeOffset) { + const { x, y } = this.iframeOffset; + visibleOffset.left -= x; + visibleOffset.top -= y; + visibleOffset.bottom += y; + visibleOffset.right += x; + } + // If this.$scrollTarget is the html tag, we need to take the scroll position in to account // as offsets positions are calculated relative to the document (thus ). if (this.scrollTargetIsDocument) { @@ -361,6 +369,8 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { */ _onSmoothDragStart(ev, ui, onDragStartCallBack) { this.scrollTargetIsDocument = this.$scrollTarget.is('html'); + const iframeEl = this.$scrollTarget[0].ownerDocument.defaultView.frameElement; + this.iframeOffset = iframeEl && iframeEl.getBoundingClientRect(); this.scrollTargetIsParent = this.$scrollTarget.get(0).contains(this.$element.get(0)); this._updatePositionOptions(ui); this._startSmoothScroll(ui); diff --git a/addons/web/static/src/legacy/js/public/public_root.js b/addons/web/static/src/legacy/js/public/public_root.js index 5401487f1f8..0d1c41b1884 100644 --- a/addons/web/static/src/legacy/js/public/public_root.js +++ b/addons/web/static/src/legacy/js/public/public_root.js @@ -21,7 +21,7 @@ import { } from "../../utils"; import { standaloneAdapter } from "web.OwlCompatibility"; -import { fetchAndProcessTemplates } from "@web/core/assets"; +import { fetchAndProcessTemplates, loadBundle } from "@web/core/assets"; import { makeEnv, startServices } from "@web/env"; import { MainComponentsContainer } from "@web/core/main_components_container"; import { browser } from '@web/core/browser/browser'; @@ -185,7 +185,6 @@ export const PublicRoot = publicWidget.RootWidget.extend({ $from = this.$el; } } - options = Object.assign({}, options, { wysiwyg: $('#wrapwrap').data('wysiwyg'), }); @@ -411,14 +410,16 @@ export async function createPublicRoot(RootWidget) { mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv); // The root widget's parent is a standalone adapter so that it has _trigger_up const publicRoot = new RootWidget(standaloneAdapter({ Component })); + const app = new App(MainComponentsContainer, { + env: wowlEnv, + dev: wowlEnv.debug, + templates: window.__OWL_TEMPLATES__, + translateFn: _t, + translatableAttributes: ["data-tooltip"], + }); + loadBundle.app = app; await Promise.all([ - new App(MainComponentsContainer, { - env: wowlEnv, - dev: wowlEnv.debug, - templates: window.__OWL_TEMPLATES__, - translateFn: _t, - translatableAttributes: ["data-tooltip"], - }).mount(document.body), + app.mount(document.body), publicRoot.attachTo(document.body), ]); return publicRoot; diff --git a/addons/web/static/src/legacy/js/widgets/colorpicker.js b/addons/web/static/src/legacy/js/widgets/colorpicker.js index 41dc2f1ab9a..d0745f2a454 100644 --- a/addons/web/static/src/legacy/js/widgets/colorpicker.js +++ b/addons/web/static/src/legacy/js/widgets/colorpicker.js @@ -41,6 +41,7 @@ var ColorpickerWidget = Widget.extend({ // Needs to be bound on document to work in all possible cases. const $document = $( + parent.options.ownerDocument || (parent.el && parent.el.parentElement && parent.el.ownerDocument) || (parent.options && parent.options.$editable && parent.options.$editable[0] && parent.options.$editable[0].ownerDocument) || document); diff --git a/addons/web/static/src/webclient/navbar/navbar.js b/addons/web/static/src/webclient/navbar/navbar.js index 44cdcf522e7..00c806d40ef 100644 --- a/addons/web/static/src/webclient/navbar/navbar.js +++ b/addons/web/static/src/webclient/navbar/navbar.js @@ -84,6 +84,10 @@ export class NavBar extends Component { ); } + // This dummy setter is only here to prevent conflicts between the + // Enterprise NavBar extension and the Website NavBar patch. + set currentAppSections(_) {} + get systrayItems() { return systrayRegistry .getEntries() @@ -92,6 +96,10 @@ export class NavBar extends Component { .reverse(); } + // This dummy setter is only here to prevent conflicts between the + // Enterprise NavBar extension and the Website NavBar patch. + set systrayItems(_) {} + /** * Adapt will check the available width for the app sections to get displayed. * If not enough space is available, it will replace by a "more" menu diff --git a/addons/web/static/src/webclient/navbar/navbar.xml b/addons/web/static/src/webclient/navbar/navbar.xml index 4ec8ad12ae6..57d1f232ffb 100644 --- a/addons/web/static/src/webclient/navbar/navbar.xml +++ b/addons/web/static/src/webclient/navbar/navbar.xml @@ -30,6 +30,8 @@