[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 <ard@odoo.com>
Co-authored-by: qsm-odoo <qsm@odoo.com>
This commit is contained in:
Younn Olivier
2022-06-24 10:28:04 +02:00
committed by qsm-odoo
co-authored by Arthur Detroux qsm-odoo
parent 8bf1f332af
commit 2d44f2792d
8 changed files with 85 additions and 17 deletions
+4 -2
View File
@@ -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):
+4 -2
View File
@@ -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):
+2 -1
View File
@@ -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)
@@ -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
// <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';
@@ -2,7 +2,10 @@
<templates xml:space="preserve">
<t t-name="website.WebsitePreview" owl="1">
<div class="o_website_preview">
<iframe t-att-src="initialUrl" class="o_iframe" t-ref="iframe"/>
<iframe t-att-src="initialUrl"
class="o_iframe"
t-ref="iframe"
t-on-load="_onPageLoaded"/>
</div>
</t>
</templates>
+4
View File
@@ -255,6 +255,10 @@ function sendRequest(route, params) {
let form = document.createElement('form');
form.setAttribute('action', route);
form.setAttribute('method', params.method || 'POST');
const isInIframe = window.frameElement && window.frameElement.classList.contains('o_iframe');
if (isInIframe) {
form.setAttribute('target', '_top');
}
if (core.csrf_token) {
_addInput(form, 'csrf_token', core.csrf_token);
+1
View File
@@ -19,6 +19,7 @@
'assets': {
'web.assets_backend': [
'website_knowledge/static/src/js/knowledge_controller.js',
'website_knowledge/static/src/client_actions/website_preview/website_preview.js',
]
},
}
@@ -0,0 +1,12 @@
/** @odoo-module **/
import { patch } from 'web.utils';
import { WebsitePreview } from '@website/client_actions/website_preview/website_preview';
patch(WebsitePreview.prototype, 'website_knowledge_website_preview', {
/**
* @override
*/
_isTopWindowURL({ pathname }) {
return (pathname && pathname.startsWith('/knowledge/article/')) || this._super(...arguments);
}
});