Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions apps/desktop/electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { app, BrowserWindow, dialog, ipcMain, Menu, protocol, Tray } from 'electron'
import { app, BrowserWindow, dialog, ipcMain, Menu, protocol, shell, Tray } from 'electron'
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import fs from 'node:fs/promises'
import { createInterface, type Interface as ReadlineInterface } from 'node:readline'
import path from 'node:path'
import type { OpenDialogOptions, SaveDialogOptions } from '../src/shared/contracts/platform'
import { resolveLinuxOrtSidecar } from './linux-cuda-runtime.mjs'
import { isExternalBrowserUrl, isInternalNavigationUrl, type NavigationPolicy } from './navigation-policy'

type RpcResponse = {
id?: number
Expand Down Expand Up @@ -33,6 +34,11 @@ const appDisplayName = process.env.MODFORGE_APP_NAME?.trim() || 'ModForge Studio
const appDesktopId = process.env.MODFORGE_DESKTOP_ID?.trim() || 'io.github.Arborsm.ModForgeStudio'
const isDev = !app.isPackaged
const devUrl = process.env.VITE_DEV_SERVER_URL ?? 'http://127.0.0.1:5173'
const appFilePath = path.resolve(__dirname, '../dist/index.html')
const navigationPolicy: NavigationPolicy = {
devUrl: isDev ? devUrl : undefined,
appFilePath,
}
const windowCloseRequestTimeoutMs = 1500
const sidecarStopTimeoutMs = 2500
let mainWindow: BrowserWindow | null = null
Expand Down Expand Up @@ -370,6 +376,8 @@ function createMainWindow() {
},
})

applyNavigationPolicy(mainWindow)

mainWindow.once('ready-to-show', () => mainWindow?.show())
mainWindow.once('closed', () => {
mainWindow = null
Expand Down Expand Up @@ -399,10 +407,38 @@ function createMainWindow() {
if (isDev) {
void mainWindow.loadURL(devUrl)
} else {
void mainWindow.loadFile(path.resolve(__dirname, '../dist/index.html'))
void mainWindow.loadFile(appFilePath)
}
}

function applyNavigationPolicy(window: BrowserWindow) {
window.webContents.setWindowOpenHandler(({ url }) => {
openInSystemBrowser(url)
return { action: 'deny' }
})

window.webContents.on('will-navigate', (event, url) => {
if (isInternalNavigationUrl(url, navigationPolicy)) {
return
}

event.preventDefault()
openInSystemBrowser(url)
})

window.webContents.on('will-attach-webview', (event) => {
event.preventDefault()
})
}

function openInSystemBrowser(url: string) {
if (!isExternalBrowserUrl(url)) {
return
}

void shell.openExternal(url)
}

function createTray() {
const iconPath = resolveWindowIconPath()
tray = new Tray(iconPath)
Expand Down
52 changes: 52 additions & 0 deletions apps/desktop/electron/navigation-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Navigation policy for the Electron renderer. The renderer holds a privileged
* preload bridge, so it must stay on the local application document: any other
* target is handed to the system browser instead of being loaded in-app.
*/
export type NavigationPolicy = {
/** Dev server document URL, when the app runs against the Vite dev server. */
devUrl?: string
/** Absolute path of the packaged renderer entry document. */
appFilePath?: string
}

function normalizeFileUrl(value: string) {
return decodeURIComponent(value).replace(/\\/gu, '/')
}

/** True when a navigation target is the application document itself. */
export function isInternalNavigationUrl(target: string, policy: NavigationPolicy) {
let url: URL
try {
url = new URL(target)
} catch {
return false
}

if (url.protocol === 'file:') {
if (!policy.appFilePath) {
return false
}
return normalizeFileUrl(url.pathname) === normalizeFileUrl(`/${policy.appFilePath.replace(/^\/+/u, '')}`)
}

if (!policy.devUrl) {
return false
}

try {
return url.origin === new URL(policy.devUrl).origin
} catch {
return false
}
}

/** True when a navigation target may be handed to the system browser. */
export function isExternalBrowserUrl(target: string) {
try {
const { protocol } = new URL(target)
return protocol === 'http:' || protocol === 'https:'
} catch {
return false
}
}
Loading
Loading