From d50216f5b267aee331f86ab62d6ea342a107aa73 Mon Sep 17 00:00:00 2001 From: Benjamin Franzke <ben@bnf.dev> Date: Tue, 5 Sep 2023 22:12:07 +0200 Subject: [PATCH] [BUGFIX] Fix race condition in module router The module components (like the iframe wrapper) are created asynchronously after their implementing module (e.g. <typo3-iframe-module>) is loaded. The router had a preparatory check that verified that existing module elements are reused, but missed to verifiy whether the element has been created in parallel while the module components javascript module was loaded. This happens when the module changes while the module component is loaded for the first time. That leads to a situation that the module componenent was created twice, which rendered two module frames into the backend. Also resolve a long overdue todo while at it. Resolves: #101851 Releases: main, 12.4, 11.5 Change-Id: I069f9e924d38b3abeb70c6d4e805d9c43a4f05e9 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80912 Tested-by: Benjamin Franzke <ben@bnf.dev> Reviewed-by: Benjamin Franzke <ben@bnf.dev> Tested-by: core-ci <typo3@b13.com> --- .../backend/Resources/Public/TypeScript/Module/Router.ts | 9 ++++++++- .../backend/Resources/Public/JavaScript/Module/Router.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/Module/Router.ts b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/Module/Router.ts index bab606b27267..a82025448d83 100644 --- a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/Module/Router.ts +++ b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/Module/Router.ts @@ -170,7 +170,14 @@ export class ModuleRouter extends LitElement { try { const module = await import(moduleName); - // @todo: Check if .componentName exists + element = this.querySelector(`*[slot="${moduleName}"]`); + if (element !== null) { + // The element has been created parallelly during the asynchronous module load; use that instance + return element; + } + if (!('componentName' in module)) { + throw new Error(`module ${moduleName} is missing the "componentName" export`); + } element = document.createElement(module.componentName); } catch (e) { console.error({msg: `Error importing ${moduleName} as backend module`, err: e}) diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Module/Router.js b/typo3/sysext/backend/Resources/Public/JavaScript/Module/Router.js index d55f91f3f149..47789e2ca933 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/Module/Router.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Module/Router.js @@ -10,7 +10,7 @@ * * The TYPO3 project - inspiring people to share! */ -var __createBinding=this&&this.__createBinding||(Object.create?function(t,e,o,r){void 0===r&&(r=o);var i=Object.getOwnPropertyDescriptor(e,o);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[o]}}),Object.defineProperty(t,r,i)}:function(t,e,o,r){void 0===r&&(r=o),t[r]=e[o]}),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),__decorate=this&&this.__decorate||function(t,e,o,r){var i,n=arguments.length,l=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)l=Reflect.decorate(t,e,o,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(l=(n<3?i(l):n>3?i(e,o,l):i(e,o))||l);return n>3&&l&&Object.defineProperty(e,o,l),l},__importStar=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var o in t)"default"!==o&&Object.prototype.hasOwnProperty.call(t,o)&&__createBinding(e,t,o);return __setModuleDefault(e,t),e};define(["require","exports","lit","lit/decorators","../Module"],(function(t,e,o,r,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ModuleRouter=void 0;const n="TYPO3/CMS/Backend/Module/Iframe",l=(t,e)=>!0;let a=class extends o.LitElement{constructor(){super(),this.module="",this.endpoint="",this.addEventListener("typo3-module-load",({target:t,detail:e})=>{const o=t.getAttribute("slot");this.pushState({slotName:o,detail:e})}),this.addEventListener("typo3-module-loaded",({detail:t})=>{this.updateBrowserState(t)}),this.addEventListener("typo3-iframe-load",({detail:t})=>{let e={slotName:n,detail:t};if(e.detail.url.includes(this.stateTrackerUrl+"?state=")){const t=e.detail.url.split("?state=");e=JSON.parse(decodeURIComponent(t[1]||"{}"))}this.slotElement.getAttribute("name")!==e.slotName&&this.slotElement.setAttribute("name",e.slotName),this.markActive(e.slotName,this.slotElement.getAttribute("name")===n?null:e.detail.url,!1),this.updateBrowserState(e.detail),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-load",{bubbles:!0,composed:!0,detail:e.detail}))}),this.addEventListener("typo3-iframe-loaded",({detail:t})=>{this.updateBrowserState(t),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-loaded",{bubbles:!0,composed:!0,detail:t}))})}render(){const t=(0,i.getRecordFromName)(this.module).component||n;return o.html`<slot name="${t}"></slot>`}updated(){const t=(0,i.getRecordFromName)(this.module).component||n;this.markActive(t,this.endpoint)}async markActive(t,e,o=!0){const r=await this.getModuleElement(t);e&&(o||r.getAttribute("endpoint")!==e)&&r.setAttribute("endpoint",e),r.hasAttribute("active")||r.setAttribute("active","");for(let t=r.previousElementSibling;null!==t;t=t.previousElementSibling)t.removeAttribute("active");for(let t=r.nextElementSibling;null!==t;t=t.nextElementSibling)t.removeAttribute("active")}async getModuleElement(e){let o=this.querySelector(`*[slot="${e}"]`);if(null!==o)return o;try{const r=await new Promise((o,r)=>{t([e],o,r)}).then(__importStar);o=document.createElement(r.componentName)}catch(t){throw console.error({msg:`Error importing ${e} as backend module`,err:t}),t}return o.setAttribute("slot",e),this.appendChild(o),o}async pushState(t){const e=this.stateTrackerUrl+"?state="+encodeURIComponent(JSON.stringify(t));(await this.getModuleElement(n)).setAttribute("endpoint",e)}updateBrowserState(t){const e=new URL(t.url||"",window.location.origin),o=new URLSearchParams(e.search),r="title"in t?t.title:"";if(null!==r){const t=[this.sitename];""!==r&&t.unshift(r),this.sitenameFirst&&t.reverse(),document.title=t.join(" · ")}if(!o.has("token")){if(!o.has("install[controller]"))return;{const t=o.get("install[controller]");o.delete("install[controller]"),o.delete("install[context]"),e.pathname=e.pathname.replace("/typo3/install.php","/typo3/module/tools/"+t)}}o.delete("token"),e.search=o.toString();const i=e.toString();window.history.replaceState(t,"",i)}};a.styles=o.css` +var __createBinding=this&&this.__createBinding||(Object.create?function(t,e,o,r){void 0===r&&(r=o);var i=Object.getOwnPropertyDescriptor(e,o);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[o]}}),Object.defineProperty(t,r,i)}:function(t,e,o,r){void 0===r&&(r=o),t[r]=e[o]}),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),__decorate=this&&this.__decorate||function(t,e,o,r){var i,n=arguments.length,l=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)l=Reflect.decorate(t,e,o,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(l=(n<3?i(l):n>3?i(e,o,l):i(e,o))||l);return n>3&&l&&Object.defineProperty(e,o,l),l},__importStar=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var o in t)"default"!==o&&Object.prototype.hasOwnProperty.call(t,o)&&__createBinding(e,t,o);return __setModuleDefault(e,t),e};define(["require","exports","lit","lit/decorators","../Module"],(function(t,e,o,r,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ModuleRouter=void 0;const n="TYPO3/CMS/Backend/Module/Iframe",l=(t,e)=>!0;let a=class extends o.LitElement{constructor(){super(),this.module="",this.endpoint="",this.addEventListener("typo3-module-load",({target:t,detail:e})=>{const o=t.getAttribute("slot");this.pushState({slotName:o,detail:e})}),this.addEventListener("typo3-module-loaded",({detail:t})=>{this.updateBrowserState(t)}),this.addEventListener("typo3-iframe-load",({detail:t})=>{let e={slotName:n,detail:t};if(e.detail.url.includes(this.stateTrackerUrl+"?state=")){const t=e.detail.url.split("?state=");e=JSON.parse(decodeURIComponent(t[1]||"{}"))}this.slotElement.getAttribute("name")!==e.slotName&&this.slotElement.setAttribute("name",e.slotName),this.markActive(e.slotName,this.slotElement.getAttribute("name")===n?null:e.detail.url,!1),this.updateBrowserState(e.detail),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-load",{bubbles:!0,composed:!0,detail:e.detail}))}),this.addEventListener("typo3-iframe-loaded",({detail:t})=>{this.updateBrowserState(t),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-loaded",{bubbles:!0,composed:!0,detail:t}))})}render(){const t=(0,i.getRecordFromName)(this.module).component||n;return o.html`<slot name="${t}"></slot>`}updated(){const t=(0,i.getRecordFromName)(this.module).component||n;this.markActive(t,this.endpoint)}async markActive(t,e,o=!0){const r=await this.getModuleElement(t);e&&(o||r.getAttribute("endpoint")!==e)&&r.setAttribute("endpoint",e),r.hasAttribute("active")||r.setAttribute("active","");for(let t=r.previousElementSibling;null!==t;t=t.previousElementSibling)t.removeAttribute("active");for(let t=r.nextElementSibling;null!==t;t=t.nextElementSibling)t.removeAttribute("active")}async getModuleElement(e){let o=this.querySelector(`*[slot="${e}"]`);if(null!==o)return o;try{const r=await new Promise((o,r)=>{t([e],o,r)}).then(__importStar);if(o=this.querySelector(`*[slot="${e}"]`),null!==o)return o;if(!("componentName"in r))throw new Error(`module ${e} is missing the "componentName" export`);o=document.createElement(r.componentName)}catch(t){throw console.error({msg:`Error importing ${e} as backend module`,err:t}),t}return o.setAttribute("slot",e),this.appendChild(o),o}async pushState(t){const e=this.stateTrackerUrl+"?state="+encodeURIComponent(JSON.stringify(t));(await this.getModuleElement(n)).setAttribute("endpoint",e)}updateBrowserState(t){const e=new URL(t.url||"",window.location.origin),o=new URLSearchParams(e.search),r="title"in t?t.title:"";if(null!==r){const t=[this.sitename];""!==r&&t.unshift(r),this.sitenameFirst&&t.reverse(),document.title=t.join(" · ")}if(!o.has("token")){if(!o.has("install[controller]"))return;{const t=o.get("install[controller]");o.delete("install[controller]"),o.delete("install[context]"),e.pathname=e.pathname.replace("/typo3/install.php","/typo3/module/tools/"+t)}}o.delete("token"),e.search=o.toString();const i=e.toString();window.history.replaceState(t,"",i)}};a.styles=o.css` :host { width: 100%; min-height: 100%; -- GitLab