From 2d44f2792dec0b2f205475a22dbedc97e9c54a64 Mon Sep 17 00:00:00 2001 From: Younn Olivier Date: Fri, 31 Dec 2021 09:41:40 +0100 Subject: [PATCH] [IMP] website, *: handle url redirections from website iframe *: auth_signup, portal, web, website_knowledge When navigating in the iframe, only same origin redirections should be open in the iframe contentWindow. External redirections should be done in the top window. Some internal pages had to be served with X-Frame-Options header set to SAMEORIGIN, and Content-Security-Policy to "frame-ancestors 'self'" (see [1]). All the links that are redirecting to another host, and the client actions, are opened in the top window. Exemples that will be opened in the iframe's top window: - Clicking on a link google.be that should not open in a new tab - Clicking on the language selector and adding a new one, or clicking on "logout". [1]: https://github.com/odoo/odoo/pull/78298#discussion_r898853383 See merge commit for more information. task-2687506 Co-authored-by: Arthur Detroux Co-authored-by: qsm-odoo --- addons/auth_signup/controllers/main.py | 6 +- addons/portal/controllers/portal.py | 6 +- addons/web/controllers/home.py | 3 +- .../website_preview/website_preview.js | 65 +++++++++++++++---- .../website_preview/website_preview.xml | 5 +- addons/website/static/src/js/utils.js | 4 ++ addons/website_knowledge/__manifest__.py | 1 + .../website_preview/website_preview.js | 12 ++++ 8 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 addons/website_knowledge/static/src/client_actions/website_preview/website_preview.js 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/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/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/website/static/src/client_actions/website_preview/website_preview.js b/addons/website/static/src/client_actions/website_preview/website_preview.js index 270b92c929a..ff54e9123b8 100644 --- a/addons/website/static/src/client_actions/website_preview/website_preview.js +++ b/addons/website/static/src/client_actions/website_preview/website_preview.js @@ -3,7 +3,7 @@ import { registry } from '@web/core/registry'; import { useService } from '@web/core/utils/hooks'; -const { Component, onWillStart, useEffect, useRef } = owl; +const { Component, onWillStart, useRef } = owl; export class WebsitePreview extends Component { setup() { @@ -16,15 +16,6 @@ export class WebsitePreview extends Component { const encodedPath = encodeURIComponent(this.path); this.initialUrl = `/website/force/${this.websiteId}?path=${encodedPath}`; }); - - useEffect(() => { - this.iframe.el.addEventListener('load', () => { - // This replaces the browser url (/web#action=website...) with - // the iframe's url (it is clearer for the user). - this.currentUrl = this.iframe.el.contentDocument.location.href; - history.replaceState({}, this.props.action.display_name, this.currentUrl); - }); - }, () => []); } get websiteId() { @@ -37,11 +28,63 @@ export class WebsitePreview extends Component { get path() { let path = this.props.action.context.params && this.props.action.context.params.path; - if (!path) { + if (path) { + const url = new URL(path, window.location.origin); + if (this._isTopWindowURL(url)) { + // If the client action is initialized with a path that + // should not be opened inside the iframe (= something we + // would want to open on the top window), we consider that + // this is not a valid flow. Instead of trying to open it on + // the top window, we initialize the iframe with the + // website homepage... + path = '/'; + } else { + // ... otherwise, the path still needs to be normalized (as + // it would be if the given path was used as an href of a + // element). + path = url.pathname + url.search + url.hash; + } + } else { path = '/'; } return path; } + + /** + * Returns true if the url should be opened in the top + * window. + * + * @param host {string} host of the route. + * @param pathname {string} path of the route. + * @private + */ + _isTopWindowURL({ host, pathname }) { + const backendRoutes = ['/web', '/web/session/logout']; + return host !== window.location.host || (pathname && backendRoutes.includes(pathname)); + } + + _onPageLoaded() { + // This replaces the browser url (/web#action=website...) with + // the iframe's url (it is clearer for the user). + this.currentUrl = this.iframe.el.contentDocument.location.href; + history.replaceState({}, this.props.action.display_name, this.currentUrl); + + // The clicks on the iframe are listened, so that links with external + // redirections can be opened in the top window. + this.iframe.el.contentDocument.addEventListener('click', (ev) => { + const linkEl = ev.target.closest('[href]'); + if (!linkEl) { + return; + } + + const { href, target } = linkEl; + if (href && target !== '_blank' && !this.websiteContext.edition && this._isTopWindowURL(linkEl)) { + ev.preventDefault(); + ev.stopPropagation(); + window.location.replace(href); + } + }); + } } WebsitePreview.template = 'website.WebsitePreview'; diff --git a/addons/website/static/src/client_actions/website_preview/website_preview.xml b/addons/website/static/src/client_actions/website_preview/website_preview.xml index 7840168b132..db9b295f392 100644 --- a/addons/website/static/src/client_actions/website_preview/website_preview.xml +++ b/addons/website/static/src/client_actions/website_preview/website_preview.xml @@ -2,7 +2,10 @@
-