From 41bc480b7b2ce0a5fd2bf60ab0ed762a6eb41d61 Mon Sep 17 00:00:00 2001 From: "Robin Lejeune (role)" Date: Fri, 17 Nov 2023 17:29:37 +0100 Subject: [PATCH] [FIX] website: avoid infinite redirects in iframe on Chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you have the backend in a language A but the website in English only, you can: 1) modify a record's (event, product...) name in language A (say "New Name"). 2) visit the page `/new-name-11` => the server will redirect you to the English page `/origin-11`, with the only slug that actually exists on the website. Chrome caches the redirection. 3) give the same name in English as in language A, try to visit => the server now wants to access `/new-name-11` => Chrome uses the cache to redirect `/new-name-11` to `/origin-11`, => the server tries to redirect to `/new-name-11` => infinite loop, Chrome puts an end to it after ± 20 redirects. In effect, Chrome injects a "Too many redirects" layout inside the iframe, which in turn raises a CORS error when the app tries to update it. At the time of this commit, the flow described here should be expected of users, because the translation UI in the backend is not clear: the default field displayed is in language A even though the website does not use it, and the way to update translations is not obvious (you have to click on the language tag, which doesn't look like a button). Another way to artificially reproduce the issue would be: - Create a website.redirect from /dog to /cat - Same from /cat to /dog - Go to /@/dog After this commit, if we detect that behavior, we reload the iframe with a new query parameter, making the URL brand-new (and not cached) for Chrome. opw-3479651 closes odoo/odoo#145306 X-original-commit: 16d47ddf128f31f13e8d0d74597635faf7f9702a Signed-off-by: Romain Derie (rde) Signed-off-by: Robin Lejeune (role) --- .../website_preview/website_preview.js | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) 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 c5dbbce69e7..c68bcc85678 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 @@ -353,7 +353,44 @@ export class WebsitePreview extends Component { this.title.setParts({ action: currentTitle }); } - _onPageLoaded() { + _onPageLoaded(ev) { + // FIX Chrome-only. If you have the backend in a language A but the + // website in English only, you can 1) modify a record's (event, + // product...) name in language A (say "New Name"). + // 2) visit the page `/new-name-11` => the server will redirect you to + // the English page `/origin-11`, which is the only one existing. + // Chrome caches the redirection. + // 3) give the same name in English as in language A, try to visit + // => the server now wants to access `/new-name-11` + // => Chrome uses the cache to redirect `/new-name-11` to `/origin-11`, + // => the server tries to redirect to `/new-name-11` => loop. + // Chrome injects a "Too many redirects" layout in the iframe, which in + // turn raises a CORS error when the app tries to update the iframe. + // If we detect that behavior, we reload the iframe with a new query + // parameter, so that it's not cached for Chrome. + if ( + navigator.userAgent.toLowerCase().includes("chrome") + && !this.iframe.el.src.includes("iframe_reload") + ) { + try { + /* eslint-disable no-unused-expressions */ + this.iframe.el.contentWindow.location.href; + } catch (err) { + if (err.name === "SecurityError") { + ev.stopImmediatePropagation(); + // Note that iframe's `src` is the URL used to start the + // website preview, it's not sync'd with iframe navigation. + const srcUrl = new URL(this.iframe.el.src); + const pathUrl = new URL(srcUrl.searchParams.get("path"), srcUrl.origin); + pathUrl.searchParams.set("iframe_reload", "1"); + srcUrl.searchParams.set("path", `${pathUrl.pathname}${pathUrl.search}`); + // We could inject `pathUrl` directly but keep the same + // expected URL format `/website/force/1?path=..` + this.iframe.el.src = srcUrl.toString(); + return; + } + } + } if (this.lastHiddenPageURL !== this.iframe.el.contentWindow.location.href) { // Hide Ace Editor when moving to another page. this.websiteService.context.showResourceEditor = false;