refactor: 删除分屏功能 + 代码审计修复 + 安全加固
## 删除分屏功能 - 删除 ViewMode 'split' 类型,仅保留 editor/preview - 删除 splitRatio 状态和 Resizer 组件 - 删除滚动同步模块 (scrollSync.ts, rehypeSourceLine.ts) - 更新快捷键: Ctrl+1 编辑, Ctrl+2 预览 ## Bug 修复 - 修复 Toast setTimeout 内存泄漏 (App.tsx) - 修复 ModifiedBanner reload 更新错误标签 (改用 modifiedFilePath 匹配) - 修复 FileWatcher error 未重置 isSelfWriting (file-watcher.ts) - 修复另存为时未 stop watcher (ipc-handlers.ts) ## 死代码清理 (10 项) - 删除 main/ipc-channels.ts (与 shared/ 重复) - 删除 main/file-system.ts 未使用的 formatBytes - 删除 constants.ts 6 个未使用常量 - 删除 fileUtils.ts 未使用的 formatBytes - 删除 Icons.tsx 5 个未使用图标 (SplitView/ChevronUp/ChevronDown/X/ArrowUp/ArrowDown) - 删除 useCodeMirror 未使用的 getContent/scrollTo - 删除 TabBar 未使用的 menuRef - 删除 Tab.scrollLeft/previewScrollTop 字段 - 删除 tabStore 未使用的 getTabIndex - 删除 Toast/ModifiedBanner 多余 React import ## 安全加固 - ipc-handlers: 添加路径遍历防护 (validatePath 函数) - preload: openExternal 仅允许 http/https 协议 - window-manager: 启用 sandbox: true - preload: removeAllListeners 改为精确取消订阅 (返回 Unsubscribe 函数) ## 状态持久化 - activeTabId 持久化到 IndexedDB (刷新后恢复正确标签) - sidebar 状态持久化到 IndexedDB (isVisible/sidebarWidth) ## 代码优化 - preload 使用 IPC_CHANNELS 常量替代硬编码字符串 - ipc-handlers _event 类型改为 IpcMainInvokeEvent - settingsRepository 删除 splitRatio 字段
This commit is contained in:
+61
-48
@@ -1,62 +1,75 @@
|
||||
import { contextBridge, ipcRenderer, shell } from 'electron'
|
||||
|
||||
// M-01: 限制 removeAllListeners 只能操作白名单通道
|
||||
const ALLOWED_REMOVE_CHANNELS = new Set([
|
||||
'file:openInTab',
|
||||
'menu:save',
|
||||
'menu:saveAs',
|
||||
'menu:viewMode',
|
||||
'file:externallyModified',
|
||||
'sidebar:dirChanged',
|
||||
'window:confirmClose'
|
||||
])
|
||||
import { IPC_CHANNELS } from '../shared/ipc-channels'
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// File operations
|
||||
openFile: () => ipcRenderer.invoke('dialog:openFile'),
|
||||
readFile: (filePath: string) => ipcRenderer.invoke('file:read', filePath),
|
||||
saveFile: (data: { filePath: string | null; content: string }) => ipcRenderer.invoke('file:save', data),
|
||||
saveFileAs: (data: { content: string }) => ipcRenderer.invoke('file:saveAs', data),
|
||||
getCurrentPath: () => ipcRenderer.invoke('file:getCurrentPath'),
|
||||
getFileStats: (filePath: string) => ipcRenderer.invoke('file:stats', filePath),
|
||||
reloadFile: () => ipcRenderer.invoke('file:reload'),
|
||||
openFile: () => ipcRenderer.invoke(IPC_CHANNELS.DIALOG_OPEN_FILE),
|
||||
readFile: (filePath: string) => ipcRenderer.invoke(IPC_CHANNELS.FILE_READ, filePath),
|
||||
saveFile: (data: { filePath: string | null; content: string }) => ipcRenderer.invoke(IPC_CHANNELS.FILE_SAVE, data),
|
||||
saveFileAs: (data: { content: string }) => ipcRenderer.invoke(IPC_CHANNELS.FILE_SAVE_AS, data),
|
||||
getCurrentPath: () => ipcRenderer.invoke(IPC_CHANNELS.FILE_GET_CURRENT_PATH),
|
||||
getFileStats: (filePath: string) => ipcRenderer.invoke(IPC_CHANNELS.FILE_STATS, filePath),
|
||||
reloadFile: () => ipcRenderer.invoke(IPC_CHANNELS.FILE_RELOAD),
|
||||
|
||||
// Tab management
|
||||
tabSwitched: (filePath: string | null) => ipcRenderer.invoke('tab:switched', filePath),
|
||||
tabSwitched: (filePath: string | null) => ipcRenderer.invoke(IPC_CHANNELS.TAB_SWITCHED, filePath),
|
||||
|
||||
// Window control
|
||||
forceClose: () => ipcRenderer.invoke('window:forceClose'),
|
||||
cancelClose: () => ipcRenderer.invoke('window:cancelClose'),
|
||||
forceClose: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_FORCE_CLOSE),
|
||||
cancelClose: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_CANCEL_CLOSE),
|
||||
|
||||
// Shell
|
||||
openExternal: (url: string) => shell.openExternal(url),
|
||||
// Shell — 仅允许 http/https 协议
|
||||
openExternal: (url: string) => {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
||||
shell.openExternal(url)
|
||||
}
|
||||
} catch {
|
||||
// 无效 URL,忽略
|
||||
}
|
||||
},
|
||||
|
||||
// File Tree (Sidebar)
|
||||
readDirTree: (dirPath: string) => ipcRenderer.invoke('dir:readTree', dirPath),
|
||||
openFolderDialog: () => ipcRenderer.invoke('dir:openDialog'),
|
||||
watchDir: (dirPath: string) => ipcRenderer.invoke('dir:watch', dirPath),
|
||||
unwatchDir: () => ipcRenderer.invoke('dir:unwatch'),
|
||||
readDirTree: (dirPath: string) => ipcRenderer.invoke(IPC_CHANNELS.DIR_READ_TREE, dirPath),
|
||||
openFolderDialog: () => ipcRenderer.invoke(IPC_CHANNELS.DIR_OPEN_DIALOG),
|
||||
watchDir: (dirPath: string) => ipcRenderer.invoke(IPC_CHANNELS.DIR_WATCH, dirPath),
|
||||
unwatchDir: () => ipcRenderer.invoke(IPC_CHANNELS.DIR_UNWATCH),
|
||||
|
||||
// Events from main process
|
||||
onFileOpenInTab: (callback: (data: { filePath: string; content: string }) => void) =>
|
||||
ipcRenderer.on('file:openInTab', (_event, data) => callback(data)),
|
||||
onMenuSave: (callback: () => void) =>
|
||||
ipcRenderer.on('menu:save', () => callback()),
|
||||
onMenuSaveAs: (callback: () => void) =>
|
||||
ipcRenderer.on('menu:saveAs', () => callback()),
|
||||
onViewModeChange: (callback: (mode: string) => void) =>
|
||||
ipcRenderer.on('menu:viewMode', (_event, mode) => callback(mode)),
|
||||
onExternalModification: (callback: (filePath: string) => void) =>
|
||||
ipcRenderer.on('file:externallyModified', (_event, filePath) => callback(filePath)),
|
||||
onDirChanged: (callback: () => void) =>
|
||||
ipcRenderer.on('sidebar:dirChanged', () => callback()),
|
||||
onConfirmClose: (callback: () => void) =>
|
||||
ipcRenderer.on('window:confirmClose', () => callback()),
|
||||
|
||||
// M-01: 只允许移除白名单通道的监听器
|
||||
removeAllListeners: (channel: string) => {
|
||||
if (ALLOWED_REMOVE_CHANNELS.has(channel)) {
|
||||
ipcRenderer.removeAllListeners(channel)
|
||||
}
|
||||
// Events from main process — 返回取消订阅函数
|
||||
onFileOpenInTab: (callback: (data: { filePath: string; content: string }) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: { filePath: string; content: string }) => callback(data)
|
||||
ipcRenderer.on(IPC_CHANNELS.FILE_OPEN_IN_TAB, handler)
|
||||
return () => { ipcRenderer.removeListener(IPC_CHANNELS.FILE_OPEN_IN_TAB, handler) }
|
||||
},
|
||||
onMenuSave: (callback: () => void) => {
|
||||
const handler = () => callback()
|
||||
ipcRenderer.on(IPC_CHANNELS.MENU_SAVE, handler)
|
||||
return () => { ipcRenderer.removeListener(IPC_CHANNELS.MENU_SAVE, handler) }
|
||||
},
|
||||
onMenuSaveAs: (callback: () => void) => {
|
||||
const handler = () => callback()
|
||||
ipcRenderer.on(IPC_CHANNELS.MENU_SAVE_AS, handler)
|
||||
return () => { ipcRenderer.removeListener(IPC_CHANNELS.MENU_SAVE_AS, handler) }
|
||||
},
|
||||
onViewModeChange: (callback: (mode: string) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, mode: string) => callback(mode)
|
||||
ipcRenderer.on(IPC_CHANNELS.MENU_VIEW_MODE, handler)
|
||||
return () => { ipcRenderer.removeListener(IPC_CHANNELS.MENU_VIEW_MODE, handler) }
|
||||
},
|
||||
onExternalModification: (callback: (filePath: string) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, filePath: string) => callback(filePath)
|
||||
ipcRenderer.on(IPC_CHANNELS.FILE_EXTERNALLY_MODIFIED, handler)
|
||||
return () => { ipcRenderer.removeListener(IPC_CHANNELS.FILE_EXTERNALLY_MODIFIED, handler) }
|
||||
},
|
||||
onDirChanged: (callback: () => void) => {
|
||||
const handler = () => callback()
|
||||
ipcRenderer.on(IPC_CHANNELS.SIDEBAR_DIR_CHANGED, handler)
|
||||
return () => { ipcRenderer.removeListener(IPC_CHANNELS.SIDEBAR_DIR_CHANGED, handler) }
|
||||
},
|
||||
onConfirmClose: (callback: () => void) => {
|
||||
const handler = () => callback()
|
||||
ipcRenderer.on(IPC_CHANNELS.WINDOW_CONFIRM_CLOSE, handler)
|
||||
return () => { ipcRenderer.removeListener(IPC_CHANNELS.WINDOW_CONFIRM_CLOSE, handler) }
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user