From c6ec1cef642f0164f7bd5a64685f0cbf72dcf917 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Sun, 23 Aug 2026 15:43:38 +0530 Subject: [PATCH] fix(renderer): migrate File.path reads to webUtils.getPathForFile for Electron 41 --- src/preload.js | 17 ++++++- src/renderer.js | 38 +++++++++------ src/renderer/document-compare-dialog.js | 6 ++- src/renderer/media-operations-dialog.js | 15 +++--- src/utils/file-path.js | 30 ++++++++++++ tests/file-path.test.js | 38 +++++++++++++++ tests/preload.test.js | 61 +++++++++++++++++++++++++ tests/setup.js | 2 + 8 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 src/utils/file-path.js create mode 100644 tests/file-path.test.js diff --git a/src/preload.js b/src/preload.js index 9f510f0..a1aaccb 100644 --- a/src/preload.js +++ b/src/preload.js @@ -12,7 +12,7 @@ * @version 4.4.1 */ -const { contextBridge, ipcRenderer } = require('electron'); +const { contextBridge, ipcRenderer, webUtils } = require('electron'); // Define allowed IPC channels for security const ALLOWED_SEND_CHANNELS = [ @@ -431,6 +431,21 @@ contextBridge.exposeInMainWorld('electronAPI', { 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), diff --git a/src/renderer.js b/src/renderer.js index e14b1ad..cff77eb 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -3,7 +3,7 @@ * @version 4.5.0 */ -const { ipcRenderer } = require('electron'); +const { ipcRenderer, webUtils } = require('electron'); const marked = require('marked'); const { markedHighlight } = require('marked-highlight'); const createDOMPurify = require('dompurify'); @@ -16,6 +16,7 @@ const { showPdfBatchDialog } = require('./renderer/pdf-batch-dialog'); const { showDocumentCompareDialog } = require('./renderer/document-compare-dialog'); const { initExportPresets, refreshExportPresets } = require('./renderer/export-presets'); const { csvToMarkdownTable } = require('./utils/csv-to-markdown-table'); +const { getFilePath } = require('./utils/file-path'); /** * Toggle body classes that drive the monospace font + ligatures CSS tokens. @@ -69,6 +70,13 @@ if (typeof window !== 'undefined' && !window.electronAPI) { ipcRenderer.removeAllListeners(channel); }, getAppVersion: () => ipcRenderer.invoke('get-app-version'), + // File.path was removed in Electron 32 — resolve picker paths via webUtils. + getFilePath: (file) => { + if (webUtils && typeof webUtils.getPathForFile === 'function') { + return webUtils.getPathForFile(file); + } + return file && file.path; + }, file: { read: (filePath) => ipcRenderer.invoke('read-file', filePath), write: (filePath, content) => @@ -2647,7 +2655,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('template-file-input').addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { - document.getElementById('custom-template-path').value = file.path; + document.getElementById('custom-template-path').value = getFilePath(file); } }); @@ -2725,7 +2733,7 @@ document.addEventListener('DOMContentLoaded', () => { input.onchange = (e) => { const file = e.target.files[0]; if (file) { - document.getElementById('bibliography-file').value = file.path; + document.getElementById('bibliography-file').value = getFilePath(file); } }; input.click(); @@ -2739,7 +2747,7 @@ document.addEventListener('DOMContentLoaded', () => { input.onchange = (e) => { const file = e.target.files[0]; if (file) { - document.getElementById('csl-file').value = file.path; + document.getElementById('csl-file').value = getFilePath(file); } }; input.click(); @@ -3590,8 +3598,9 @@ document.addEventListener('DOMContentLoaded', () => { input.onchange = (e) => { const file = e.target.files[0]; if (file) { - converterFilePath = file.path; - document.getElementById('converter-file-path').value = file.path; + const filePath = getFilePath(file); + converterFilePath = filePath; + document.getElementById('converter-file-path').value = filePath; } }; input.click(); @@ -3942,8 +3951,9 @@ document.addEventListener('DOMContentLoaded', () => { input.onchange = (e) => { const files = Array.from(e.target.files); files.forEach((file) => { - if (!mergeFilePaths.includes(file.path)) { - mergeFilePaths.push(file.path); + const filePath = getFilePath(file); + if (!mergeFilePaths.includes(filePath)) { + mergeFilePaths.push(filePath); } }); updateMergeFilesList(); @@ -4127,8 +4137,9 @@ document.addEventListener('DOMContentLoaded', () => { input.onchange = (e) => { const file = e.target.files[0]; if (file) { - document.getElementById(button.inputId).value = file.path; - onPDFFileSelected(button.inputId, file.path); + const filePath = getFilePath(file); + document.getElementById(button.inputId).value = filePath; + onPDFFileSelected(button.inputId, filePath); } }; input.click(); @@ -4137,10 +4148,11 @@ document.addEventListener('DOMContentLoaded', () => { input.onchange = (e) => { const file = e.target.files[0]; if (file) { - document.getElementById(button.inputId).value = file.path; - onPDFFileSelected(button.inputId, file.path); + const filePath = getFilePath(file); + document.getElementById(button.inputId).value = filePath; + onPDFFileSelected(button.inputId, filePath); if (button.inputId === 'fill-form-input-path') { - loadFillFormFields(file.path); + loadFillFormFields(filePath); } } }; diff --git a/src/renderer/document-compare-dialog.js b/src/renderer/document-compare-dialog.js index 4d9ae69..0011013 100644 --- a/src/renderer/document-compare-dialog.js +++ b/src/renderer/document-compare-dialog.js @@ -9,7 +9,8 @@ * markup appended to document.body on first use, driven by ModalManager, with a * status line reusing the `info-message` / `warning-message` classes. File * selection reuses the app's existing convention: a plain `` - * whose `.path` is read directly (nodeIntegration is enabled for this renderer) — + * whose chosen File is resolved to a path via `window.electronAPI.getFilePath` + * (webUtils.getPathForFile — `File.path` was removed in Electron 32) — * no new IPC channel for picking files. File *contents* are read through the * existing `read-file` invoke channel (the same one the renderer's electronAPI * adapters use), not through a new privileged renderer-side file path. @@ -28,6 +29,7 @@ */ const { ipcRenderer } = require('electron'); +const { getFilePath } = require('../utils/file-path'); const { computeLineDiff } = require('../utils/line-diff'); const COMPARE_ACCEPT = '.md,.markdown,.txt,.text,.log,.json,.xml,.yml,.yaml,.csv'; @@ -140,7 +142,7 @@ function chooseFile(input) { fileInput.onchange = (e) => { const file = e.target.files[0]; if (file) { - input.value = file.path; + input.value = getFilePath(file); clearResult(); } }; diff --git a/src/renderer/media-operations-dialog.js b/src/renderer/media-operations-dialog.js index a7a3225..9327529 100644 --- a/src/renderer/media-operations-dialog.js +++ b/src/renderer/media-operations-dialog.js @@ -12,8 +12,9 @@ * three media kinds together cover 13 distinct operations. * * File selection reuses the app's existing convention: a plain `` - * whose `.path` is read directly (nodeIntegration is enabled for this renderer), the - * same approach already used throughout the PDF Editor and Universal Converter + * whose chosen File is resolved to a path via `window.electronAPI.getFilePath` + * (webUtils.getPathForFile — `File.path` was removed in Electron 32), the same + * approach already used throughout the PDF Editor and Universal Converter * dialogs. No new IPC channel is needed for single-file or save-file pickers. Output * *folder* selection (used by the video "Extract Frames" operation, and by batch * mode below) reuses the existing generic `select-folder` / `folder-selected` IPC @@ -33,6 +34,7 @@ */ const { ipcRenderer } = require('electron'); +const { getFilePath } = require('../utils/file-path'); const IMAGE_ACCEPT = '.jpg,.jpeg,.png,.webp,.avif,.tiff,.tif,.gif'; const AUDIO_ACCEPT = '.mp3,.wav,.ogg,.flac,.aac,.m4a,.wma'; @@ -447,7 +449,7 @@ function renderFileField(field) { fileInput.accept = field.accept || '*'; fileInput.onchange = (e) => { const file = e.target.files[0]; - if (file) input.value = file.path; + if (file) input.value = getFilePath(file); }; fileInput.click(); }, @@ -462,7 +464,7 @@ function renderSaveField(field) { fileInput.nwsaveas = true; fileInput.onchange = (e) => { const file = e.target.files[0]; - if (file) input.value = file.path; + if (file) input.value = getFilePath(file); }; fileInput.click(); }, @@ -588,8 +590,9 @@ function renderFilesField(field) { fileInput.multiple = true; fileInput.onchange = (e) => { Array.from(e.target.files).forEach((file) => { - if (!mergeFilePaths.includes(file.path)) { - mergeFilePaths.push(file.path); + const filePath = getFilePath(file); + if (!mergeFilePaths.includes(filePath)) { + mergeFilePaths.push(filePath); } }); updateMergeFilesList(listContainer); diff --git a/src/utils/file-path.js b/src/utils/file-path.js new file mode 100644 index 0000000..76c35c8 --- /dev/null +++ b/src/utils/file-path.js @@ -0,0 +1,30 @@ +/** + * File-input path resolution for Electron 41. + * + * `File.path` was removed in Electron 32, so a plain `` + * picker can no longer read the chosen file's absolute path directly. The + * replacement is `webUtils.getPathForFile(file)`, exposed to renderers as + * `window.electronAPI.getFilePath` (by src/preload.js in preload-loaded + * windows, and by the fallback shim in src/renderer.js for the main window, + * which does not load the preload script). + * + * @module utils/file-path + */ + +/** + * Resolve a File object chosen via `` to its absolute path. + * Falls back to `file.path` when the electronAPI helper is absent + * (Electron < 32, or jsdom tests without the bridge). + * + * @param {File} file - File object from a file input's files list + * @returns {string | undefined} Absolute filesystem path when resolvable + */ +function getFilePath(file) { + const api = typeof window !== 'undefined' ? window.electronAPI : undefined; + if (api && typeof api.getFilePath === 'function') { + return api.getFilePath(file); + } + return file && file.path; +} + +module.exports = { getFilePath }; diff --git a/tests/file-path.test.js b/tests/file-path.test.js new file mode 100644 index 0000000..3328f63 --- /dev/null +++ b/tests/file-path.test.js @@ -0,0 +1,38 @@ +/** + * Tests for the File-input path resolution helper (Electron 41 migration). + * File.path was removed in Electron 32; getFilePath() routes through + * window.electronAPI.getFilePath (webUtils.getPathForFile) when the bridge is + * available and falls back to file.path otherwise (older Electron, jsdom). + */ + +const { getFilePath } = require('../src/utils/file-path'); + +describe('getFilePath helper', () => { + const originalAPI = window.electronAPI; + + afterEach(() => { + window.electronAPI = originalAPI; + }); + + it('delegates to window.electronAPI.getFilePath when exposed', () => { + const file = { path: '/stale/file.path' }; + window.electronAPI = { getFilePath: jest.fn(() => '/resolved/report.md') }; + + expect(getFilePath(file)).toBe('/resolved/report.md'); + expect(window.electronAPI.getFilePath).toHaveBeenCalledWith(file); + }); + + it('falls back to file.path when the helper is absent', () => { + const file = { path: '/legacy/electron/file.md' }; + window.electronAPI = { send: jest.fn() }; // no getFilePath on the surface + + expect(getFilePath(file)).toBe('/legacy/electron/file.md'); + }); + + it('falls back to file.path when electronAPI is undefined', () => { + const file = { path: '/no-bridge/file.md' }; + window.electronAPI = undefined; + + expect(getFilePath(file)).toBe('/no-bridge/file.md'); + }); +}); diff --git a/tests/preload.test.js b/tests/preload.test.js index e1a5139..08c9ea0 100644 --- a/tests/preload.test.js +++ b/tests/preload.test.js @@ -182,4 +182,65 @@ describe('Preload Security', () => { expect(window.electronAPI.pdf.getPageCount).toBeDefined(); }); }); + + // Loads the real src/preload.js against a mocked electron module, following + // the jest.mock('electron') pattern used by the renderer dialog tests. + describe('getFilePath (webUtils.getPathForFile bridge)', () => { + const setupApi = window.electronAPI; + + afterAll(() => { + window.electronAPI = setupApi; + }); + + test('exposes getFilePath and delegates to webUtils.getPathForFile', () => { + jest.mock('electron', () => ({ + contextBridge: { + exposeInMainWorld: (key, api) => { + window[key] = api; + }, + }, + ipcRenderer: { + send: jest.fn(), + invoke: jest.fn(), + on: jest.fn(), + once: jest.fn(), + removeListener: jest.fn(), + removeAllListeners: jest.fn(), + }, + webUtils: { + getPathForFile: jest.fn((file) => file && `/resolved${file.name}`), + }, + })); + const { webUtils } = require('electron'); + require('../src/preload.js'); + + expect(typeof window.electronAPI.getFilePath).toBe('function'); + const file = { name: '/report.md' }; + expect(window.electronAPI.getFilePath(file)).toBe('/resolved/report.md'); + expect(webUtils.getPathForFile).toHaveBeenCalledWith(file); + }); + + test('falls back to file.path when webUtils is unavailable', () => { + jest.mock('electron', () => ({ + contextBridge: { + exposeInMainWorld: (key, api) => { + window[key] = api; + }, + }, + ipcRenderer: { + send: jest.fn(), + invoke: jest.fn(), + on: jest.fn(), + once: jest.fn(), + removeListener: jest.fn(), + removeAllListeners: jest.fn(), + }, + })); + require('../src/preload.js'); + + expect(typeof window.electronAPI.getFilePath).toBe('function'); + const file = { path: '/legacy/file.md' }; + expect(window.electronAPI.getFilePath(file)).toBe('/legacy/file.md'); + }); + }); }); diff --git a/tests/setup.js b/tests/setup.js index 58d1f25..c592b0b 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -11,6 +11,8 @@ global.window.electronAPI = { once: jest.fn(), invoke: jest.fn(() => Promise.resolve(null)), removeAllListeners: jest.fn(), + // webUtils.getPathForFile bridge (File.path was removed in Electron 32) + getFilePath: jest.fn((file) => file && file.path), file: { save: jest.fn(), saveCurrent: jest.fn(),