feat: improve electron sandbox (#14156)

This commit is contained in:
DarkSky
2025-12-27 03:23:28 +08:00
committed by GitHub
parent 3fe8923fc3
commit 4eed92cebf
32 changed files with 570 additions and 213 deletions

View File

@@ -82,7 +82,8 @@ export class EmbedFigmaBlockComponent extends EmbedBlockComponent<EmbedFigmaMode
<div class="affine-embed-figma-iframe-container">
<iframe
src=${`https://www.figma.com/embed?embed_host=blocksuite&url=${url}`}
allowfullscreen
sandbox="allow-same-origin allow-scripts allow-presentation"
allow="fullscreen"
loading="lazy"
credentialless
></iframe>

View File

@@ -0,0 +1,79 @@
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
import {
type EmbedIframeUrlValidationOptions,
validateEmbedIframeUrl,
} from '../../utils';
const BILIBILI_DEFAULT_WIDTH_IN_SURFACE = 800;
const BILIBILI_DEFAULT_HEIGHT_IN_SURFACE = 450;
const BILIBILI_DEFAULT_HEIGHT_IN_NOTE = 450;
const BILIBILI_DEFAULT_WIDTH_PERCENT = 100;
const bilibiliValidationOptions: EmbedIframeUrlValidationOptions = {
protocols: ['https:'],
hostnames: ['player.bilibili.com', 'www.bilibili.com', 'bilibili.com'],
};
const biliPlayerValidationOptions: EmbedIframeUrlValidationOptions = {
protocols: ['https:'],
hostnames: ['player.bilibili.com'],
};
const AV_REGEX = /av([0-9]+)/i;
const BV_REGEX = /(BV[0-9A-Za-z]{10})/;
const extractAvid = (url: string) => {
const match = url.match(AV_REGEX);
return match ? match[1] : undefined;
};
const extractBvid = (url: string) => {
const match = url.match(BV_REGEX);
return match ? match[1] : undefined;
};
const buildBiliPlayerEmbedUrl = (url: string) => {
// If the user pasted the embed URL directly, keep it
if (validateEmbedIframeUrl(url, biliPlayerValidationOptions)) {
return url;
}
const avid = extractAvid(url);
if (avid) {
const params = new URLSearchParams({
aid: avid,
autoplay: '0',
});
return `https://player.bilibili.com/player.html?${params.toString()}`;
}
const bvid = extractBvid(url);
if (bvid) {
const params = new URLSearchParams({
bvid,
autoplay: '0',
});
return `https://player.bilibili.com/player.html?${params.toString()}`;
}
return undefined;
};
const bilibiliConfig = {
name: 'bilibili',
match: (url: string) =>
validateEmbedIframeUrl(url, bilibiliValidationOptions) &&
(!!extractAvid(url) || !!extractBvid(url)),
buildOEmbedUrl: buildBiliPlayerEmbedUrl,
useOEmbedUrlDirectly: true,
options: {
widthInSurface: BILIBILI_DEFAULT_WIDTH_IN_SURFACE,
heightInSurface: BILIBILI_DEFAULT_HEIGHT_IN_SURFACE,
heightInNote: BILIBILI_DEFAULT_HEIGHT_IN_NOTE,
widthPercent: BILIBILI_DEFAULT_WIDTH_PERCENT,
allow: 'clipboard-write; encrypted-media; picture-in-picture',
sandbox: 'allow-same-origin allow-scripts',
style: 'border: none; border-radius: 8px;',
allowFullscreen: true,
},
};
export const BilibiliEmbedConfig = EmbedIframeConfigExtension(bilibiliConfig);

View File

@@ -67,8 +67,9 @@ const genericConfig = {
heightInNote: GENERIC_DEFAULT_HEIGHT_IN_NOTE,
allowFullscreen: true,
style: 'border: none; border-radius: 8px;',
allow: 'clipboard-read; clipboard-write; picture-in-picture;',
allow: '',
referrerpolicy: 'no-referrer-when-downgrade',
sandbox: 'allow-scripts',
},
};

View File

@@ -1,3 +1,4 @@
import { BilibiliEmbedConfig } from './bilibili';
import { ExcalidrawEmbedConfig } from './excalidraw';
import { GenericEmbedConfig } from './generic';
import { GoogleDocsEmbedConfig } from './google-docs';
@@ -11,5 +12,6 @@ export const EmbedIframeConfigExtensions = [
MiroEmbedConfig,
ExcalidrawEmbedConfig,
GoogleDocsEmbedConfig,
BilibiliEmbedConfig,
GenericEmbedConfig,
];

View File

@@ -23,7 +23,7 @@ import {
type ReadonlySignal,
signal,
} from '@preact/signals-core';
import { html } from 'lit';
import { html, nothing } from 'lit';
import { query } from 'lit/decorators.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
@@ -45,6 +45,10 @@ import { safeGetIframeSrc } from './utils.js';
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
const TRUSTED_SANDBOX =
'allow-same-origin allow-scripts allow-forms allow-presentation';
const UNTRUSTED_SANDBOX = 'allow-scripts';
export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIframeBlockModel> {
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
() => ({
@@ -89,6 +93,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
});
protected iframeOptions: IframeOptions | undefined = undefined;
private currentConfigName: string | undefined;
get embedIframeService() {
return this.std.get(EmbedIframeService);
@@ -279,6 +284,10 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
const config = this.embedIframeService?.getConfig(url);
if (config) {
this.iframeOptions = config.options;
this.currentConfigName = config.name;
} else {
this.iframeOptions = undefined;
this.currentConfigName = undefined;
}
};
@@ -328,26 +337,46 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
referrerpolicy,
scrolling,
allowFullscreen,
sandbox,
} = this.iframeOptions ?? {};
const width = `${widthPercent}%`;
// if the block is in the surface, use 100% as the height
// otherwise, use the heightInNote
const height = this.inSurface ? '100%' : heightInNote;
return html`
<iframe
const sandboxValue =
sandbox ??
(this.currentConfigName === 'generic'
? UNTRUSTED_SANDBOX
: TRUSTED_SANDBOX);
const sourceHost = this._getSourceHost();
return html`<iframe
width=${width ?? DEFAULT_IFRAME_WIDTH}
height=${height ?? DEFAULT_IFRAME_HEIGHT}
?allowfullscreen=${allowFullscreen}
loading="lazy"
frameborder="0"
credentialless
sandbox=${sandboxValue}
src=${ifDefined(iframeUrl)}
allow=${ifDefined(allow)}
referrerpolicy=${ifDefined(referrerpolicy)}
scrolling=${ifDefined(scrolling)}
style=${ifDefined(style)}
></iframe>
`;
${sourceHost
? html`<div class="affine-embed-iframe-source">${sourceHost}</div>`
: nothing}`;
};
private readonly _getSourceHost = () => {
const url = this.model.props.url ?? this.model.props.iframeUrl;
if (!url) return null;
try {
const parsed = new URL(url);
return parsed.hostname;
} catch {
return null;
}
};
private readonly _renderContent = () => {

View File

@@ -23,6 +23,19 @@ export const embedIframeBlockStyles = css`
height: 100%;
display: none;
}
.affine-embed-iframe-source {
position: absolute;
left: 8px;
bottom: 8px;
padding: 2px 6px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
border-radius: 4px;
font-size: 12px;
line-height: 16px;
pointer-events: none;
}
.affine-embed-iframe-block-overlay.show {
display: block;
}

View File

@@ -124,7 +124,8 @@ export class EmbedLoomBlockComponent extends EmbedBlockComponent<
<iframe
src=${`https://www.loom.com/embed/${videoId}?hide_title=true`}
frameborder="0"
allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
sandbox="allow-scripts allow-same-origin allow-presentation"
loading="lazy"
credentialless
></iframe>

View File

@@ -148,8 +148,8 @@ export class EmbedYoutubeBlockComponent extends EmbedBlockComponent<
type="text/html"
src=${`https://www.youtube.com/embed/${videoId}`}
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
sandbox="allow-scripts allow-same-origin allow-presentation"
loading="lazy"
credentialless
></iframe>