mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(pdf): stop page tools failing on short and encrypted PDFs (#594)
Empty the hardcoded page-range default in remove/split/extract PDF tools (remove-pages defaulted to "2,4-6", out of range for any PDF under 6 pages) and disable submit until a range is entered. Reject password-protected PDFs up front for PDF-only tools with guidance to unlock first, instead of failing cryptically in the qpdf worker. Adds integration + e2e coverage.
This commit is contained in:
@@ -43,7 +43,9 @@ export async function validatePdfPath(
|
||||
opts.signal?.throwIfAborted();
|
||||
if (passwordProtected) {
|
||||
if (opts.rejectPasswordProtected) {
|
||||
throw new InputValidationError("Password-protected PDFs cannot be processed by this tool");
|
||||
throw new InputValidationError(
|
||||
"This PDF is password-protected. Unlock it first with the Unlock PDF tool, then try again.",
|
||||
);
|
||||
}
|
||||
// Without a password qpdf cannot safely inspect the structure or pages.
|
||||
return;
|
||||
|
||||
@@ -118,6 +118,14 @@ export interface ToolRouteConfig<T> {
|
||||
* tools that intentionally accept damaged inputs (e.g. repair-pdf).
|
||||
*/
|
||||
skipStructuralValidation?: boolean;
|
||||
/**
|
||||
* When set, the factory does NOT reject password-protected PDFs at input
|
||||
* validation. Only unlock-pdf sets this: it takes an encrypted PDF plus a
|
||||
* password and decrypts it. Every other document tool leaves this off, so
|
||||
* the factory rejects encrypted PDFs up front (400) with guidance to unlock
|
||||
* first, instead of letting qpdf fail cryptically in the worker.
|
||||
*/
|
||||
allowPasswordProtectedPdf?: boolean;
|
||||
/**
|
||||
* When set, produces a redacted copy of settings for the durable DB
|
||||
* row. Passwords and other secrets are replaced so they do not persist
|
||||
@@ -398,6 +406,15 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
const prepared = await handlerForPosition(i).prepare(fileBuffer, fname, {
|
||||
scratchDir,
|
||||
lenient: config.skipStructuralValidation,
|
||||
// Reject encrypted PDFs up front only for PDF-only tools (qpdf
|
||||
// page ops etc.). Scoped to acceptedInputs === [".pdf"] so the
|
||||
// flag never forces a %PDF- header on non-PDF document tools
|
||||
// (markdown/epub/docx converters). unlock-pdf opts out.
|
||||
rejectPasswordProtected:
|
||||
modality === "document" &&
|
||||
!config.allowPasswordProtectedPdf &&
|
||||
!!accepted?.length &&
|
||||
accepted.every((e) => e === ".pdf"),
|
||||
});
|
||||
fileBuffer = prepared.buffer;
|
||||
fname = prepared.filename;
|
||||
|
||||
@@ -123,7 +123,13 @@ export function registerSignPdf(app: FastifyInstance) {
|
||||
|
||||
const pdfBuffer = await getObjectBuffer(pdfKey);
|
||||
try {
|
||||
await inputHandlerFor("document").prepare(pdfBuffer, filename, { scratchDir: tmpdir() });
|
||||
// Signing needs a readable PDF; reject encrypted ones up front (the
|
||||
// "unlock first" guidance rides in details) with the same policy the
|
||||
// factory gives other PDF-only tools, instead of failing in the worker.
|
||||
await inputHandlerFor("document").prepare(pdfBuffer, filename, {
|
||||
scratchDir: tmpdir(),
|
||||
rejectPasswordProtected: true,
|
||||
});
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid PDF",
|
||||
|
||||
@@ -13,6 +13,10 @@ export function registerUnlockPdf(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
toolId: "unlock-pdf",
|
||||
settingsSchema,
|
||||
// unlock-pdf's whole job is to decrypt: its input is an encrypted PDF plus
|
||||
// the password, so it must opt out of the factory's password-protected
|
||||
// rejection that every other document tool gets by default.
|
||||
allowPasswordProtectedPdf: true,
|
||||
redactSettingsForAudit: (settings) => {
|
||||
const s = settings as z.infer<typeof settingsSchema>;
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user