/** * Preload Script for PanConverter * * This script creates a secure bridge between the main process and renderer process. * It exposes only specific IPC channels, preventing direct Node.js access in the renderer. * * Security Benefits: * - No direct access to Node.js APIs (fs, path, child_process, etc.) * - All IPC channels are explicitly whitelisted * - Prevents XSS from escalating to full system access * * @version 4.4.1 */ const { contextBridge, ipcRenderer, webUtils } = require('electron'); // Define allowed IPC channels for security const ALLOWED_SEND_CHANNELS = [ // File operations 'save-file', 'save-current-file', 'set-current-file', 'save-recent-files', 'clear-recent-files', 'renderer-ready', 'select-custom-css', // Theme 'get-theme', // Print 'do-print', 'do-print-with-options', // Export 'export-with-options', 'export-spreadsheet', // Plugin export formats 'plugin-export-formats-registered', 'plugin-export-format-result', // AI Assistant plugin (completions proxied through main; keys stay there) 'ai-assistant:complete', 'ai-assistant:status', // Batch conversion 'batch-convert', 'select-folder', // Universal converter 'universal-convert', 'universal-convert-batch', // Image converter 'process-image-operation', 'select-image-folder', 'batch-image-operation', // Audio converter 'process-audio-operation', 'batch-audio-operation', // Video converter 'process-video-operation', 'batch-video-operation', // Header/Footer 'get-header-footer-settings', 'save-header-footer-settings', 'browse-header-footer-logo', 'save-header-footer-logo', 'clear-header-footer-logo', // Word template settings 'get-word-template-settings', 'save-word-template-settings', 'browse-word-template', 'clear-word-template', // Export presets (invoke channels — gated by this same array) 'get-export-presets', 'save-export-preset', 'delete-export-preset', // Page settings 'get-page-settings', 'update-page-settings', // PDF operations 'process-pdf-operation', 'get-pdf-page-count', 'get-pdf-form-fields', 'get-pdf-capabilities', 'select-pdf-folder', 'batch-pdf-operation', // ASCII generator (separate window) 'open-ascii-generator', // Table generator (separate window) 'open-table-generator', // Insert generated content 'insert-generated-content', // Image paste/drop 'save-pasted-image', // Templates 'load-template', // File Explorer 'list-directory', 'read-file', 'write-file', 'delete-file', 'ensure-directory', 'path-exists', 'is-directory', 'copy-path', 'move-path', // Git 'git-status', 'git-stage', 'git-commit', 'git-log', 'git-branches', 'git-checkout', 'git-push', 'git-pull', // Snippets 'get-snippets', 'save-snippet', 'delete-snippet', // Code execution (REPL) 'execute-code', // File open by path 'open-file-path', // PDF editor from toolbar 'show-pdf-editor-from-toolbar', // Menu triggers 'menu-open', 'export', // Git diff 'git-diff', // Plugin settings 'plugin-settings:get', 'plugin-settings:set', // Monospace font settings 'get-monospace-settings', 'set-monospace-settings', // Vim keybinding mode 'get-vim-mode', 'set-vim-mode', // PlantUML local rendering (optional CLI) 'plantuml:available', 'plantuml:render', // Quick Note scratchpad 'quick-note:save', // Document version history 'version-history:list', 'version-history:read', 'version-history:save', 'version-history:delete', ]; const ALLOWED_RECEIVE_CHANNELS = [ // File operations 'file-new', 'file-opened', 'file-save', 'get-content-for-save', 'get-content-for-spreadsheet', 'recent-files-cleared', 'load-custom-css', 'clear-custom-css', // UI toggles 'toggle-preview', 'toggle-find', // Theme 'theme-changed', 'theme-data', // Edit operations 'undo', 'redo', // Font 'adjust-font-size', 'monospace-setting-change', // Print 'print-preview', 'print-preview-styled', // Export dialogs 'show-export-dialog', 'show-batch-dialog', 'show-universal-converter-dialog', 'show-table-generator', 'show-pdf-editor-dialog', 'show-document-compare', // Converter dialogs 'show-image-converter', 'show-audio-converter', 'show-video-converter', // PDF viewer 'open-pdf-viewer', // Conversion status 'conversion-status', 'conversion-complete', 'batch-progress', 'image-conversion-complete', 'audio-conversion-complete', 'video-conversion-complete', // Batch media operations (Image/Audio/Video Tools dialog batch mode) 'media-batch-progress', 'media-batch-complete', // Folder selection 'folder-selected', 'pdf-folder-selected', 'image-folder-selected', // Header/Footer 'header-footer-settings-data', 'header-footer-logo-selected', 'header-footer-logo-saved', // Word template settings 'word-template-settings-data', 'word-template-browsed', 'open-word-template-dialog', // Page settings 'page-settings-data', // PDF operations 'pdf-page-count', 'pdf-form-fields', 'pdf-operation-complete', 'pdf-operation-error', 'pdf-batch-complete', // ASCII Art Generator 'show-ascii-generator-window', 'show-ascii-generator', // Table Generator 'show-table-generator-window', // Header/Footer dialog 'open-header-footer-dialog', 'header-footer-logo-cleared', // PDF operation progress 'pdf-operation-progress', // Insert content from generator windows 'insert-content', // Batch converter 'show-batch-converter', // v4 menu-triggered events 'load-template-menu', 'toggle-command-palette', 'toggle-sidebar-panel', 'toggle-bottom-panel', // Plugin export formats 'run-plugin-export-format', ]; /** * Secure API exposed to renderer process * Access via window.electronAPI in renderer */ contextBridge.exposeInMainWorld('electronAPI', { // ============================================ // SEND METHODS (Renderer -> Main) // ============================================ /** * Send a message to the main process * @param {string} channel - IPC channel name * @param {any} data - Data to send */ send: (channel, data) => { if (ALLOWED_SEND_CHANNELS.includes(channel)) { ipcRenderer.send(channel, data); } else { console.warn(`[Preload] Blocked send to unauthorized channel: ${channel}`); } }, /** * Invoke a main process handler and get a response * @param {string} channel - IPC channel name * @param {any} data - Data to send * @returns {Promise} Response from main process */ invoke: async (channel, data) => { if (ALLOWED_SEND_CHANNELS.includes(channel)) { return await ipcRenderer.invoke(channel, data); } else { console.warn(`[Preload] Blocked invoke to unauthorized channel: ${channel}`); return null; } }, // ============================================ // RECEIVE METHODS (Main -> Renderer) // ============================================ /** * Register a listener for messages from main process * @param {string} channel - IPC channel name * @param {Function} callback - Function to call with received data * @returns {Function} Cleanup function to remove listener */ on: (channel, callback) => { if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { const subscription = (event, ...args) => callback(...args); ipcRenderer.on(channel, subscription); // Return cleanup function return () => { ipcRenderer.removeListener(channel, subscription); }; } else { console.warn(`[Preload] Blocked listener for unauthorized channel: ${channel}`); return () => {}; // No-op cleanup } }, /** * Register a one-time listener for messages from main process * @param {string} channel - IPC channel name * @param {Function} callback - Function to call with received data */ once: (channel, callback) => { if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { ipcRenderer.once(channel, (event, ...args) => callback(...args)); } else { console.warn(`[Preload] Blocked once listener for unauthorized channel: ${channel}`); } }, /** * Remove all listeners for a channel * @param {string} channel - IPC channel name */ removeAllListeners: (channel) => { if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { ipcRenderer.removeAllListeners(channel); } }, // ============================================ // CONVENIENCE METHODS // ============================================ // File Operations file: { save: (filePath, content) => ipcRenderer.send('save-file', { path: filePath, content }), saveCurrent: (content) => ipcRenderer.send('save-current-file', content), setCurrent: (filePath) => ipcRenderer.send('set-current-file', filePath), saveRecent: (recentFiles) => ipcRenderer.send('save-recent-files', recentFiles), clearRecent: () => ipcRenderer.send('clear-recent-files'), rendererReady: () => ipcRenderer.send('renderer-ready'), read: (filePath) => ipcRenderer.invoke('read-file', filePath), write: (filePath, content) => ipcRenderer.invoke('write-file', { path: filePath, content }), delete: (filePath) => ipcRenderer.invoke('delete-file', filePath), ensureDir: (dirPath) => ipcRenderer.invoke('ensure-directory', dirPath), exists: (filePath) => ipcRenderer.invoke('path-exists', filePath), isDirectory: (filePath) => ipcRenderer.invoke('is-directory', filePath), copy: (source, destination) => ipcRenderer.invoke('copy-path', { source, destination }), move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination }), }, // Theme Operations theme: { get: () => ipcRenderer.send('get-theme'), }, // Print Operations print: { doPrint: (options) => ipcRenderer.send('do-print', options), }, // Export Operations export: { withOptions: (format, options) => ipcRenderer.send('export-with-options', { format, options }), spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format }), }, // Batch Conversion batch: { convert: (inputFolder, outputFolder, format, options) => { ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options }); }, selectFolder: (type) => ipcRenderer.send('select-folder', type), }, // Universal Converter converter: { convert: (tool, fromFormat, toFormat, filePath) => { ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath }); }, convertBatch: (tool, fromFormat, toFormat, inputFolder, outputFolder) => { ipcRenderer.send('universal-convert-batch', { tool, fromFormat, toFormat, inputFolder, outputFolder, }); }, }, // Header/Footer Operations headerFooter: { getSettings: () => ipcRenderer.send('get-header-footer-settings'), saveSettings: (settings) => ipcRenderer.send('save-header-footer-settings', settings), browseLogo: (position) => ipcRenderer.send('browse-header-footer-logo', position), saveLogo: (position, filePath) => ipcRenderer.send('save-header-footer-logo', { position, filePath }), clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position), }, // Page Settings page: { getSettings: () => ipcRenderer.send('get-page-settings'), updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings), }, /** * Resolve a File object chosen via `` to its absolute * path. `File.path` was removed in Electron 32; `webUtils.getPathForFile` * is its replacement. Falls back to `file.path` on older Electron where * webUtils is unavailable. * @param {File} file - File object from a file input's files list * @returns {string | undefined} Absolute filesystem path when resolvable */ getFilePath: (file) => { if (webUtils && typeof webUtils.getPathForFile === 'function') { return webUtils.getPathForFile(file); } return file && file.path; }, // PDF Operations pdf: { processOperation: (data) => ipcRenderer.send('process-pdf-operation', data), getPageCount: (filePath) => ipcRenderer.send('get-pdf-page-count', filePath), selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId), }, // Generator Windows generators: { openAscii: () => ipcRenderer.send('open-ascii-generator'), openTable: () => ipcRenderer.send('open-table-generator'), }, getAppVersion: () => ipcRenderer.invoke('get-app-version'), }); // Log successful preload initialization console.log('[Preload] Secure IPC bridge initialized'); console.log('[Preload] Allowed send channels:', ALLOWED_SEND_CHANNELS.length); console.log('[Preload] Allowed receive channels:', ALLOWED_RECEIVE_CHANNELS.length);