From 21a00a53a40297a74e868b743afc8cb8200214ea Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Sat, 13 Jun 2026 19:52:51 +0530 Subject: [PATCH] fix: resolve 12 critical runtime failures, security issue, and dead code CRITICAL fixes: - GitStatusPanel: use ipc.file.gitStage/gitCommit instead of non-existent top-level methods - HeaderFooterDialog: use send/on channels instead of non-existent headerFooter namespace - CodeMirrorEditor: use invoke('save-pasted-image') instead of non-existent savePastedImage - ipc.ts: map to correct preload namespaces (git.status, git.stage, git.commit) - Add pick-folder/pick-file to ALLOWED_SEND_CHANNELS whitelist - Add git convenience namespace to preload (git.status/stage/commit/log/diff) HIGH fixes: - FindReplaceBar: replace broken match count stub with regex-based counter - PDF export window: disable nodeIntegration, enable contextIsolation + sandbox - Git handler: accept rootPath argument from renderer - Add git-diff IPC handler + GitOperations.diff() MEDIUM fixes: - Remove 4 dead functions: checkPandocAvailable, safeExecFile, runPandoc, openPDFFile, exportWithPandocPDF - Remove stale ODT header/footer console.log stub - Rewrite electron.d.ts to match actual preload API shape --- src/main/GitOperations.js | 12 +- src/main/files/git.js | 41 ++-- src/main/index.js | 100 +--------- src/preload.js | 11 ++ .../components/editor/CodeMirrorEditor.tsx | 2 +- .../components/editor/FindReplaceBar.tsx | 62 ++++-- .../modals/BatchMediaConverterDialog.tsx | 6 +- .../components/modals/ExportDocxDialog.tsx | 6 +- .../components/modals/ExportHtmlDialog.tsx | 12 +- .../components/modals/ExportPdfDialog.tsx | 11 +- .../components/modals/HeaderFooterDialog.tsx | 50 +++-- .../components/sidebar/GitStatusPanel.tsx | 22 +-- src/renderer/lib/ipc.ts | 48 ++--- src/renderer/types/electron.d.ts | 180 +++++++++++------- .../component/modals/ExportPdfDialog.test.tsx | 5 +- 15 files changed, 306 insertions(+), 262 deletions(-) diff --git a/src/main/GitOperations.js b/src/main/GitOperations.js index 60dac59..92ab298 100644 --- a/src/main/GitOperations.js +++ b/src/main/GitOperations.js @@ -41,4 +41,14 @@ async function log(dir, maxCount = 20) { } } -module.exports = { getStatus, stage, commit, log }; +async function diff(dir, filePath) { + try { + const git = getGitInstance(dir); + const args = filePath ? ['--', filePath] : []; + return await git.diff(args); + } catch (err) { + return { error: err.message }; + } +} + +module.exports = { getStatus, stage, commit, log, diff }; diff --git a/src/main/files/git.js b/src/main/files/git.js index 4d8bebc..b2611dc 100644 --- a/src/main/files/git.js +++ b/src/main/files/git.js @@ -4,33 +4,42 @@ const { ipcMain } = require('electron'); const GitOperations = require('../GitOperations'); function register(currentFileRef) { - ipcMain.handle('git-status', async () => { - const dir = currentFileRef.current - ? require('path').dirname(currentFileRef.current) - : process.cwd(); + ipcMain.handle('git-status', async (_event, rootPath) => { + const dir = + rootPath || + (currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd()); return GitOperations.getStatus(dir); }); - ipcMain.handle('git-stage', async (_event, { files }) => { - const dir = currentFileRef.current - ? require('path').dirname(currentFileRef.current) - : process.cwd(); + ipcMain.handle('git-stage', async (_event, { rootPath, files }) => { + const dir = + rootPath || + (currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd()); return GitOperations.stage(dir, files); }); - ipcMain.handle('git-commit', async (_event, { message }) => { - const dir = currentFileRef.current - ? require('path').dirname(currentFileRef.current) - : process.cwd(); + ipcMain.handle('git-commit', async (_event, { rootPath, message }) => { + const dir = + rootPath || + (currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd()); return GitOperations.commit(dir, message); }); - ipcMain.handle('git-log', async () => { - const dir = currentFileRef.current - ? require('path').dirname(currentFileRef.current) - : process.cwd(); + ipcMain.handle('git-log', async (_event, rootPath) => { + const dir = + rootPath || + (currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd()); return GitOperations.log(dir); }); + + ipcMain.handle('git-diff', async (_event, filePath) => { + const dir = filePath + ? require('path').dirname(filePath) + : currentFileRef.current + ? require('path').dirname(currentFileRef.current) + : process.cwd(); + return GitOperations.diff(dir, filePath); + }); } module.exports = { register }; diff --git a/src/main/index.js b/src/main/index.js index 75546ec..144a1e3 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -57,41 +57,6 @@ function getFFmpegPath() { return process.platform === 'win32' ? 'ffmpeg' : 'ffmpeg'; } -// eslint-disable-next-line no-unused-vars -function checkPandocAvailable() { - return new Promise((resolve) => { - const pandocPath = getPandocPath(); - execFile(pandocPath, ['--version'], (error) => { - resolve(!error); - }); - }); -} - -/** - * Safe command execution using execFile (no shell injection risk) - * @param {string} command - The command to execute - * @param {string[]} args - Array of arguments - * @param {object} options - Options for execFile - * @returns {Promise<{stdout: string, stderr: string}>} - */ -// eslint-disable-next-line no-unused-vars -function safeExecFile(command, args, options = {}) { - return new Promise((resolve, reject) => { - execFile( - command, - args, - { maxBuffer: 10 * 1024 * 1024, ...options }, - (error, stdout, stderr) => { - if (error) { - reject({ error, stdout, stderr }); - } else { - resolve({ stdout, stderr }); - } - } - ); - }); -} - // File size validation const MAX_FILE_SIZE_MB = 50; const MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024; @@ -134,17 +99,6 @@ function convertDataToMarkdown(content, format) { } } -/** - * Run Pandoc command safely with execFile - * @param {string[]} args - Pandoc arguments array - * @param {Function} callback - Callback function (error, stdout, stderr) - */ -// eslint-disable-next-line no-unused-vars -function runPandoc(args, callback) { - const pandocPath = getPandocPath(); - execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); -} - /** * Run a Pandoc command string safely using execFile * Parses the command string and uses execFile to prevent shell injection @@ -593,23 +547,6 @@ function showDependenciesDialog() { depsWindow.setMenuBarVisibility(false); } -// eslint-disable-next-line no-unused-vars -function openPDFFile() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [{ name: 'PDF Files', extensions: ['pdf'] }], - }); - - if (files && files[0]) { - const stats = fs.statSync(files[0]); - if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; - } - mainWindow.webContents.send('open-pdf-viewer', files[0]); - } -} - function openFile() { const files = dialog.showOpenDialogSync(mainWindow, { properties: ['openFile'], @@ -1958,42 +1895,14 @@ function exportWithPandoc(pandocCmd, outputFile, format) { } } - // Add headers/footers to ODT if enabled - if (format === 'odt' && headerFooterSettings.enabled) { - // ODT format is similar to DOCX in structure, we could implement this - console.log('ODT header/footer support not yet implemented'); - } + // ODT header/footer is not supported by Pandoc's ODT output + // Headers/footers are applied only for DOCX format showExportSuccess(outputFile); } }); } -// Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety -// eslint-disable-next-line no-unused-vars -function exportWithPandocPDF(pandocCmd, outputFile) { - runPandocCmd(pandocCmd, (error, _stdout, _stderr) => { - if (error) { - console.log('XeLaTeX failed, trying PDFLaTeX...'); - // Fallback to pdflatex - const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex'); - runPandocCmd(fallbackCmd, (fallbackError, _fallbackStdout, _fallbackStderr) => { - if (fallbackError) { - console.log('PDFLaTeX failed, trying Electron PDF...'); - // Final fallback to Electron PDF - exportToPDFElectron(outputFile); - } else { - console.log('Successfully exported PDF with PDFLaTeX'); - showExportSuccess(outputFile); - } - }); - } else { - console.log('Successfully exported PDF with XeLaTeX'); - showExportSuccess(outputFile); - } - }); -} - // Export to HTML using marked (no pandoc required) function exportToHTML(outputFile) { try { @@ -2219,8 +2128,9 @@ function exportToPDFElectron(outputFile) { const pdfWindow = new BrowserWindow({ show: false, webPreferences: { - nodeIntegration: true, - contextIsolation: false, + nodeIntegration: false, + contextIsolation: true, + sandbox: true, }, }); diff --git a/src/preload.js b/src/preload.js index eac3238..cb338f1 100644 --- a/src/preload.js +++ b/src/preload.js @@ -105,6 +105,8 @@ const ALLOWED_SEND_CHANNELS = [ 'is-directory', 'copy-path', 'move-path', + 'pick-folder', + 'pick-file', // Git 'git-status', @@ -460,6 +462,15 @@ contextBridge.exposeInMainWorld('electronAPI', { getAppVersion: () => ipcRenderer.invoke('get-app-version'), + // Git Operations + git: { + status: (rootPath) => ipcRenderer.invoke('git-status', rootPath), + stage: (args) => ipcRenderer.invoke('git-stage', args), + commit: (args) => ipcRenderer.invoke('git-commit', args), + log: (rootPath) => ipcRenderer.invoke('git-log', rootPath), + diff: (filePath) => ipcRenderer.invoke('git-diff', filePath), + }, + app: { quit: () => ipcRenderer.send('app:quit'), openExternal: (url) => ipcRenderer.send('app:open-external', url), diff --git a/src/renderer/components/editor/CodeMirrorEditor.tsx b/src/renderer/components/editor/CodeMirrorEditor.tsx index cdfe9d6..f87c30a 100644 --- a/src/renderer/components/editor/CodeMirrorEditor.tsx +++ b/src/renderer/components/editor/CodeMirrorEditor.tsx @@ -57,7 +57,7 @@ async function handleImageFile(file: File): Promise { const base64 = (reader.result as string).split(',')[1]; if (!base64) return; const ext = guessExt(file.type); - const result = await window.electronAPI.savePastedImage({ base64, ext }); + const result = await window.electronAPI.invoke('save-pasted-image', { base64, ext }); if (result) { insertSnippet(`![${file.name}](${result.relativePath})`); toast.success('Image pasted'); diff --git a/src/renderer/components/editor/FindReplaceBar.tsx b/src/renderer/components/editor/FindReplaceBar.tsx index 65aeab0..3eee1a6 100644 --- a/src/renderer/components/editor/FindReplaceBar.tsx +++ b/src/renderer/components/editor/FindReplaceBar.tsx @@ -8,6 +8,7 @@ import { getSearchQuery, setSearchQuery, } from '@codemirror/search'; +import type { EditorView } from '@codemirror/view'; import { getActiveView } from '@/lib/editor-commands'; import { useAppStore } from '@/stores/app-store'; import { Input } from '@/components/ui/input'; @@ -24,14 +25,17 @@ export function FindReplaceBar() { const [useRegex, setUseRegex] = useState(false); const [matchInfo, setMatchInfo] = useState<{ current: number; total: number } | null>(null); - const executeCommand = useCallback((fn: (view: any) => any) => { - const view = getActiveView(); - if (!view) return false; - const result = fn(view); - updateMatchCount(); - view.focus(); - return result; - }, []); + const executeCommand = useCallback( + (fn: (view: EditorView) => boolean | void) => { + const view = getActiveView(); + if (!view) return false; + const result = fn(view); + updateMatchCount(); + view.focus(); + return result; + }, + [updateMatchCount] + ); const updateMatchCount = useCallback(() => { const view = getActiveView(); @@ -40,22 +44,34 @@ export function FindReplaceBar() { return; } const query = getSearchQuery(view.state); - if (!query) { + if (!query || !query.search) { setMatchInfo(null); return; } - const searchState = view.state.field( - // @ts-expect-error - internal field accessor - query.spec.create() && - (() => { - for (const facet of view.state.field) return null; - })(), - false - ); try { - // @ts-expect-error - search state inspection - const panel = view.state.facet?.(searchPanelFacet)?.[0]; - setMatchInfo(null); + const docText = view.state.doc.toString(); + const searchStr = query.regexp + ? query.search + : query.search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const flags = `g${query.caseSensitive ? '' : 'i'}`; + const regex = new RegExp(searchStr, flags); + const matches = docText.match(regex); + if (!matches) { + setMatchInfo({ current: 0, total: 0 }); + return; + } + const selectionHead = view.state.selection.main.head; + let currentMatch = 0; + const matchPositions: number[] = []; + let match; + while ((match = regex.exec(docText)) !== null) { + matchPositions.push(match.index); + if (match.index <= selectionHead && selectionHead <= match.index + match[0].length) { + currentMatch = matchPositions.length; + } + if (matchPositions.length > 10000) break; + } + setMatchInfo({ current: currentMatch || 1, total: matchPositions.length }); } catch { setMatchInfo(null); } @@ -210,6 +226,12 @@ export function FindReplaceBar() { + {matchInfo && matchInfo.total > 0 && ( + + {matchInfo.current}/{matchInfo.total} + + )} +
- +
diff --git a/src/renderer/components/modals/ExportDocxDialog.tsx b/src/renderer/components/modals/ExportDocxDialog.tsx index 2915303..d8f5d0b 100644 --- a/src/renderer/components/modals/ExportDocxDialog.tsx +++ b/src/renderer/components/modals/ExportDocxDialog.tsx @@ -147,7 +147,11 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) { )}
- +
diff --git a/src/renderer/components/modals/ExportHtmlDialog.tsx b/src/renderer/components/modals/ExportHtmlDialog.tsx index a2ed3a1..609ff01 100644 --- a/src/renderer/components/modals/ExportHtmlDialog.tsx +++ b/src/renderer/components/modals/ExportHtmlDialog.tsx @@ -124,7 +124,11 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
- +