From fb9bec8ed70b9405362d9f2e14b755d333efd4fe Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 4 May 2026 17:49:43 +0200 Subject: [PATCH] warn before opening external app-specific URIs --- tabby-core/src/api/platform.ts | 2 +- .../src/services/platform.service.ts | 41 ++++++++++++++++++- tabby-web/src/platform.ts | 2 +- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/tabby-core/src/api/platform.ts b/tabby-core/src/api/platform.ts index 4088e22a..4a8e1eb3 100644 --- a/tabby-core/src/api/platform.ts +++ b/tabby-core/src/api/platform.ts @@ -262,7 +262,7 @@ export abstract class PlatformService { abstract getOSRelease (): string abstract getAppVersion (): string - abstract openExternal (url: string): void + abstract openExternal (url: string): Promise abstract listFonts (): Promise abstract setErrorHandler (handler: (_: any) => void): void abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void diff --git a/tabby-electron/src/services/platform.service.ts b/tabby-electron/src/services/platform.service.ts index a25227d4..7915503e 100644 --- a/tabby-electron/src/services/platform.service.ts +++ b/tabby-electron/src/services/platform.service.ts @@ -25,6 +25,7 @@ try { @Injectable({ providedIn: 'root' }) export class ElectronPlatformService extends PlatformService { supportsWindowControls = true + private safeExternalSchemes = new Set(['http', 'https', 'ftp', 'mailto']) private configPath: string constructor ( @@ -140,8 +141,44 @@ export class ElectronPlatformService extends PlatformService { this.electron.shell.showItemInFolder(p) } - openExternal (url: string): void { - this.electron.shell.openExternal(url) + async openExternal (url: string): Promise { + const scheme = this.getExternalScheme(url) + if (scheme && this.safeExternalSchemes.has(scheme)) { + await this.electron.shell.openExternal(url) + } else { + await this.confirmAndOpenExternal(url) + } + } + + private getExternalScheme (url: string): string | null { + try { + const protocol = new URL(url.trim()).protocol + return protocol ? protocol.replace(':', '').toLowerCase() : null + } catch { + return null + } + } + + private async confirmAndOpenExternal (url: string): Promise { + const scheme = this.getExternalScheme(url) + const result = await this.electron.dialog.showMessageBox( + this.hostWindow.getWindow(), + { + type: 'warning', + message: this.translate.instant(`Open this app-specific "${scheme}" URI?`), + detail: url, + buttons: [ + this.translate.instant('Open'), + this.translate.instant('Cancel'), + ], + defaultId: 0, + cancelId: 1, + }, + ) + + if (result.response === 0) { + await this.electron.shell.openExternal(url) + } } openPath (p: string): void { diff --git a/tabby-web/src/platform.ts b/tabby-web/src/platform.ts index f1592382..dd45b62f 100644 --- a/tabby-web/src/platform.ts +++ b/tabby-web/src/platform.ts @@ -55,7 +55,7 @@ export class WebPlatformService extends PlatformService { return '1.0' } - openExternal (url: string): void { + async openExternal (url: string): Promise { window.open(url) }