import React, { useEffect, useRef, useState } from 'react'; import { Box, Icon, Icons, Text, color } from 'folds'; import { Room } from 'matrix-js-sdk'; import { ClientWidgetApi, Widget } from 'matrix-widget-api'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { GeneralWidgetDriver } from './GeneralWidgetDriver'; import { isWidgetUrlSafe } from './widgetUtils'; type RoomWidgetViewProps = { room: Room; widget: Widget; }; // Hosts one room widget in a sandboxed iframe via ClientWidgetApi (so widgets // that wait on the client handshake load), with a conservative capability driver. // Re-mounts only when the widget id or its (template) URL changes — not on every // unrelated room-state update — so viewing a widget doesn't reload constantly. export function RoomWidgetView({ room, widget }: RoomWidgetViewProps) { const mx = useMatrixClient(); const containerRef = useRef(null); const widgetRef = useRef(widget); widgetRef.current = widget; const [blocked, setBlocked] = useState(false); useEffect(() => { const container = containerRef.current; if (!container) return undefined; const current = widgetRef.current; const completeUrl = current.getCompleteUrl({ currentUserId: mx.getSafeUserId(), widgetRoomId: room.roomId, deviceId: mx.getDeviceId() ?? undefined, baseUrl: mx.baseUrl, }); // Security: never render a same-origin widget with allow-same-origin (a // same-origin frame could break out of the sandbox against our own origin). if (!isWidgetUrlSafe(completeUrl, window.location.origin)) { setBlocked(true); return undefined; } setBlocked(false); const iframe = document.createElement('iframe'); iframe.title = current.name || 'Widget'; iframe.sandbox.value = 'allow-forms allow-scripts allow-same-origin allow-popups allow-downloads'; iframe.allow = 'autoplay; clipboard-write;'; iframe.src = completeUrl; iframe.style.width = '100%'; iframe.style.height = '100%'; iframe.style.border = 'none'; container.append(iframe); const clientApi = new ClientWidgetApi(current, iframe, new GeneralWidgetDriver()); clientApi.setViewedRoomId(room.roomId); return () => { clientApi.stop(); iframe.remove(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [mx, room.roomId, widget.id, widget.templateUrl]); if (blocked) { return ( This widget can't be loaded because its URL is on this app's own origin. ); } return ; }