Open browser links in workspace tabs (#1375)

* Open browser window requests as workspace tabs

* Tighten browser new-tab request boundaries
This commit is contained in:
Mohamed Boudra
2026-06-06 19:30:21 +08:00
committed by GitHub
parent fd022bc44b
commit 9a8912b3ef
9 changed files with 299 additions and 54 deletions
@@ -0,0 +1,94 @@
import { webContents as allWebContents, type WebContents } from "electron";
import {
BROWSER_NEW_TAB_REQUEST_EVENT,
handleBrowserWindowOpenRequest,
isAllowedBrowserWebviewUrl,
} from "./window-open.js";
export { BROWSER_NEW_TAB_REQUEST_EVENT, handleBrowserWindowOpenRequest };
const browserIdsByWebContentsId = new Map<number, string>();
let workspaceActiveBrowserId: string | null = null;
function getBrowserIdFromWebviewPartition(partition: string | undefined): string | null {
const prefix = "persist:paseo-browser-";
if (!partition?.startsWith(prefix)) {
return null;
}
const browserId = partition.slice(prefix.length).trim();
return browserId.length > 0 ? browserId : null;
}
export function readBrowserIdFromWebviewAttach(input: {
src?: string;
partition?: string;
}): string | null {
if (!isAllowedBrowserWebviewUrl(input.src)) {
return null;
}
return getBrowserIdFromWebviewPartition(input.partition);
}
export function listRegisteredPaseoBrowserIds(): string[] {
return Array.from(new Set(browserIdsByWebContentsId.values())).sort();
}
export function registerPaseoBrowserWebContents(contents: WebContents, browserId: string): void {
browserIdsByWebContentsId.set(contents.id, browserId);
contents.once("destroyed", () => {
browserIdsByWebContentsId.delete(contents.id);
if (workspaceActiveBrowserId === browserId) {
workspaceActiveBrowserId = null;
}
});
}
export function getPaseoBrowserIdForWebContents(contents: WebContents | null): string | null {
if (!contents || contents.isDestroyed()) {
return null;
}
return browserIdsByWebContentsId.get(contents.id) ?? null;
}
export function setWorkspaceActivePaseoBrowserId(browserId: string | null): void {
workspaceActiveBrowserId = browserId;
}
export function getPaseoBrowserWebContents(browserId: string): WebContents | null {
for (const [contentsId, registeredBrowserId] of browserIdsByWebContentsId) {
if (registeredBrowserId !== browserId) continue;
const contents = allWebContents.fromId(contentsId);
if (contents && !contents.isDestroyed()) {
return contents;
}
}
return null;
}
export function getWorkspaceActivePaseoBrowserWebContents(): WebContents | null {
if (!workspaceActiveBrowserId) {
return null;
}
return getPaseoBrowserWebContents(workspaceActiveBrowserId);
}
function preventUnsafeBrowserWebviewNavigation(
event: { preventDefault: () => void },
url: string | undefined,
): void {
if (!isAllowedBrowserWebviewUrl(url)) {
event.preventDefault();
}
}
export function registerBrowserWebviewNavigationGuards(contents: WebContents): void {
contents.on("will-navigate", (event) => {
preventUnsafeBrowserWebviewNavigation(event, event.url);
});
contents.on("will-frame-navigate", (event) => {
preventUnsafeBrowserWebviewNavigation(event, event.url);
});
contents.on("will-redirect", (event) => {
preventUnsafeBrowserWebviewNavigation(event, event.url);
});
}
@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from "vitest";
import { handleBrowserWindowOpenRequest } from ".";
describe("browser webview window-open requests", () => {
it("denies Electron window creation and requests a Paseo browser tab", () => {
const requestNewTab = vi.fn();
const result = handleBrowserWindowOpenRequest({
url: "https://example.com/target",
sourceBrowserId: "browser-1",
requestNewTab,
});
expect(result).toEqual({ action: "deny" });
expect(requestNewTab).toHaveBeenCalledWith({
sourceBrowserId: "browser-1",
url: "https://example.com/target",
});
});
it("denies unsupported window-open requests before asking for a Paseo browser tab", () => {
const requestNewTab = vi.fn();
const result = handleBrowserWindowOpenRequest({
url: "file:///etc/passwd",
sourceBrowserId: "browser-1",
requestNewTab,
});
expect(result).toEqual({ action: "deny" });
expect(requestNewTab).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,36 @@
export const BROWSER_NEW_TAB_REQUEST_EVENT = "paseo:event:browser-new-tab-request";
export interface BrowserNewTabRequestPayload {
sourceBrowserId: string;
url: string;
}
export function isAllowedBrowserWebviewUrl(value: string | undefined): boolean {
if (!value) {
return true;
}
try {
const parsed = new URL(value);
return (
parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.href === "about:blank"
);
} catch {
return false;
}
}
export function handleBrowserWindowOpenRequest(input: {
url: string;
sourceBrowserId: string | null;
requestNewTab: (payload: BrowserNewTabRequestPayload) => void;
}): { action: "deny" } {
if (!isAllowedBrowserWebviewUrl(input.url) || !input.sourceBrowserId) {
return { action: "deny" };
}
input.requestNewTab({
sourceBrowserId: input.sourceBrowserId,
url: input.url,
});
return { action: "deny" };
}