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:
+47
@@ -1,8 +1,34 @@
|
||||
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();
|
||||
}
|
||||
@@ -45,3 +71,24 @@ export function getWorkspaceActivePaseoBrowserWebContents(): WebContents | 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" };
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { app, Menu, BrowserWindow, ipcMain } from "electron";
|
||||
import { getWorkspaceActivePaseoBrowserWebContents } from "./browser-webviews.js";
|
||||
import { getWorkspaceActivePaseoBrowserWebContents } from "./browser-webviews/index.js";
|
||||
|
||||
interface ShowContextMenuInput {
|
||||
kind?: "terminal";
|
||||
|
||||
Reference in New Issue
Block a user