Tldraw/apps/dotcom/src/components/IFrameProtector.tsx

140 wiersze
4.9 KiB
TypeScript

import { ReactNode, useEffect, useState } from 'react'
import { LoadingScreen } from 'tldraw'
import { version } from '../../version'
import { useUrl } from '../hooks/useUrl'
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
/*
If we're in an iframe, we need to figure out whether we're on a whitelisted host (e.g. tldraw itself)
or a not-allowed host (e.g. someone else's website). Some websites embed tldraw in iframes and this is kinda
risky for us and for them, too—and hey, if we decide to offer a hosted thing, then that's another stor
Figuring this out is a little tricky because the same code here is going to run on:
- the website as a top window (tldraw-top)
- the website in an iframe (tldraw-iframe)
We set a listener on the current window (which may be top or not) to listen for a "are-we-cool" message,
which responds "yes" with the current library version.
If we detect that we're in an iframe (i.e. that our window is not the top window) then we send this
"are-we-cool" message to the parent window. If we get back the "yes" + version message, then that means
the iframe is embedded inside of another tldraw window, and so we can show the contents of the iframe.
If we don't get a message back in time, then that means the iframe is embedded in a not-allowed website,
and we should show an annoying messsage.
If we're not in an iframe, we don't need to do anything.
*/
// Which routes do we allow to be embedded in tldraw.com itself?
const WHITELIST_CONTEXT = ['public-multiplayer', 'public-readonly', 'public-snapshot']
const EXPECTED_QUESTION = 'are we cool?'
const EXPECTED_RESPONSE = 'yes' + version
const isInIframe = () => {
return typeof window !== 'undefined' && (window !== window.top || window.self !== window.parent)
}
export function IFrameProtector({
slug,
context,
children,
}: {
slug: string
context:
| 'public-multiplayer'
| 'public-readonly'
| 'public-snapshot'
| 'history-snapshot'
| 'history'
| 'local'
children: ReactNode
}) {
const [embeddedState, setEmbeddedState] = useState<
'iframe-unknown' | 'iframe-not-allowed' | 'not-iframe' | 'iframe-ok'
>(isInIframe() ? 'iframe-unknown' : 'not-iframe')
const url = useUrl()
useEffect(() => {
if (typeof window === 'undefined') {
return
}
let timeout: any | undefined
function handleMessageEvent(event: MessageEvent) {
if (!event.source) return
if (event.data === EXPECTED_QUESTION) {
if (!isInIframe()) {
// If _we're_ in an iframe, then we don't want to show a nested
// iframe, even if we're on a whitelisted host / context
event.source.postMessage(EXPECTED_RESPONSE)
}
}
if (event.data === EXPECTED_RESPONSE) {
// todo: check the origin?
setEmbeddedState('iframe-ok')
clearTimeout(timeout)
}
}
window.addEventListener('message', handleMessageEvent, false)
if (embeddedState === 'iframe-unknown') {
// We iframe embeddings on multiplayer or readonly
if (WHITELIST_CONTEXT.includes(context)) {
window.parent.postMessage(EXPECTED_QUESTION, '*') // todo: send to a specific origin?
timeout = setTimeout(() => {
setEmbeddedState('iframe-not-allowed')
trackAnalyticsEvent('connect_to_room_in_iframe', { slug, context })
}, 1000)
} else {
// We don't allow iframe embeddings on other routes
setEmbeddedState('iframe-not-allowed')
}
}
return () => {
clearTimeout(timeout)
window.removeEventListener('message', handleMessageEvent)
}
}, [embeddedState, slug, context])
if (embeddedState === 'iframe-unknown') {
// We're in an iframe, but we don't know if it's a tldraw iframe
return <LoadingScreen>Loading in an iframe...</LoadingScreen>
}
if (embeddedState === 'iframe-not-allowed') {
// We're in an iframe and its not one of ours
return (
<div className="tldraw__editor tl-container">
<div className="iframe-warning__container">
<a className="iframe-warning__link" href={url} target="_blank">
{'Visit this page on tldraw.com'}
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 2C2.44772 2 2 2.44772 2 3V12C2 12.5523 2.44772 13 3 13H12C12.5523 13 13 12.5523 13 12V8.5C13 8.22386 12.7761 8 12.5 8C12.2239 8 12 8.22386 12 8.5V12H3V3L6.5 3C6.77614 3 7 2.77614 7 2.5C7 2.22386 6.77614 2 6.5 2H3ZM12.8536 2.14645C12.9015 2.19439 12.9377 2.24964 12.9621 2.30861C12.9861 2.36669 12.9996 2.4303 13 2.497L13 2.5V2.50049V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3.70711L6.85355 8.85355C6.65829 9.04882 6.34171 9.04882 6.14645 8.85355C5.95118 8.65829 5.95118 8.34171 6.14645 8.14645L11.2929 3H9.5C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2H12.4999H12.5C12.5678 2 12.6324 2.01349 12.6914 2.03794C12.7504 2.06234 12.8056 2.09851 12.8536 2.14645Z"
fill="currentColor"
stroke="black"
strokeWidth=".5"
></path>
</svg>
</a>
</div>
</div>
)
}
return children
}