mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-23 23:10:17 +05:30
fix(pdf): make encrypt/decrypt/permissions fail honestly instead of silent no-op
This commit is contained in:
@@ -4760,6 +4760,12 @@ ipcMain.on('process-pdf-operation', async (event, data) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
// Reports pdf-lib's encryption capability (Task 27) so the renderer can
|
||||
// disable the password-protection controls instead of letting the user
|
||||
// fill the form only to see the operation fail.
|
||||
ipcMain.handle('get-pdf-capabilities', async () => ({
|
||||
passwordProtection: await PDFOperations.pdfEncryptionSupported,
|
||||
}));
|
||||
ipcMain.on('get-pdf-page-count', async (event, filePath) => {
|
||||
try {
|
||||
const count = await PDFOperations.getPageCount(filePath);
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
* cannot supply (merge takes many inputs in one op; reorder needs each
|
||||
* file's full page order; fillForm's field values differ per file).
|
||||
* - formFields — a read-only query returning data, not a transform.
|
||||
* - encrypt / decrypt / permissions — pdf-lib 1.17.1 (bundled) silently
|
||||
* ignores encryption options in save() and cannot decrypt on load, so a
|
||||
* batch run would either write unprotected files while reporting success
|
||||
* or deterministically fail every file.
|
||||
* - encrypt / decrypt / permissions — pdf-lib 1.17.1 (bundled) lacks
|
||||
* encryption support, so since Task 27 these ops fail honestly with an
|
||||
* "unavailable" result instead of silently writing unprotected files;
|
||||
* a batch run would deterministically fail every file.
|
||||
*
|
||||
* @module PDFBatchOperations
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,30 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
|
||||
|
||||
// pdf-lib 1.17.1 cannot encrypt: SaveOptions has no userPassword/ownerPassword/
|
||||
// permissions fields, so save() silently ignores them and writes an unprotected
|
||||
// file, and PDFDocument.load() cannot open password-protected input (verified
|
||||
// empirically in Task 22's review). Rather than trusting a pinned version
|
||||
// string, probe the installed library once at module load: save a tiny
|
||||
// in-memory document with a userPassword and check the raw bytes for an
|
||||
// /Encrypt dictionary (which an unencrypted document never contains). A library
|
||||
// that supports encryption passes the probe and the password ops re-enable
|
||||
// automatically. Probe errors fail closed (treated as unsupported).
|
||||
const pdfEncryptionSupported = (async () => {
|
||||
try {
|
||||
const probeDoc = await PDFDocument.create();
|
||||
const probeBytes = await probeDoc.save({ userPassword: 'encryption-capability-probe' });
|
||||
return Buffer.from(probeBytes).includes('/Encrypt');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
// Returned by the password ops when the probe reports no encryption support
|
||||
// (Task 27): fail honestly instead of silently writing an unprotected file.
|
||||
const PDF_ENCRYPTION_UNAVAILABLE_MESSAGE =
|
||||
'Password protection is not available in this build (pdf-lib lacks encryption support).';
|
||||
|
||||
function parsePageRanges(rangeString, totalPages) {
|
||||
const pages = [];
|
||||
const ranges = rangeString.split(',').map((r) => r.trim());
|
||||
@@ -301,6 +325,9 @@ async function pdfWatermark(data) {
|
||||
}
|
||||
|
||||
async function pdfEncrypt(data) {
|
||||
if (!(await pdfEncryptionSupported)) {
|
||||
return { success: false, message: PDF_ENCRYPTION_UNAVAILABLE_MESSAGE };
|
||||
}
|
||||
try {
|
||||
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||
const pdf = await PDFDocument.load(pdfBytes);
|
||||
@@ -335,6 +362,9 @@ async function pdfEncrypt(data) {
|
||||
}
|
||||
|
||||
async function pdfDecrypt(data) {
|
||||
if (!(await pdfEncryptionSupported)) {
|
||||
return { success: false, message: PDF_ENCRYPTION_UNAVAILABLE_MESSAGE };
|
||||
}
|
||||
try {
|
||||
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||
const pdf = await PDFDocument.load(pdfBytes, { password: data.password });
|
||||
@@ -352,6 +382,9 @@ async function pdfDecrypt(data) {
|
||||
}
|
||||
|
||||
async function pdfSetPermissions(data) {
|
||||
if (!(await pdfEncryptionSupported)) {
|
||||
return { success: false, message: PDF_ENCRYPTION_UNAVAILABLE_MESSAGE };
|
||||
}
|
||||
try {
|
||||
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||
const loadOptions = data.currentPassword ? { password: data.currentPassword } : {};
|
||||
@@ -701,6 +734,8 @@ async function getPageCount(filePath) {
|
||||
module.exports = {
|
||||
parsePageRanges,
|
||||
hexToRgb,
|
||||
pdfEncryptionSupported,
|
||||
PDF_ENCRYPTION_UNAVAILABLE_MESSAGE,
|
||||
pdfMerge,
|
||||
pdfSplit,
|
||||
pdfCompress,
|
||||
|
||||
@@ -86,6 +86,7 @@ const ALLOWED_SEND_CHANNELS = [
|
||||
// PDF operations
|
||||
'process-pdf-operation',
|
||||
'get-pdf-page-count',
|
||||
'get-pdf-capabilities',
|
||||
'select-pdf-folder',
|
||||
'batch-pdf-operation',
|
||||
|
||||
|
||||
+34
-1
@@ -3922,8 +3922,41 @@ function updateMergeFilesList() {
|
||||
});
|
||||
}
|
||||
|
||||
// Task 27: pdf-lib 1.17.1 cannot encrypt, so encrypt/decrypt/permissions fail
|
||||
// honestly in the main process. Ask main once whether password protection is
|
||||
// available and, when it is not, disable the three sections' controls with an
|
||||
// explanatory hint instead of letting the user fill the form only to see the
|
||||
// operation fail. If the capability query itself fails, leave the controls
|
||||
// enabled — main still fails the operation honestly on submit.
|
||||
async function applyPDFPasswordProtectionAvailability() {
|
||||
let capabilities;
|
||||
try {
|
||||
capabilities = await ipcRenderer.invoke('get-pdf-capabilities');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!capabilities || capabilities.passwordProtection !== false) return;
|
||||
|
||||
const sectionIds = ['pdf-encrypt-section', 'pdf-decrypt-section', 'pdf-permissions-section'];
|
||||
for (const sectionId of sectionIds) {
|
||||
const section = document.getElementById(sectionId);
|
||||
if (!section) continue;
|
||||
section.querySelectorAll('input, select, button').forEach((control) => {
|
||||
control.disabled = true;
|
||||
});
|
||||
const hint = document.createElement('p');
|
||||
hint.className = 'warning-message pdf-unavailable-hint';
|
||||
hint.textContent =
|
||||
'Password protection is not available in this build (pdf-lib lacks encryption support).';
|
||||
section.prepend(hint);
|
||||
}
|
||||
}
|
||||
|
||||
// PDF Editor Event Listeners
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Disable password-protection controls when pdf-lib lacks encryption support
|
||||
applyPDFPasswordProtectionAvailability();
|
||||
|
||||
// Close PDF Editor Dialog
|
||||
const pdfEditorClose = document.getElementById('pdf-editor-dialog-close');
|
||||
if (pdfEditorClose) {
|
||||
@@ -4694,7 +4727,7 @@ ipcRenderer.on('pdf-operation-complete', (event, { success, error, message }) =>
|
||||
}
|
||||
}, 800);
|
||||
} else {
|
||||
showPDFStatus(`Error: ${error || 'PDF operation failed'}`, 'warning');
|
||||
showPDFStatus(`Error: ${error || message || 'PDF operation failed'}`, 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -385,3 +385,110 @@ describe('PDFOperations - Task 16 form field fill/flatten', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PDFOperations - Task 27 honest encryption failure', () => {
|
||||
let tmpDir, inputPath;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pdfops_pw_'));
|
||||
inputPath = path.join(tmpDir, 'in.pdf');
|
||||
|
||||
const doc = await PDFDocument.create();
|
||||
doc.addPage([600, 800]);
|
||||
fs.writeFileSync(inputPath, await doc.save());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('detects the bundled pdf-lib as encryption-incapable via the module-load probe', async () => {
|
||||
// Pins the Task 27 premise: pdf-lib 1.17.1's save() ignores password
|
||||
// options (SaveOptions has no such fields), so the probe — which saves a
|
||||
// tiny document with a userPassword and checks the bytes for /Encrypt —
|
||||
// must report false. If this fails after a library swap, the probe
|
||||
// re-enabled the ops and the honest-failure tests below no longer apply.
|
||||
await expect(PDFOperations.pdfEncryptionSupported).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('pdfEncrypt fails honestly without writing an output file', async () => {
|
||||
const outputPath = path.join(tmpDir, 'encrypted.pdf');
|
||||
const result = await PDFOperations.pdfEncrypt({
|
||||
inputPath,
|
||||
outputPath,
|
||||
userPassword: 'secret',
|
||||
ownerPassword: 'owner-secret',
|
||||
permissions: { printing: true },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(fs.existsSync(outputPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('pdfDecrypt fails honestly without writing an output file', async () => {
|
||||
const outputPath = path.join(tmpDir, 'decrypted.pdf');
|
||||
const result = await PDFOperations.pdfDecrypt({
|
||||
inputPath,
|
||||
outputPath,
|
||||
password: 'secret',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(fs.existsSync(outputPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('pdfSetPermissions fails honestly without writing an output file', async () => {
|
||||
const outputPath = path.join(tmpDir, 'permissions.pdf');
|
||||
const result = await PDFOperations.pdfSetPermissions({
|
||||
inputPath,
|
||||
outputPath,
|
||||
ownerPassword: 'owner-secret',
|
||||
permissions: { printing: true },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(fs.existsSync(outputPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('fails honestly even before reading the input, so a missing input reports unavailability', async () => {
|
||||
const result = await PDFOperations.pdfEncrypt({
|
||||
inputPath: path.join(tmpDir, 'missing.pdf'),
|
||||
outputPath: path.join(tmpDir, 'never-written.pdf'),
|
||||
userPassword: 'secret',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(fs.existsSync(path.join(tmpDir, 'never-written.pdf'))).toBe(false);
|
||||
});
|
||||
|
||||
it('executeOperation routes the password ops to the honest failure', async () => {
|
||||
const result = await PDFOperations.executeOperation('encrypt', {
|
||||
inputPath,
|
||||
outputPath: path.join(tmpDir, 'exec-encrypted.pdf'),
|
||||
userPassword: 'secret',
|
||||
permissions: { printing: true },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
});
|
||||
|
||||
it('the module-load probe does not affect other operations', async () => {
|
||||
const outputPath = path.join(tmpDir, 'rotated.pdf');
|
||||
const result = await PDFOperations.pdfRotate({
|
||||
inputPath,
|
||||
outputPath,
|
||||
pages: '1',
|
||||
angle: 90,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
const rotated = await PDFDocument.load(fs.readFileSync(outputPath));
|
||||
expect(rotated.getPageCount()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user