Skip to content

Commit

Permalink
fix(rsc): prevent draft content leaks in visual editor (#1337)
Browse files Browse the repository at this point in the history
* 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

* fix: remove internalid property check as it's not in our story interface
  • Loading branch information
edodusi authored Feb 7, 2025
1 parent 73b1228 commit fd3d9e4
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 7 deletions.
10 changes: 10 additions & 0 deletions src/__tests__/live-editing.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
2 changes: 1 addition & 1 deletion src/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
7 changes: 6 additions & 1 deletion src/rsc/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const componentsMap: Map<string, React.ElementType> = new Map<string, React.Elem
let enableFallbackComponent: boolean = false;
let customFallbackComponent: React.ElementType = null;

globalThis.storyCache = new Map<string, ISbStoryData>();
declare global {
// eslint-disable-next-line no-var, vars-on-top
var storyCache: Map<string, ISbStoryData>;
}

globalThis.storyCache = !globalThis.storyCache ? new Map<string, ISbStoryData>() : globalThis.storyCache;

export const useStoryblokApi = (): StoryblokClient => {
if (!storyblokApiInstance) {
Expand Down
4 changes: 3 additions & 1 deletion src/rsc/live-edit-update-action.ts
Original file line number Diff line number Diff line change
@@ -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');
}
Expand Down
15 changes: 11 additions & 4 deletions src/rsc/live-editing.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -18,7 +25,7 @@ const StoryblokLiveEditing = ({ story = null, bridgeOptions = {} }) => {
});
};

const storyId = story?.internalId ?? story?.id ?? 0;
const storyId = story?.id ?? 0;
useEffect(() => {
registerStoryblokBridge(storyId, newStory => handleInput(newStory), bridgeOptions);
}, []);
Expand Down
2 changes: 2 additions & 0 deletions src/rsc/story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const StoryblokStory = forwardRef<HTMLElement, StoryblokStoryProps>(

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') {
Expand Down

0 comments on commit fd3d9e4

Please sign in to comment.