[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:
committed by
qsm-odoo
co-authored by
Arthur Detroux
qsm-odoo
parent
8bf1f332af
commit
2d44f2792d
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user