From 397ec6438371dd10f1bf88190d4d57581de1d303 Mon Sep 17 00:00:00 2001 From: Edoardo Dusi Date: Thu, 6 Feb 2025 18:03:10 +0100 Subject: [PATCH 1/2] fix(rsc): prevent draft content leaks in visual editor - Add proper cleanup of story cache after use - Improve type safety in live editing components - Optimize visual editor detection logic - Add defensive initialization of global story cache --- src/__tests__/live-editing.test.ts | 10 ++++++++++ src/rsc/common.ts | 7 ++++++- src/rsc/live-edit-update-action.ts | 4 +++- src/rsc/live-editing.tsx | 13 ++++++++++--- src/rsc/story.tsx | 2 ++ 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/live-editing.test.ts diff --git a/src/__tests__/live-editing.test.ts b/src/__tests__/live-editing.test.ts new file mode 100644 index 00000000..c29bcf6f --- /dev/null +++ b/src/__tests__/live-editing.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest'; +import StoryblokLiveEditing from '../rsc/live-editing'; +import type { ISbStoryData } from '@storyblok/js'; + +describe('storyblokLiveEditing', () => { + it('should return null when not in visual editor', () => { + const component = StoryblokLiveEditing({ story: { uuid: '123' } as ISbStoryData, bridgeOptions: {} }); + expect(component).toBeNull(); + }); +}); diff --git a/src/rsc/common.ts b/src/rsc/common.ts index 1cd3b1a5..d8ff5c71 100644 --- a/src/rsc/common.ts +++ b/src/rsc/common.ts @@ -11,7 +11,12 @@ const componentsMap: Map = new Map(); +declare global { + // eslint-disable-next-line no-var, vars-on-top + var storyCache: Map; +} + +globalThis.storyCache = !globalThis.storyCache ? new Map() : globalThis.storyCache; export const useStoryblokApi = (): StoryblokClient => { if (!storyblokApiInstance) { diff --git a/src/rsc/live-edit-update-action.ts b/src/rsc/live-edit-update-action.ts index 8e988219..23233a93 100644 --- a/src/rsc/live-edit-update-action.ts +++ b/src/rsc/live-edit-update-action.ts @@ -1,6 +1,8 @@ 'use server'; -export async function liveEditUpdateAction({ story, pathToRevalidate }) { +import type { ISbStoryData } from '@storyblok/js'; + +export async function liveEditUpdateAction({ story, pathToRevalidate }: { story: ISbStoryData; pathToRevalidate: string }) { if (!story || !pathToRevalidate) { return console.error('liveEditUpdateAction: story or pathToRevalidate is not provided'); } diff --git a/src/rsc/live-editing.tsx b/src/rsc/live-editing.tsx index 3c5672eb..a18ae06f 100644 --- a/src/rsc/live-editing.tsx +++ b/src/rsc/live-editing.tsx @@ -1,15 +1,22 @@ 'use client'; -import { registerStoryblokBridge } from '@storyblok/js'; +import { type ISbStoryData, registerStoryblokBridge, type StoryblokBridgeConfigV2 } from '@storyblok/js'; import { startTransition, useEffect } from 'react'; import { liveEditUpdateAction } from './live-edit-update-action'; -const StoryblokLiveEditing = ({ story = null, bridgeOptions = {} }) => { +const isVisualEditor = (): boolean => { if (typeof window === 'undefined') { + return false; + } + return typeof window.storyblokRegisterEvent !== 'undefined' && window.location.search.includes('_storyblok'); +}; + +const StoryblokLiveEditing = ({ story = null, bridgeOptions = {} }: { story: ISbStoryData; bridgeOptions: StoryblokBridgeConfigV2 }) => { + if (!isVisualEditor()) { return null; } - const handleInput = (story) => { + const handleInput = (story: ISbStoryData) => { if (!story) { return; } diff --git a/src/rsc/story.tsx b/src/rsc/story.tsx index 6a9fffff..1edbcc0d 100644 --- a/src/rsc/story.tsx +++ b/src/rsc/story.tsx @@ -19,6 +19,8 @@ const StoryblokStory = forwardRef( if (globalThis.storyCache.has(story.uuid)) { story = globalThis.storyCache.get(story.uuid); + // Delete the story from the cache to avoid draft content leaking + globalThis.storyCache.delete(story.uuid); } if (typeof story.content === 'string') { From 786d6a8bb7451a2a057820f7b002e0ad1cc53d1a Mon Sep 17 00:00:00 2001 From: Edoardo Dusi Date: Fri, 7 Feb 2025 10:23:49 +0100 Subject: [PATCH 2/2] fix: remove internalid property check as it's not in our story interface --- src/common/client.ts | 2 +- src/rsc/live-editing.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/client.ts b/src/common/client.ts index cdff6f96..c3ab56c5 100644 --- a/src/common/client.ts +++ b/src/common/client.ts @@ -10,7 +10,7 @@ export const useStoryblokState: TUseStoryblokState = ( ) => { const [story, setStory] = useState(initialStory); - const storyId = (initialStory as any)?.internalId ?? initialStory?.id ?? 0; + const storyId = initialStory?.id ?? 0; const isBridgeEnabled = typeof window !== 'undefined' && typeof window.storyblokRegisterEvent !== 'undefined'; diff --git a/src/rsc/live-editing.tsx b/src/rsc/live-editing.tsx index a18ae06f..1b48b5f6 100644 --- a/src/rsc/live-editing.tsx +++ b/src/rsc/live-editing.tsx @@ -25,7 +25,7 @@ const StoryblokLiveEditing = ({ story = null, bridgeOptions = {} }: { story: ISb }); }; - const storyId = story?.internalId ?? story?.id ?? 0; + const storyId = story?.id ?? 0; useEffect(() => { registerStoryblokBridge(storyId, newStory => handleInput(newStory), bridgeOptions); }, []);