Fix mermaid / contentkit webframe not updating properly (#2427)

This commit is contained in:
Samy Pessé
2024-08-13 15:26:11 +02:00
committed by GitHub
parent 3996110ebd
commit 0f1565cb4b
5 changed files with 52 additions and 26 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-contentkit': patch
---
Fix rendering of webframe with SSR causing wrong communication between frame and renderer
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
@@ -3,6 +3,7 @@ import { Icon } from '@gitbook/icons';
import { ContentKit, ContentKitOutput, ContentKitServerContext } from '@gitbook/react-contentkit'; import { ContentKit, ContentKitOutput, ContentKitServerContext } from '@gitbook/react-contentkit';
import { ignoreAPIError, renderIntegrationUi } from '@/lib/api'; import { ignoreAPIError, renderIntegrationUi } from '@/lib/api';
import { INTEGRATIONS_HOST } from '@/lib/csp';
import { parseMarkdown } from '@/lib/markdown'; import { parseMarkdown } from '@/lib/markdown';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
@@ -69,7 +70,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
return ( return (
<div className={tcls(style)}> <div className={tcls(style)}>
<ContentKit <ContentKit
security={{ firstPartyDomains: ['integrations.gitbook.com'] }} security={{ firstPartyDomains: [INTEGRATIONS_HOST] }}
initialInput={initialInput} initialInput={initialInput}
initialOutput={initialOutput} initialOutput={initialOutput}
render={async (request) => { render={async (request) => {
+8 -2
View File
@@ -25,6 +25,12 @@ export function createContentSecurityPolicyNonce(): string {
return nonce; return nonce;
} }
/**
* Hostname serving the integrations.
*/
export const INTEGRATIONS_HOST =
process.env.GITBOOK_INTEGRATIONS_HOST ?? 'integrations.gitbook.com';
/** /**
* Generate a Content Security Policy header for a space. * Generate a Content Security Policy header for a space.
*/ */
@@ -39,10 +45,10 @@ export function getContentSecurityPolicy(scripts: SpaceIntegrationScript[], nonc
// Since I can't get the nonce to work for inline styles, we need to allow unsafe-inline // Since I can't get the nonce to work for inline styles, we need to allow unsafe-inline
const defaultCSP = ` const defaultCSP = `
default-src 'self' ${assetsDomain}; default-src 'self' ${assetsDomain};
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' 'unsafe-eval' ${assetsDomain} https://integrations.gitbook.com https://cdn.iframe.ly; script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' 'unsafe-eval' ${assetsDomain} https://${INTEGRATIONS_HOST} https://cdn.iframe.ly;
style-src 'self' ${assetsDomain} fonts.googleapis.com 'unsafe-inline'; style-src 'self' ${assetsDomain} fonts.googleapis.com 'unsafe-inline';
img-src * 'self' blob: data: files.gitbook.com ${assetsDomain} ${iconsAssetsSrc}; img-src * 'self' blob: data: files.gitbook.com ${assetsDomain} ${iconsAssetsSrc};
connect-src * 'self' integrations.gitbook.com app.gitbook.com api.gitbook.com srv.buysellads.com ${assetsDomain} ${iconsAssetsSrc}; connect-src * 'self' ${INTEGRATIONS_HOST} app.gitbook.com api.gitbook.com srv.buysellads.com ${assetsDomain} ${iconsAssetsSrc};
font-src 'self' fonts.gstatic.com ${assetsDomain}; font-src 'self' fonts.gstatic.com ${assetsDomain};
frame-src *; frame-src *;
object-src 'none'; object-src 'none';
@@ -10,6 +10,7 @@ import { resolveDynamicBinding } from './dynamic';
export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWebFrame>) { export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWebFrame>) {
const { element } = props; const { element } = props;
const [mounted, setMounted] = React.useState(false);
const renderer = useContentKitClientContext(); const renderer = useContentKitClientContext();
const iframeRef = React.useRef<HTMLIFrameElement>(null); const iframeRef = React.useRef<HTMLIFrameElement>(null);
const [size, setSize] = React.useState<{ const [size, setSize] = React.useState<{
@@ -23,10 +24,6 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
const sendMessage = React.useCallback( const sendMessage = React.useCallback(
(message: object) => { (message: object) => {
if (!iframeRef.current) {
return;
}
const target = new URL(element.source.url); const target = new URL(element.source.url);
// For security reasons, only iframe from our integrations domains are allowed // For security reasons, only iframe from our integrations domains are allowed
@@ -36,6 +33,10 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
} }
if (readyRef.current) { if (readyRef.current) {
if (!iframeRef.current) {
return;
}
iframeRef.current.contentWindow!.postMessage( iframeRef.current.contentWindow!.postMessage(
message, message,
`${target.protocol}//${target.host}`, `${target.protocol}//${target.host}`,
@@ -61,7 +62,7 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
// For security reasons, only iframe from our integrations domains are allowed // For security reasons, only iframe from our integrations domains are allowed
// to send and receive messages // to send and receive messages
if (!renderer.security.firstPartyDomains.includes(origin.host) && 0) { if (!renderer.security.firstPartyDomains.includes(origin.host)) {
return; return;
} }
@@ -117,6 +118,11 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
}; };
window.addEventListener('message', callback); window.addEventListener('message', callback);
// We only render the iframe once we have added the event listener
// otherwise during SSR, we'll miss messages
setMounted(true);
return () => { return () => {
window.removeEventListener('message', callback); window.removeEventListener('message', callback);
}; };
@@ -142,26 +148,29 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
<div <div
className={`contentkit-webframe`} className={`contentkit-webframe`}
style={{ style={{
aspectRatio: element.aspectRatio, aspectRatio: size.aspectRatio || element.aspectRatio || undefined,
...size, maxWidth: size.maxWidth || undefined,
maxHeight: size.maxHeight || undefined,
}} }}
> >
<iframe {mounted ? (
ref={iframeRef} <iframe
src={element.source.url} ref={iframeRef}
allowFullScreen src={element.source.url}
allow="clipboard-write" allowFullScreen
style={{ allow="clipboard-write"
position: 'absolute', style={{
top: 0, position: 'absolute',
left: 0, top: 0,
bottom: 0, left: 0,
right: 0, bottom: 0,
width: '100%', right: 0,
height: '100%', width: '100%',
border: 'none', height: '100%',
}} border: 'none',
/> }}
/>
) : null}
</div> </div>
); );
} }