Files
markdown-converter/src/preload.js
T
amitwh c4dcbd8caf feat: v4.6.0 — AI assistant, collaboration, knowledge base, and 15 more features
- AI Assistant plugin: multi-provider chat (OpenAI/Anthropic/Ollama/LM Studio),
  summarize/improve/translate commands, proofread via ai:analyze; calls
  proxied through main so API keys stay out of the renderer
- Collaboration plugin: anchor-based comments in .comments/ sidecars with
  drift detection and F8 navigation
- Local knowledge base: [[wiki-links]] with click-to-create + Backlinks panel
- Crash recovery: debounced session snapshots with restore prompt on launch
- Version history: pre-save snapshots, History panel with restore/diff/delete
- Real PDF encryption: swap pdf-lib for @cantoo/pdf-lib (probe-driven UI)
- XLSX export (native workbooks via JSZip), ODT headers/footers + page size
- Offline KaTeX (bundled CSS+fonts), local-first PlantUML rendering
- Editor: vim mode toggle, snippet Tab-expansion, zen word-goal setter,
  writing heatmap, writing-studio panels wired with rail icons
- Quick Note global scratchpad (Ctrl+Alt+Q), markdownconverter:// deep links,
  REPL first-run confirmation
- Fix: Ctrl+Shift+P collision, pandoc converter availability check, CLI
  dangling --css/--reference-doc flags, dead converter button

8 new test suites; 613 tests green; lint clean
2026-09-05 20:48:39 +05:30

493 lines
13 KiB
JavaScript

/**
* 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<any>} 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 `<input type="file">` 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);