diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/index.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/index.ts index 68e96acf8..c8d3b3215 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/index.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/index.ts @@ -1,7 +1,9 @@ import { GoogleDriveEmbedConfig } from './google-drive'; +import { MiroEmbedConfig } from './miro'; import { SpotifyEmbedConfig } from './spotify'; export const EmbedIframeConfigExtensions = [ SpotifyEmbedConfig, GoogleDriveEmbedConfig, + MiroEmbedConfig, ]; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/miro.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/miro.ts new file mode 100644 index 000000000..da3f7b2ff --- /dev/null +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/miro.ts @@ -0,0 +1,44 @@ +import { EmbedIframeConfigExtension } from '../../extension/embed-iframe-config'; +import { + type EmbedIframeUrlValidationOptions, + validateEmbedIframeUrl, +} from '../../utils'; + +const MIRO_DEFAULT_WIDTH_IN_SURFACE = 640; +const MIRO_DEFAULT_HEIGHT_IN_SURFACE = 480; +const MIRO_DEFAULT_HEIGHT_IN_NOTE = 480; +const MIRO_DEFAULT_WIDTH_PERCENT = 100; + +// https://developers.miro.com/reference/getembeddata +const miroEndpoint = 'https://miro.com/api/v1/oembed'; + +const miroUrlValidationOptions: EmbedIframeUrlValidationOptions = { + protocols: ['https:'], + hostnames: ['miro.com'], +}; + +const miroConfig = { + name: 'miro', + match: (url: string) => validateEmbedIframeUrl(url, miroUrlValidationOptions), + buildOEmbedUrl: (url: string) => { + const match = validateEmbedIframeUrl(url, miroUrlValidationOptions); + if (!match) { + return undefined; + } + const encodedUrl = encodeURIComponent(url); + const oEmbedUrl = `${miroEndpoint}?url=${encodedUrl}`; + return oEmbedUrl; + }, + useOEmbedUrlDirectly: false, + options: { + widthInSurface: MIRO_DEFAULT_WIDTH_IN_SURFACE, + heightInSurface: MIRO_DEFAULT_HEIGHT_IN_SURFACE, + heightInNote: MIRO_DEFAULT_HEIGHT_IN_NOTE, + widthPercent: MIRO_DEFAULT_WIDTH_PERCENT, + allow: 'fullscreen; clipboard-read; clipboard-write', + style: 'border: 0; border-radius: 8px;', + allowFullscreen: true, + }, +}; + +export const MiroEmbedConfig = EmbedIframeConfigExtension(miroConfig); diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts index 4c0e31758..c0fa3f171 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts @@ -36,8 +36,7 @@ const spotifyConfig = { heightInSurface: SPOTIFY_DEFAULT_HEIGHT_IN_SURFACE, heightInNote: SPOTIFY_DEFAULT_HEIGHT_IN_NOTE, widthPercent: SPOTIFY_DEFAULT_WIDTH_PERCENT, - allow: - 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture', + allow: 'autoplay; clipboard-write; encrypted-media; picture-in-picture', style: 'border-radius: 8px;', allowFullscreen: true, }, diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-iframe-block.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-iframe-block.ts index f3ad8ec7e..fc03a0d4e 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-iframe-block.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-iframe-block.ts @@ -17,9 +17,13 @@ import { type ClassInfo, classMap } from 'lit/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import type { IframeOptions } from './extension/embed-iframe-config.js'; -import { EmbedIframeService } from './extension/embed-iframe-service.js'; +import { + type EmbedIframeData, + EmbedIframeService, +} from './extension/embed-iframe-service.js'; import { embedIframeBlockStyles } from './style.js'; import type { EmbedIframeStatusCardOptions } from './types.js'; +import { safeGetIframeSrc } from './utils.js'; export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error'; const DEFAULT_IFRAME_HEIGHT = 152; @@ -126,8 +130,9 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent { + const { iframe_url, html } = embedData ?? {}; + return iframe_url ?? (html && safeGetIframeSrc(html)); + }; + private readonly _updateIframeOptions = (url: string) => { const config = this.embedIframeService?.getConfig(url); if (config) { diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/utils.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/utils.ts index 9c0f246e8..f33c572c1 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/utils.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/utils.ts @@ -29,3 +29,33 @@ export function validateEmbedIframeUrl( return false; } } + +/** + * Safely extracts the src URL from an iframe HTML string + * @param htmlString The iframe HTML string to parse + * @param options Optional validation configuration + * @returns The validated src URL or undefined if validation fails + */ +export function safeGetIframeSrc(htmlString: string): string | undefined { + try { + // Create a DOMParser instance + const parser = new DOMParser(); + // Parse the HTML string + const doc = parser.parseFromString(htmlString, 'text/html'); + // Get the iframe element + const iframe = doc.querySelector('iframe'); + if (!iframe) { + return undefined; + } + + // Get the src attribute + const src = iframe.getAttribute('src'); + if (!src) { + return undefined; + } + + return src; + } catch { + return undefined; + } +}