diff --git a/src/LiveComponent/assets/dist/Backend/Backend.d.ts b/src/LiveComponent/assets/dist/Backend/Backend.d.ts index 53e5daae9ac..c664be92d5b 100644 --- a/src/LiveComponent/assets/dist/Backend/Backend.d.ts +++ b/src/LiveComponent/assets/dist/Backend/Backend.d.ts @@ -13,6 +13,7 @@ export interface BackendInterface { }, files: { [key: string]: FileList; }): BackendRequest; + updateCsrfToken(csrfToken: string): void; } export interface BackendAction { name: string; @@ -28,4 +29,5 @@ export default class implements BackendInterface { }, files: { [key: string]: FileList; }): BackendRequest; + updateCsrfToken(csrfToken: string): void; } diff --git a/src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts b/src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts index 2cee6c2fe49..8a29af3b516 100644 --- a/src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts +++ b/src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts @@ -2,7 +2,7 @@ import type { BackendAction, ChildrenFingerprints } from './Backend'; export default class { private url; private method; - private readonly csrfToken; + private csrfToken; constructor(url: string, method?: 'get' | 'post', csrfToken?: string | null); buildRequest(props: any, actions: BackendAction[], updated: { [key: string]: any; @@ -15,4 +15,5 @@ export default class { fetchOptions: RequestInit; }; private willDataFitInUrl; + updateCsrfToken(csrfToken: string): void; } diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js index 05af8986dbe..22b6b68f43a 100644 --- a/src/LiveComponent/assets/dist/live_controller.js +++ b/src/LiveComponent/assets/dist/live_controller.js @@ -2052,6 +2052,9 @@ class Component { return response; } this.processRerender(html, backendResponse); + if (this.element.dataset.liveCsrfValue) { + this.backend.updateCsrfToken(this.element.dataset.liveCsrfValue); + } this.backendRequest = null; thisPromiseResolve(backendResponse); if (this.isRequestPending) { @@ -2325,6 +2328,9 @@ class RequestBuilder { const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenJson + propsFromParentJson).toString(); return (urlEncodedJsonData + params.toString()).length < 1500; } + updateCsrfToken(csrfToken) { + this.csrfToken = csrfToken; + } } class Backend { @@ -2335,6 +2341,9 @@ class Backend { const { url, fetchOptions } = this.requestBuilder.buildRequest(props, actions, updated, children, updatedPropsFromParent, files); return new BackendRequest(fetch(url, fetchOptions), actions.map((backendAction) => backendAction.name), Object.keys(updated)); } + updateCsrfToken(csrfToken) { + this.requestBuilder.updateCsrfToken(csrfToken); + } } class StimulusElementDriver { diff --git a/src/LiveComponent/assets/src/Backend/Backend.ts b/src/LiveComponent/assets/src/Backend/Backend.ts index b7bb6445f11..b3d019a4be4 100644 --- a/src/LiveComponent/assets/src/Backend/Backend.ts +++ b/src/LiveComponent/assets/src/Backend/Backend.ts @@ -15,6 +15,7 @@ export interface BackendInterface { updatedPropsFromParent: { [key: string]: any }, files: { [key: string]: FileList } ): BackendRequest; + updateCsrfToken(csrfToken: string): void; } export interface BackendAction { @@ -52,4 +53,8 @@ export default class implements BackendInterface { Object.keys(updated) ); } + + updateCsrfToken(csrfToken: string) { + this.requestBuilder.updateCsrfToken(csrfToken); + } } diff --git a/src/LiveComponent/assets/src/Backend/RequestBuilder.ts b/src/LiveComponent/assets/src/Backend/RequestBuilder.ts index 328f0647b91..b9bd2fc13dc 100644 --- a/src/LiveComponent/assets/src/Backend/RequestBuilder.ts +++ b/src/LiveComponent/assets/src/Backend/RequestBuilder.ts @@ -3,7 +3,7 @@ import type { BackendAction, ChildrenFingerprints } from './Backend'; export default class { private url: string; private method: 'get' | 'post'; - private readonly csrfToken: string | null; + private csrfToken: string | null; constructor(url: string, method: 'get' | 'post' = 'post', csrfToken: string | null = null) { this.url = url; @@ -117,4 +117,8 @@ export default class { // if the URL gets remotely close to 2000 chars, it may not fit return (urlEncodedJsonData + params.toString()).length < 1500; } + + updateCsrfToken(csrfToken: string) { + this.csrfToken = csrfToken; + } } diff --git a/src/LiveComponent/assets/src/Component/index.ts b/src/LiveComponent/assets/src/Component/index.ts index 28807c57884..18c6a5dea81 100644 --- a/src/LiveComponent/assets/src/Component/index.ts +++ b/src/LiveComponent/assets/src/Component/index.ts @@ -329,6 +329,11 @@ export default class Component { this.processRerender(html, backendResponse); + // Store updated csrf token + if (this.element.dataset.liveCsrfValue) { + this.backend.updateCsrfToken(this.element.dataset.liveCsrfValue); + } + // finally resolve this promise this.backendRequest = null; thisPromiseResolve(backendResponse); diff --git a/src/LiveComponent/assets/test/controller/render.test.ts b/src/LiveComponent/assets/test/controller/render.test.ts index efa875c0ba9..0e328a61517 100644 --- a/src/LiveComponent/assets/test/controller/render.test.ts +++ b/src/LiveComponent/assets/test/controller/render.test.ts @@ -630,4 +630,23 @@ describe('LiveController rendering Tests', () => { // verify the selectedIndex of the select option 2 is 0 expect(selectOption2.selectedIndex).toBe(0); }); + + it('backend will have a new csrf token', async () => { + const test = await createTest( + {}, + (data: any) => ` +
+
+ ` + ); + + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change csrf token + data.csrf = 'Hello'; + }); + + await test.component.render(); + + expect(test.mockedBackend.csrfToken).toEqual('Hello'); + }); }); diff --git a/src/LiveComponent/assets/test/tools.ts b/src/LiveComponent/assets/test/tools.ts index 4b0f67993d4..958fdcc8b53 100644 --- a/src/LiveComponent/assets/test/tools.ts +++ b/src/LiveComponent/assets/test/tools.ts @@ -98,6 +98,8 @@ class FunctionalTest { class MockedBackend implements BackendInterface { private expectedMockedAjaxCalls: Array = []; + public csrfToken: string | null = null; + addMockedAjaxCall(mock: MockedAjaxCall) { this.expectedMockedAjaxCalls.push(mock); } @@ -139,6 +141,10 @@ class MockedBackend implements BackendInterface { return matchedMock.createBackendRequest(); } + updateCsrfToken(csrfToken: string) { + this.csrfToken = csrfToken; + } + getExpectedMockedAjaxCalls(): Array { return this.expectedMockedAjaxCalls; }