import { ipcMain, dialog, BrowserWindow, type IpcMainInvokeEvent } from 'electron' import { readFileContent, saveFileContent, buildDirTree } from './file-system' import { FileWatcher, SidebarWatcher } from './file-watcher' import { IPC_CHANNELS } from '../shared/ipc-channels' import { stat, readFile, writeFile } from 'fs/promises' import { basename, isAbsolute } from 'path' // 安全校验:拒绝路径遍历攻击 function validatePath(filePath: string): boolean { if (!filePath || typeof filePath !== 'string') return false // 拒绝空字节 if (filePath.includes('\0')) return false // 检查路径遍历:以路径分隔符分割后检查是否存在完整的 '..' 段 // H-02: 用段检查替代全局 includes('..'),避免误伤含 '..' 的合法路径 const sepPattern = /[/\\]/ const parts = filePath.split(sepPattern) if (parts.some(part => part === '..')) return false // 必须是绝对路径 if (!isAbsolute(filePath)) return false return true } export function registerIpcHandlers( getMainWindow: () => BrowserWindow | null, fileWatcher: FileWatcher, sidebarWatcher: SidebarWatcher, state: { activeFilePath: string | null pendingFilePath: string | null isClosing: boolean closeTimeout: NodeJS.Timeout | null }, ): void { // 打开文件对话框 ipcMain.handle(IPC_CHANNELS.DIALOG_OPEN_FILE, async () => { const win = getMainWindow() if (!win) return null try { const result = await dialog.showOpenDialog(win, { properties: ['openFile'], filters: [{ name: 'Markdown 文件', extensions: ['md', 'markdown', 'txt'] }], }) if (!result.canceled && result.filePaths.length > 0) { const filePath = result.filePaths[0] const fileResult = await readFileContent(filePath) if (fileResult.success) { state.activeFilePath = filePath fileWatcher.start(filePath) win.setTitle(`MarkLite - ${basename(filePath)}`) return { filePath, content: fileResult.content } } return { error: fileResult.error } } return null } catch (err) { return { error: (err as Error).message } } }) // 读取文件 ipcMain.handle(IPC_CHANNELS.FILE_READ, async (_event: IpcMainInvokeEvent, filePath: string) => { if (!validatePath(filePath)) { return { success: false, error: '无效的文件路径' } } return readFileContent(filePath) }) // 保存文件 ipcMain.handle( IPC_CHANNELS.FILE_SAVE, async (_event: IpcMainInvokeEvent, data: { filePath: string | null; content: string }) => { if (data.filePath && !validatePath(data.filePath)) { return { success: false, error: '无效的文件路径' } } const win = getMainWindow() try { if (data.filePath) { // Mark self-writing before stopping watcher to suppress any events // that arrive between stop and start fileWatcher.setSelfWriting(true) fileWatcher.stop() const result = await saveFileContent(data.filePath, data.content) // Restart watcher while selfWriting is still true so any immediate 'change' // event from the restart is suppressed — prevents overwriting editor content fileWatcher.start(data.filePath) fileWatcher.setSelfWriting(false) state.activeFilePath = data.filePath if (win && !win.isDestroyed()) { win.setTitle(`MarkLite - ${basename(data.filePath)}`) } return result } else { if (!win) return { success: false, error: '窗口不可用' } const saveResult = await dialog.showSaveDialog(win, { filters: [{ name: 'Markdown 文件', extensions: ['md'] }], }) if (!saveResult.canceled) { fileWatcher.setSelfWriting(true) const result = await saveFileContent(saveResult.filePath, data.content) if (result.success) { state.activeFilePath = saveResult.filePath fileWatcher.start(saveResult.filePath) win.setTitle(`MarkLite - ${basename(saveResult.filePath)}`) } fileWatcher.setSelfWriting(false) return result } return { success: false, canceled: true } } } catch (err) { fileWatcher.setSelfWriting(false) if (state.activeFilePath) { fileWatcher.start(state.activeFilePath) } return { success: false, error: (err as Error).message } } }, ) // 另存为 ipcMain.handle( IPC_CHANNELS.FILE_SAVE_AS, async (_event: IpcMainInvokeEvent, data: { content: string }) => { const win = getMainWindow() if (!win) return { success: false, error: '窗口不可用' } try { const result = await dialog.showSaveDialog(win, { filters: [{ name: 'Markdown 文件', extensions: ['md'] }], }) if (!result.canceled) { fileWatcher.setSelfWriting(true) const saveResult = await saveFileContent(result.filePath, data.content) if (saveResult.success) { state.activeFilePath = result.filePath fileWatcher.start(result.filePath) win.setTitle(`MarkLite - ${basename(result.filePath)}`) } fileWatcher.setSelfWriting(false) return saveResult } return { success: false, canceled: true } } catch (err) { fileWatcher.setSelfWriting(false) return { success: false, error: (err as Error).message } } }, ) // 获取当前路径 ipcMain.handle(IPC_CHANNELS.FILE_GET_CURRENT_PATH, () => state.activeFilePath) // 文件统计 ipcMain.handle(IPC_CHANNELS.FILE_STATS, async (_event: IpcMainInvokeEvent, filePath: string) => { if (!validatePath(filePath)) { return { success: false, error: '无效的文件路径' } } try { const fileStat = await stat(filePath) return { success: true, size: fileStat.size, mtime: fileStat.mtime.toISOString() } } catch (err) { return { success: false, error: (err as Error).message } } }) // 重新加载 ipcMain.handle(IPC_CHANNELS.FILE_RELOAD, async () => { if (!state.activeFilePath) return { success: false, error: '没有打开的文件' } const result = await readFileContent(state.activeFilePath) return { ...result, filePath: state.activeFilePath } }) // 目录树 ipcMain.handle( IPC_CHANNELS.DIR_READ_TREE, async (_event: IpcMainInvokeEvent, dirPath: string) => { if (!validatePath(dirPath)) { return { success: false, error: '无效的目录路径' } } try { const tree = await buildDirTree(dirPath) return { success: true, tree, rootPath: dirPath } } catch (err) { return { success: false, error: (err as Error).message } } }, ) // 打开文件夹对话框 ipcMain.handle(IPC_CHANNELS.DIR_OPEN_DIALOG, async () => { const win = getMainWindow() if (!win) return null const result = await dialog.showOpenDialog(win, { properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { return result.filePaths[0] } return null }) // 目录监听 ipcMain.handle(IPC_CHANNELS.DIR_WATCH, (_event: IpcMainInvokeEvent, dirPath: string) => { if (!validatePath(dirPath)) return sidebarWatcher.start(dirPath) }) ipcMain.handle(IPC_CHANNELS.DIR_UNWATCH, () => { sidebarWatcher.stop() }) // 标签切换 ipcMain.handle( IPC_CHANNELS.TAB_SWITCHED, (_event: IpcMainInvokeEvent, filePath: string | null) => { const normalizedPath = filePath || null if (normalizedPath && !validatePath(normalizedPath)) { fileWatcher.stop() return } state.activeFilePath = normalizedPath fileWatcher.start(normalizedPath || '') const win = getMainWindow() if (win && !win.isDestroyed()) { win.setTitle(normalizedPath ? `MarkLite - ${basename(normalizedPath)}` : 'MarkLite') } }, ) // 窗口控制 ipcMain.handle(IPC_CHANNELS.WINDOW_FORCE_CLOSE, () => { const win = getMainWindow() if (win && !win.isDestroyed()) { fileWatcher.stop() win.removeAllListeners('close') win.close() } }) // B-01: 重置关闭状态 + 清除超时定时器 ipcMain.handle(IPC_CHANNELS.WINDOW_CANCEL_CLOSE, () => { state.isClosing = false if (state.closeTimeout) { clearTimeout(state.closeTimeout) state.closeTimeout = null } }) // v0.6.0: 数据备份导出 — 保存对话框 + 写 JSON 文件 ipcMain.handle(IPC_CHANNELS.DATA_EXPORT, async (_event: IpcMainInvokeEvent, content: string) => { const win = getMainWindow() if (!win) return { success: false, error: '窗口不可用' } try { const dateStr = new Date().toISOString().slice(0, 10) const result = await dialog.showSaveDialog(win, { title: '导出数据备份', defaultPath: `marklite-backup-${dateStr}.json`, filters: [{ name: 'JSON 备份文件', extensions: ['json'] }], }) if (result.canceled) return { success: false, canceled: true } await writeFile(result.filePath, content, 'utf8') return { success: true, filePath: result.filePath } } catch (err) { return { success: false, error: (err as Error).message } } }) // v0.6.0: 数据备份导入 — 打开对话框 + 读 JSON 文件 ipcMain.handle(IPC_CHANNELS.DATA_IMPORT, async () => { const win = getMainWindow() if (!win) return { success: false, error: '窗口不可用' } try { const result = await dialog.showOpenDialog(win, { title: '导入数据备份', properties: ['openFile'], filters: [{ name: 'JSON 备份文件', extensions: ['json'] }], }) if (result.canceled || result.filePaths.length === 0) { return { success: false, canceled: true } } const content = await readFile(result.filePaths[0], 'utf8') return { success: true, content } } catch (err) { return { success: false, error: (err as Error).message } } }) }