diff --git a/apps/x/apps/main/src/browser/control-service.ts b/apps/x/apps/main/src/browser/control-service.ts index 04edd93e..55f76d35 100644 --- a/apps/x/apps/main/src/browser/control-service.ts +++ b/apps/x/apps/main/src/browser/control-service.ts @@ -2,40 +2,7 @@ import { BrowserWindow } from 'electron'; import type { IBrowserControlService } from '@x/core/dist/application/browser-control/service.js'; import type { BrowserControlAction, BrowserControlInput, BrowserControlResult } from '@x/shared/dist/browser-control.js'; import { browserViewManager } from './view.js'; - -const SEARCH_ENGINE_BASE_URL = 'https://www.google.com/search?q='; - -function normalizeNavigationTarget(target: string): string { - const trimmed = target.trim(); - if (!trimmed) { - throw new Error('Navigation target cannot be empty.'); - } - - const lower = trimmed.toLowerCase(); - if ( - lower.startsWith('javascript:') - || lower.startsWith('file://') - || lower.startsWith('chrome://') - || lower.startsWith('chrome-extension://') - ) { - throw new Error('That URL scheme is not allowed in the embedded browser.'); - } - - if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) { - return trimmed; - } - - const looksLikeHost = - trimmed.startsWith('localhost') - || /^[\w.-]+\.[a-z]{2,}/i.test(trimmed) - || /^\d{1,3}(?:\.\d{1,3}){3}(?::\d+)?(?:\/.*)?$/.test(trimmed); - - if (looksLikeHost && !/\s/.test(trimmed)) { - return trimmed; - } - - return `${SEARCH_ENGINE_BASE_URL}${encodeURIComponent(trimmed)}`; -} +import { normalizeNavigationTarget } from './navigation.js'; function emitPaneState(open: boolean): void { const windows = BrowserWindow.getAllWindows(); diff --git a/apps/x/apps/main/src/browser/navigation.ts b/apps/x/apps/main/src/browser/navigation.ts new file mode 100644 index 00000000..c99165ab --- /dev/null +++ b/apps/x/apps/main/src/browser/navigation.ts @@ -0,0 +1,33 @@ +const SEARCH_ENGINE_BASE_URL = 'https://www.google.com/search?q='; + +export function normalizeNavigationTarget(target: string): string { + const trimmed = target.trim(); + if (!trimmed) { + throw new Error('Navigation target cannot be empty.'); + } + + const lower = trimmed.toLowerCase(); + if ( + lower.startsWith('javascript:') + || lower.startsWith('file://') + || lower.startsWith('chrome://') + || lower.startsWith('chrome-extension://') + ) { + throw new Error('That URL scheme is not allowed in the embedded browser.'); + } + + if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) { + return trimmed; + } + + const looksLikeHost = + trimmed.startsWith('localhost') + || /^[\w.-]+\.[a-z]{2,}/i.test(trimmed) + || /^\d{1,3}(?:\.\d{1,3}){3}(?::\d+)?(?:\/.*)?$/.test(trimmed); + + if (looksLikeHost && !/\s/.test(trimmed)) { + return trimmed; + } + + return `${SEARCH_ENGINE_BASE_URL}${encodeURIComponent(trimmed)}`; +} diff --git a/apps/x/apps/main/src/browser/view.ts b/apps/x/apps/main/src/browser/view.ts index 6b4978bb..75a55cd0 100644 --- a/apps/x/apps/main/src/browser/view.ts +++ b/apps/x/apps/main/src/browser/view.ts @@ -7,6 +7,7 @@ import type { BrowserState, BrowserTabState, } from '@x/shared/dist/browser-control.js'; +import { normalizeNavigationTarget } from './navigation.js'; export type { BrowserPageSnapshot, BrowserState, BrowserTabState }; @@ -478,14 +479,6 @@ export class BrowserViewManager extends EventEmitter { this.snapshotCache.delete(tabId); } - private normalizeUrl(rawUrl: string): string { - let url = rawUrl.trim(); - if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) { - url = `https://${url}`; - } - return url; - } - private isEmbeddedTabUrl(url: string): boolean { return /^https?:\/\//i.test(url) || url === 'about:blank'; } @@ -613,7 +606,7 @@ export class BrowserViewManager extends EventEmitter { const targetUrl = initialUrl === 'about:blank' ? HOME_URL - : this.normalizeUrl(initialUrl); + : normalizeNavigationTarget(initialUrl); void tab.view.webContents.loadURL(targetUrl).catch(() => { this.emitState(); }); @@ -792,7 +785,7 @@ export class BrowserViewManager extends EventEmitter { try { const activeTab = this.getActiveTab() ?? this.ensureInitialTab(); this.invalidateSnapshot(activeTab.id); - await activeTab.view.webContents.loadURL(this.normalizeUrl(rawUrl)); + await activeTab.view.webContents.loadURL(normalizeNavigationTarget(rawUrl)); return { ok: true }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) };