mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-05-18 05:05:36 +02:00
editor: sandbox clip iframe (#9521)
* editor: sandbox clip iframe Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> * editor: render webclips via bloburl with strict sandboxing --------- Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
This commit is contained in:
@@ -28,6 +28,7 @@ import { Icon } from "@notesnook/ui";
|
||||
import { Resizer } from "../../components/resizer/index.js";
|
||||
import { useThemeEngineStore } from "@notesnook/theme";
|
||||
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
|
||||
import { getSandboxFeatures } from "../../utils/sandbox.js";
|
||||
|
||||
export function EmbedComponent(
|
||||
props: ReactNodeViewProps<EmbedAttributes & EmbedAlignmentOptions>
|
||||
@@ -157,27 +158,6 @@ export function EmbedComponent(
|
||||
);
|
||||
}
|
||||
|
||||
function getSandboxFeatures(src: string) {
|
||||
const features = [];
|
||||
try {
|
||||
const url = new URL(src);
|
||||
if (url.protocol === "http:" || url.protocol === "https:")
|
||||
features.push(
|
||||
"allow-scripts",
|
||||
"allow-same-origin",
|
||||
"allow-popups",
|
||||
"allow-popups-to-escape-sandbox",
|
||||
"allow-forms",
|
||||
"allow-modals",
|
||||
"allow-downloads",
|
||||
"allow-presentation"
|
||||
);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return features.join(" ");
|
||||
}
|
||||
|
||||
function isYouTubeEmbed(urlString: string) {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Box, Flex, Text } from "@theme-ui/components";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ReactNodeViewProps } from "../react/index.js";
|
||||
import { Icons } from "../../toolbar/index.js";
|
||||
import { Icon } from "@notesnook/ui";
|
||||
@@ -36,45 +36,34 @@ const FAILED_CONTENT = `<html><head>
|
||||
|
||||
export function WebClipComponent(props: ReactNodeViewProps<WebClipAttributes>) {
|
||||
const { editor, selected, node, updateAttributes } = props;
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const embedRef = useRef<HTMLIFrameElement>(null);
|
||||
const resizeObserverRef = useRef<ResizeObserver>();
|
||||
const { src, title, fullscreen, progress } = node.attrs;
|
||||
const { src, title, progress } = node.attrs;
|
||||
const [source, setSource] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (!isLoading) return;
|
||||
|
||||
const html = await editor.storage
|
||||
.getAttachmentData?.(node.attrs)
|
||||
.catch(() => null);
|
||||
|
||||
const iframe = embedRef.current;
|
||||
if (!iframe || !iframe.contentDocument) return;
|
||||
iframe.contentDocument.open();
|
||||
iframe.contentDocument.write(
|
||||
typeof html !== "string" || !html ? FAILED_CONTENT : html
|
||||
const doc = new DOMParser().parseFromString(
|
||||
html || FAILED_CONTENT,
|
||||
"text/html"
|
||||
);
|
||||
iframe.contentDocument.close();
|
||||
iframe.contentDocument.head.innerHTML += `<base target="_blank">`;
|
||||
setIsLoading(false);
|
||||
doc.head.innerHTML += `<base target="_blank">`;
|
||||
const blob = new Blob([doc.documentElement.outerHTML], {
|
||||
type: "text/html"
|
||||
});
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
setSource(blobUrl);
|
||||
return () => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
};
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function fullscreenchanged() {
|
||||
if (document.fullscreenElement) {
|
||||
resizeObserverRef.current?.disconnect();
|
||||
resetIframeSize(embedRef.current);
|
||||
} else {
|
||||
if (!document.fullscreenElement) {
|
||||
updateAttributes({ fullscreen: false });
|
||||
resizeIframe(node.attrs, embedRef.current);
|
||||
|
||||
if (embedRef.current?.contentDocument) {
|
||||
resizeObserverRef.current?.observe(
|
||||
embedRef.current?.contentDocument?.body
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,20 +73,6 @@ export function WebClipComponent(props: ReactNodeViewProps<WebClipAttributes>) {
|
||||
};
|
||||
}, [updateAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embedRef.current?.contentDocument) {
|
||||
resizeObserverRef.current = new ResizeObserver(() => {
|
||||
setTimeout(() => resizeIframe(node.attrs, embedRef.current), 100);
|
||||
});
|
||||
resizeObserverRef.current.observe(
|
||||
embedRef.current?.contentDocument?.body
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
resizeObserverRef.current?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
@@ -183,20 +158,18 @@ export function WebClipComponent(props: ReactNodeViewProps<WebClipAttributes>) {
|
||||
>
|
||||
<Box sx={{ overflow: "hidden" }}>
|
||||
<iframe
|
||||
ref={embedRef}
|
||||
width="auto"
|
||||
frameBorder={"0"}
|
||||
scrolling={fullscreen ? "yes" : "no"}
|
||||
style={{ transformOrigin: "0 0", overflow: "hidden" }}
|
||||
onLoad={() => {
|
||||
if (fullscreen) return;
|
||||
|
||||
resizeIframe(node.attrs, embedRef.current);
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100vh",
|
||||
background: "var(--background)"
|
||||
}}
|
||||
src={source}
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{isLoading && (
|
||||
{!source && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
@@ -218,40 +191,3 @@ export function WebClipComponent(props: ReactNodeViewProps<WebClipAttributes>) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function resizeIframe(
|
||||
attributes: WebClipAttributes,
|
||||
iframe?: HTMLIFrameElement | null
|
||||
) {
|
||||
if (!iframe || !iframe.contentDocument || !iframe.contentDocument.body)
|
||||
return;
|
||||
|
||||
const height = attributes.height
|
||||
? parseInt(attributes.height)
|
||||
: iframe.contentDocument.body.scrollHeight;
|
||||
|
||||
const width = attributes.width
|
||||
? parseInt(attributes.width)
|
||||
: iframe.contentDocument.body.scrollWidth;
|
||||
|
||||
iframe.style.height = `${height}px`;
|
||||
iframe.style.width = `${width}px`;
|
||||
|
||||
const container = iframe.parentElement;
|
||||
if (!container || container.clientWidth > width) return;
|
||||
const scale = container.clientWidth / width;
|
||||
iframe.style.scale = `${scale}`;
|
||||
container.style.height = `${height * scale}px`;
|
||||
}
|
||||
|
||||
function resetIframeSize(iframe?: HTMLIFrameElement | null) {
|
||||
if (!iframe || !iframe.contentDocument || !iframe.contentDocument.body)
|
||||
return;
|
||||
|
||||
const height = iframe.contentDocument.body.scrollHeight;
|
||||
const width = iframe.contentDocument.body.scrollWidth;
|
||||
|
||||
iframe.style.height = `${height}px`;
|
||||
iframe.style.width = `${width}px`;
|
||||
iframe.style.scale = `1`;
|
||||
}
|
||||
|
||||
39
packages/editor/src/utils/sandbox.ts
Normal file
39
packages/editor/src/utils/sandbox.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function getSandboxFeatures(src: string) {
|
||||
const features = [];
|
||||
try {
|
||||
const url = new URL(src);
|
||||
if (url.protocol === "http:" || url.protocol === "https:")
|
||||
features.push(
|
||||
"allow-scripts",
|
||||
"allow-same-origin",
|
||||
"allow-popups",
|
||||
"allow-popups-to-escape-sandbox",
|
||||
"allow-forms",
|
||||
"allow-modals",
|
||||
"allow-downloads",
|
||||
"allow-presentation"
|
||||
);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return features.join(" ");
|
||||
}
|
||||
Reference in New Issue
Block a user