fix(renderer): migrate File.path reads to webUtils.getPathForFile for Electron 41

This commit is contained in:
2026-08-23 19:31:33 +05:30
parent 5fcc282fe0
commit c6ec1cef64
8 changed files with 185 additions and 22 deletions
+16 -1
View File
@@ -12,7 +12,7 @@
* @version 4.4.1 * @version 4.4.1
*/ */
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer, webUtils } = require('electron');
// Define allowed IPC channels for security // Define allowed IPC channels for security
const ALLOWED_SEND_CHANNELS = [ const ALLOWED_SEND_CHANNELS = [
@@ -431,6 +431,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
updateSettings: (settings) => ipcRenderer.send('update-page-settings', 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 Operations
pdf: { pdf: {
processOperation: (data) => ipcRenderer.send('process-pdf-operation', data), processOperation: (data) => ipcRenderer.send('process-pdf-operation', data),
+25 -13
View File
@@ -3,7 +3,7 @@
* @version 4.5.0 * @version 4.5.0
*/ */
const { ipcRenderer } = require('electron'); const { ipcRenderer, webUtils } = require('electron');
const marked = require('marked'); const marked = require('marked');
const { markedHighlight } = require('marked-highlight'); const { markedHighlight } = require('marked-highlight');
const createDOMPurify = require('dompurify'); const createDOMPurify = require('dompurify');
@@ -16,6 +16,7 @@ const { showPdfBatchDialog } = require('./renderer/pdf-batch-dialog');
const { showDocumentCompareDialog } = require('./renderer/document-compare-dialog'); const { showDocumentCompareDialog } = require('./renderer/document-compare-dialog');
const { initExportPresets, refreshExportPresets } = require('./renderer/export-presets'); const { initExportPresets, refreshExportPresets } = require('./renderer/export-presets');
const { csvToMarkdownTable } = require('./utils/csv-to-markdown-table'); 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. * Toggle body classes that drive the monospace font + ligatures CSS tokens.
@@ -69,6 +70,13 @@ if (typeof window !== 'undefined' && !window.electronAPI) {
ipcRenderer.removeAllListeners(channel); ipcRenderer.removeAllListeners(channel);
}, },
getAppVersion: () => ipcRenderer.invoke('get-app-version'), 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: { file: {
read: (filePath) => ipcRenderer.invoke('read-file', filePath), read: (filePath) => ipcRenderer.invoke('read-file', filePath),
write: (filePath, content) => write: (filePath, content) =>
@@ -2647,7 +2655,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('template-file-input').addEventListener('change', (e) => { document.getElementById('template-file-input').addEventListener('change', (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { 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) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
document.getElementById('bibliography-file').value = file.path; document.getElementById('bibliography-file').value = getFilePath(file);
} }
}; };
input.click(); input.click();
@@ -2739,7 +2747,7 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
document.getElementById('csl-file').value = file.path; document.getElementById('csl-file').value = getFilePath(file);
} }
}; };
input.click(); input.click();
@@ -3590,8 +3598,9 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
converterFilePath = file.path; const filePath = getFilePath(file);
document.getElementById('converter-file-path').value = file.path; converterFilePath = filePath;
document.getElementById('converter-file-path').value = filePath;
} }
}; };
input.click(); input.click();
@@ -3942,8 +3951,9 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => { input.onchange = (e) => {
const files = Array.from(e.target.files); const files = Array.from(e.target.files);
files.forEach((file) => { files.forEach((file) => {
if (!mergeFilePaths.includes(file.path)) { const filePath = getFilePath(file);
mergeFilePaths.push(file.path); if (!mergeFilePaths.includes(filePath)) {
mergeFilePaths.push(filePath);
} }
}); });
updateMergeFilesList(); updateMergeFilesList();
@@ -4127,8 +4137,9 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
document.getElementById(button.inputId).value = file.path; const filePath = getFilePath(file);
onPDFFileSelected(button.inputId, file.path); document.getElementById(button.inputId).value = filePath;
onPDFFileSelected(button.inputId, filePath);
} }
}; };
input.click(); input.click();
@@ -4137,10 +4148,11 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
document.getElementById(button.inputId).value = file.path; const filePath = getFilePath(file);
onPDFFileSelected(button.inputId, file.path); document.getElementById(button.inputId).value = filePath;
onPDFFileSelected(button.inputId, filePath);
if (button.inputId === 'fill-form-input-path') { if (button.inputId === 'fill-form-input-path') {
loadFillFormFields(file.path); loadFillFormFields(filePath);
} }
} }
}; };
+4 -2
View File
@@ -9,7 +9,8 @@
* markup appended to document.body on first use, driven by ModalManager, with a * markup appended to document.body on first use, driven by ModalManager, with a
* status line reusing the `info-message` / `warning-message` classes. File * status line reusing the `info-message` / `warning-message` classes. File
* selection reuses the app's existing convention: a plain `<input type="file">` * selection reuses the app's existing convention: a plain `<input type="file">`
* 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 * 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 * existing `read-file` invoke channel (the same one the renderer's electronAPI
* adapters use), not through a new privileged renderer-side file path. * adapters use), not through a new privileged renderer-side file path.
@@ -28,6 +29,7 @@
*/ */
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const { getFilePath } = require('../utils/file-path');
const { computeLineDiff } = require('../utils/line-diff'); const { computeLineDiff } = require('../utils/line-diff');
const COMPARE_ACCEPT = '.md,.markdown,.txt,.text,.log,.json,.xml,.yml,.yaml,.csv'; const COMPARE_ACCEPT = '.md,.markdown,.txt,.text,.log,.json,.xml,.yml,.yaml,.csv';
@@ -140,7 +142,7 @@ function chooseFile(input) {
fileInput.onchange = (e) => { fileInput.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
input.value = file.path; input.value = getFilePath(file);
clearResult(); clearResult();
} }
}; };
+9 -6
View File
@@ -12,8 +12,9 @@
* three media kinds together cover 13 distinct operations. * three media kinds together cover 13 distinct operations.
* *
* File selection reuses the app's existing convention: a plain `<input type="file">` * File selection reuses the app's existing convention: a plain `<input type="file">`
* whose `.path` is read directly (nodeIntegration is enabled for this renderer), the * whose chosen File is resolved to a path via `window.electronAPI.getFilePath`
* same approach already used throughout the PDF Editor and Universal Converter * (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 * 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 * *folder* selection (used by the video "Extract Frames" operation, and by batch
* mode below) reuses the existing generic `select-folder` / `folder-selected` IPC * mode below) reuses the existing generic `select-folder` / `folder-selected` IPC
@@ -33,6 +34,7 @@
*/ */
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const { getFilePath } = require('../utils/file-path');
const IMAGE_ACCEPT = '.jpg,.jpeg,.png,.webp,.avif,.tiff,.tif,.gif'; const IMAGE_ACCEPT = '.jpg,.jpeg,.png,.webp,.avif,.tiff,.tif,.gif';
const AUDIO_ACCEPT = '.mp3,.wav,.ogg,.flac,.aac,.m4a,.wma'; const AUDIO_ACCEPT = '.mp3,.wav,.ogg,.flac,.aac,.m4a,.wma';
@@ -447,7 +449,7 @@ function renderFileField(field) {
fileInput.accept = field.accept || '*'; fileInput.accept = field.accept || '*';
fileInput.onchange = (e) => { fileInput.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) input.value = file.path; if (file) input.value = getFilePath(file);
}; };
fileInput.click(); fileInput.click();
}, },
@@ -462,7 +464,7 @@ function renderSaveField(field) {
fileInput.nwsaveas = true; fileInput.nwsaveas = true;
fileInput.onchange = (e) => { fileInput.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) input.value = file.path; if (file) input.value = getFilePath(file);
}; };
fileInput.click(); fileInput.click();
}, },
@@ -588,8 +590,9 @@ function renderFilesField(field) {
fileInput.multiple = true; fileInput.multiple = true;
fileInput.onchange = (e) => { fileInput.onchange = (e) => {
Array.from(e.target.files).forEach((file) => { Array.from(e.target.files).forEach((file) => {
if (!mergeFilePaths.includes(file.path)) { const filePath = getFilePath(file);
mergeFilePaths.push(file.path); if (!mergeFilePaths.includes(filePath)) {
mergeFilePaths.push(filePath);
} }
}); });
updateMergeFilesList(listContainer); updateMergeFilesList(listContainer);
+30
View File
@@ -0,0 +1,30 @@
/**
* File-input path resolution for Electron 41.
*
* `File.path` was removed in Electron 32, so a plain `<input type="file">`
* 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 `<input type="file">` 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 };
+38
View File
@@ -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');
});
});
+61
View File
@@ -182,4 +182,65 @@ describe('Preload Security', () => {
expect(window.electronAPI.pdf.getPageCount).toBeDefined(); 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');
});
});
}); });
+2
View File
@@ -11,6 +11,8 @@ global.window.electronAPI = {
once: jest.fn(), once: jest.fn(),
invoke: jest.fn(() => Promise.resolve(null)), invoke: jest.fn(() => Promise.resolve(null)),
removeAllListeners: jest.fn(), removeAllListeners: jest.fn(),
// webUtils.getPathForFile bridge (File.path was removed in Electron 32)
getFilePath: jest.fn((file) => file && file.path),
file: { file: {
save: jest.fn(), save: jest.fn(),
saveCurrent: jest.fn(), saveCurrent: jest.fn(),