fix: harden external protocol handling and unblock dev:electron (#4863)

* fix(desktop): harden external protocol handling

* fix(build): avoid optional peer resolution in dev
This commit is contained in:
DIYgod 2026-02-20 21:36:23 +08:00 committed by GitHub
parent 8963930f29
commit b465ec1463
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 80 additions and 16 deletions

View File

@ -18,6 +18,7 @@ export default defineConfig({
"@pkg": resolve("./package.json"),
"@locales": resolve("../../locales"),
"~": resolve("./layer/main/src"),
"utf-8-validate": resolve("./layer/main/src/shims/utf-8-validate.cjs"),
},
},
define: {

View File

@ -82,19 +82,24 @@ class WindowManagerStatic {
refreshBound(window, this.config.refreshBoundDelay)
})
window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: "deny" }
})
const handleExternalProtocol = async (e: Event, url: string, window: BrowserWindow) => {
const { protocol } = new URL(url)
if (this.config.ignoreProtocols.includes(protocol.slice(0, -1) as any)) {
return
const parseProtocol = (url: string) => {
try {
return new URL(url).protocol.slice(0, -1)
} catch {
logger.warn("Blocked external URL with invalid format", { url })
return null
}
e.preventDefault()
}
const isIgnoredProtocol = (
protocol: string,
): protocol is (typeof this.config.ignoreProtocols)[number] => {
return this.config.ignoreProtocols.includes(
protocol as (typeof this.config.ignoreProtocols)[number],
)
}
const confirmAndOpenExternalProtocol = async (url: string) => {
const caller = callWindowExpose(window)
const confirm = await caller.dialog.ask({
title: t("dialog.openExternalApp.title"),
@ -108,15 +113,57 @@ class WindowManagerStatic {
if (!confirm) {
return
}
shell.openExternal(url)
void shell.openExternal(url)
}
window.webContents.setWindowOpenHandler((details) => {
const protocol = parseProtocol(details.url)
if (!protocol) {
return { action: "deny" }
}
if (protocol === "http" || protocol === "https") {
void shell.openExternal(details.url)
return { action: "deny" }
}
if (isIgnoredProtocol(protocol)) {
logger.warn("Blocked window.open for ignored protocol", {
protocol,
url: details.url,
})
return { action: "deny" }
}
void confirmAndOpenExternalProtocol(details.url)
return { action: "deny" }
})
const handleExternalProtocol = async (e: Event, url: string) => {
const protocol = parseProtocol(url)
if (!protocol) {
e.preventDefault()
return
}
if (isIgnoredProtocol(protocol)) {
return
}
e.preventDefault()
await confirmAndOpenExternalProtocol(url)
}
// Handle main window external links
window.webContents.on("will-navigate", (e, url) => handleExternalProtocol(e, url, window))
window.webContents.on("will-navigate", (e, url) => {
void handleExternalProtocol(e, url)
})
// Handle webview external links
window.webContents.on("did-attach-webview", (_, webContents) => {
webContents.on("will-navigate", (e, url) => handleExternalProtocol(e, url, window))
webContents.on("will-navigate", (e, url) => {
void handleExternalProtocol(e, url)
})
})
if (isWindows) {

View File

@ -0,0 +1,16 @@
"use strict"
const { isUtf8 } = require("node:buffer")
module.exports = function isValidUTF8(buffer) {
if (typeof isUtf8 === "function") {
return isUtf8(buffer)
}
try {
new TextDecoder("utf-8", { fatal: true }).decode(buffer)
return true
} catch {
return false
}
}

View File

@ -5,7 +5,7 @@ import type { env as EnvType } from "@follow/shared/env.desktop"
import legacy from "@vitejs/plugin-legacy"
import { minify as htmlMinify } from "html-minifier-terser"
import { cyan, dim, green } from "kolorist"
import { parseHTML } from "linkedom"
import { parseHTML } from "linkedom/worker"
import { join, resolve } from "pathe"
import type { PluginOption, ResolvedConfig, ViteDevServer } from "vite"
import { defineConfig, loadEnv } from "vite"

View File

@ -1,7 +1,7 @@
import { Readability } from "@mozilla/readability"
import chardet from "chardet"
import DOMPurify from "dompurify"
import { parseHTML } from "linkedom"
import { parseHTML } from "linkedom/worker"
const isDev = process.env.NODE_ENV === "development"