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