Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 100 additions & 6 deletions test/core/workflow/workflow-acrobat/action-binder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,6 @@ describe('ActionBinder', () => {
describe('isDirectUploadVerb', () => {
beforeEach(() => {
actionBinder.workflowCfg.enabledFeatures = ['word-to-pdf'];
actionBinder.workflowCfg.targetCfg.directUploadVerbs = ['word-to-pdf'];
actionBinder.workflowCfg.targetCfg.directUploadMaxSize = 1048576;
});

Expand All @@ -1538,11 +1537,6 @@ describe('ActionBinder', () => {
it('should return false for direct upload verbs without file size', () => {
expect(actionBinder.isDirectUploadVerb()).to.be.false;
});

it('should return false for verbs not configured for direct upload', () => {
actionBinder.workflowCfg.enabledFeatures = ['compress-pdf'];
expect(actionBinder.isDirectUploadVerb(500000)).to.be.false;
});
});

describe('continueInApp', () => {
Expand Down Expand Up @@ -3142,6 +3136,106 @@ describe('ActionBinder', () => {
});
});

describe('filterFilesWithPdflite - integrity/acroform/scanned checks', () => {
let originalPdflite;
let pdfDetailsStub;

const pdf = (name) => new File(['%PDF-1.4 test'], name, { type: 'application/pdf' });

beforeEach(() => {
originalPdflite = window.pdflite;
actionBinder.MULTI_FILE = false;
actionBinder.multiFileValidationFailure = false;
actionBinder.limits = { pageLimit: { maxNumPages: 100 } };
actionBinder.workflowCfg = {
enabledFeatures: ['stylize'],
targetCfg: {
pdfIntegrityCheckVerbs: ['stylize'],
pdfAcroformCheckVerbs: ['stylize'],
pdfScannedCheckVerbs: ['stylize'],
},
};
sinon.stub(actionBinder, 'dispatchErrorToast').resolves();
pdfDetailsStub = sinon.stub().returns({ NUM_PAGES: 1 });
window.pdflite = { pdfDetails: pdfDetailsStub };
});

afterEach(() => {
window.pdflite = originalPdflite;
});

it('excludes AcroForm files and dispatches the acroform error', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 1, HAS_ACROFORM: true });
const result = await actionBinder.filterFilesWithPdflite([pdf('form.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_acroform_not_supported')).to.be.true;
});

it('excludes scanned files and dispatches the scanned-document error', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 1, IS_SCANNED_DOCUMENT: true });
const result = await actionBinder.filterFilesWithPdflite([pdf('scanned.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_scanned_document')).to.be.true;
});

it('excludes empty files with the empty-file error, not the corrupt/encrypted one', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 0, IS_EMPTY: true });
const result = await actionBinder.filterFilesWithPdflite([pdf('empty.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_empty_file')).to.be.true;
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_password_protected')).to.be.false;
});

it('excludes encrypted/password-protected files via the integrity error', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 1, IS_ENCRYPTED: true });
const result = await actionBinder.filterFilesWithPdflite([pdf('encrypted.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_password_protected')).to.be.true;
});

it('treats a fileDetails failure as a corrupted file via the integrity error', async () => {
pdfDetailsStub.returns({ error: 'not found' });
const result = await actionBinder.filterFilesWithPdflite([pdf('corrupt.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_password_protected')).to.be.true;
});

it('passes checkScanned=true to pdflite when the verb is in pdfScannedCheckVerbs', async () => {
await actionBinder.filterFilesWithPdflite([pdf('a.pdf')]);
expect(pdfDetailsStub.calledWith(sinon.match.any, true)).to.be.true;
});

it('does not request the scan when the verb is not in pdfScannedCheckVerbs', async () => {
actionBinder.workflowCfg.targetCfg.pdfScannedCheckVerbs = [];
await actionBinder.filterFilesWithPdflite([pdf('a.pdf')]);
expect(pdfDetailsStub.calledWith(sinon.match.any, false)).to.be.true;
});

it('passes a clean PDF through without dispatching an error', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 5, HAS_ACROFORM: false, IS_ENCRYPTED: false, IS_SCANNED_DOCUMENT: false });
const file = pdf('clean.pdf');
const result = await actionBinder.filterFilesWithPdflite([file]);
expect(result).to.deep.equal([file]);
expect(actionBinder.dispatchErrorToast.called).to.be.false;
});

it('skips pdflite entirely for verbs not in any check list and without page limits', async () => {
actionBinder.limits = {};
actionBinder.workflowCfg.targetCfg = {};
const files = [pdf('x.pdf')];
const result = await actionBinder.filterFilesWithPdflite(files);
expect(result).to.equal(files);
expect(pdfDetailsStub.called).to.be.false;
});

it('sets multiFileValidationFailure when a file is excluded in multi-file mode', async () => {
actionBinder.MULTI_FILE = true;
pdfDetailsStub.returns({ NUM_PAGES: 1, HAS_ACROFORM: true });
await actionBinder.filterFilesWithPdflite([pdf('form.pdf')]);
expect(actionBinder.multiFileValidationFailure).to.be.true;
});
});

describe('ensurePageConfig', () => {
let originalFetch;

Expand Down
44 changes: 44 additions & 0 deletions test/unitylibs/scripts/pdflite-validator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,50 @@ describe('PDFLite Validator', () => {
});
});

describe('encrypted / password-protected flags (black box)', () => {
it('returns a results entry for every input file', async () => {
const files = [
{ type: 'application/pdf', name: 'a.pdf' },
{ type: 'application/pdf', name: 'b.pdf' },
];
const limits = { pageLimit: { maxNumPages: 100 } };

const result = await validateFilesWithPdflite(files, limits);

expect(result).to.have.property('results');
expect(result.results).to.have.lengthOf(files.length);
});

it('passes files through gracefully when pdflite cannot read them (no throw)', async () => {
const files = [{ type: 'application/pdf', name: 'unreadable.pdf' }];
const limits = { pageLimit: { maxNumPages: 100 } };

const result = await validateFilesWithPdflite(files, limits);

expect(result.passed.length + result.failed.length).to.equal(files.length);
result.results.forEach((r) => {
if (r.ok === false) {
expect(['OVER_MAX_PAGE_COUNT', 'UNDER_MIN_PAGE_COUNT']).to.include(r.errorType);
}
});
});

it('never flags non-PDF files as encrypted or password-protected', async () => {
const files = [
{ type: 'image/jpeg', name: 'photo.jpg' },
{ type: 'application/pdf', name: 'doc.pdf' },
];
const limits = { pageLimit: { maxNumPages: 100 } };

const result = await validateFilesWithPdflite(files, limits);

const jpegResult = result.results.find((r) => r.file.name === 'photo.jpg');
expect(jpegResult.ok).to.equal(true);
expect(jpegResult.isEncrypted).to.be.undefined;
expect(jpegResult.isPasswordProtected).to.be.undefined;
});
});

describe('getPageCountErrorCode', () => {
const SINGLE_FILE_ERRORS = {
OVER_MAX_PAGE_COUNT: 'upload_validation_error_max_page_count',
Expand Down
77 changes: 69 additions & 8 deletions unitylibs/core/workflow/workflow-acrobat/action-binder.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export default class ActionBinder {
SAME_FILE_TYPE: 'validation_error_file_same_type',
OVER_MAX_PAGE_COUNT: 'upload_validation_error_max_page_count',
UNDER_MIN_PAGE_COUNT: 'upload_validation_error_min_page_count',
PASSWORD_PROTECTED: 'validation_error_password_protected',
ACROFORM_NOT_SUPPORTED: 'validation_error_acroform_not_supported',
SCANNED_DOCUMENT: 'validation_error_scanned_document',
};

static MULTI_FILE_ERROR_MESSAGES = {
Expand All @@ -42,6 +45,9 @@ export default class ActionBinder {
FILE_TOO_LARGE: 'validation_error_file_too_large_multi',
SAME_FILE_TYPE: 'validation_error_file_same_type_multi',
OVER_MAX_PAGE_COUNT: 'upload_validation_error_max_page_count_multi',
PASSWORD_PROTECTED: 'validation_error_password_protected_multi',
ACROFORM_NOT_SUPPORTED: 'validation_error_acroform_not_supported_multi',
SCANNED_DOCUMENT: 'validation_error_scanned_document_multi',
};

static LIMITS_MAP = {
Expand Down Expand Up @@ -87,6 +93,9 @@ export default class ActionBinder {
'flashcard-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'],
'mindmap-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'],
'resume-builder': ['single', 'allowed-filetypes-resume', 'page-limit-10', 'max-filesize-20-mb'],
'gen-presentation-v2': ['single', 'allowed-filetypes-pdf-only', 'page-limit-600', 'max-filesize-100-mb'],
'interactive-report': ['single', 'allowed-filetypes-pdf-only', 'page-limit-600', 'max-filesize-100-mb'],
stylize: ['single', 'allowed-filetypes-pdf-only', 'page-limit-15', 'max-filesize-100-mb'],
};

static ERROR_MAP = {
Expand All @@ -106,12 +115,18 @@ export default class ActionBinder {
validation_error_file_too_large: -103,
validation_error_only_accept_one_file: -104,
validation_error_file_same_type: -105,
validation_error_password_protected: -106,
validation_error_acroform_not_supported: -107,
validation_error_scanned_document: -108,
validation_error_unsupported_type_multi: -200,
validation_error_empty_file_multi: -201,
validation_error_file_too_large_multi: -202,
validation_error_multiple_invalid_files: -203,
validation_error_max_num_files: -204,
validation_error_password_protected_multi: -205,
validation_error_file_same_type_multi: -206,
validation_error_acroform_not_supported_multi: -207,
validation_error_scanned_document_multi: -208,
upload_validation_error_max_page_count: -300,
upload_validation_error_min_page_count: -301,
upload_validation_error_max_page_count_multi: -303,
Expand Down Expand Up @@ -142,6 +157,12 @@ export default class ActionBinder {
validation_error_file_too_large: 'verb_upload_error_file_too_large',
validation_error_only_accept_one_file: 'verb_upload_error_only_accept_one_file',
validation_error_file_same_type: 'verb_upload_error_file_same_type',
validation_error_password_protected: 'verb_upload_error_password_protected',
validation_error_password_protected_multi: 'verb_upload_error_password_protected_multi',
validation_error_acroform_not_supported: 'verb_upload_error_acroform_not_supported',
validation_error_acroform_not_supported_multi: 'verb_upload_error_acroform_not_supported_multi',
validation_error_scanned_document: 'verb_upload_error_scanned_document',
validation_error_scanned_document_multi: 'verb_upload_error_scanned_document_multi',
validation_error_unsupported_type_multi: 'verb_upload_error_unsupported_type_multi',
validation_error_empty_file_multi: 'verb_upload_error_empty_file_multi',
validation_error_file_too_large_multi: 'verb_upload_error_file_too_large_multi',
Expand Down Expand Up @@ -588,20 +609,62 @@ export default class ActionBinder {
}

async filterFilesWithPdflite(files) {
if (!this.limits.pageLimit) return files;
const verb = this.workflowCfg.enabledFeatures[0];
const runIntegrityCheck = (this.workflowCfg.targetCfg.pdfIntegrityCheckVerbs || []).includes(verb);
const runAcroformCheck = (this.workflowCfg.targetCfg.pdfAcroformCheckVerbs || []).includes(verb);
const runScannedCheck = (this.workflowCfg.targetCfg.pdfScannedCheckVerbs || []).includes(verb);
const runPageCountCheck = !!this.limits.pageLimit;
if (!runIntegrityCheck && !runAcroformCheck && !runScannedCheck && !runPageCountCheck) return files;
if (!files.some((file) => file.type === 'application/pdf')) return files;
try {
const { validateFilesWithPdflite, getPageCountErrorCode } = await import('../../../scripts/pdflite-validator.js');
const errorMessages = this.MULTI_FILE ? ActionBinder.MULTI_FILE_ERROR_MESSAGES : ActionBinder.SINGLE_FILE_ERROR_MESSAGES;
const { passed, failed, results } = await validateFilesWithPdflite(files, this.limits);
if (failed && failed.length > 0) {
const { passed, failed, results } = await validateFilesWithPdflite(files, this.limits, runScannedCheck);
let remaining = passed;
const applyPdfliteCheck = async (enabled, matches, errorKey, describe) => {
if (!enabled || !Array.isArray(results)) return;
const remainingSet = new Set(remaining);
const matched = results.filter((r) => r.ok && remainingSet.has(r.file) && matches(r));
if (matched.length === 0) return;
const matchedFiles = new Set(matched.map((r) => r.file));
remaining = remaining.filter((f) => !matchedFiles.has(f));
const errorCode = errorMessages[errorKey];
await this.dispatchErrorToast(errorCode, null, describe(matched.length), false, true, { code: 'validation_error_validate_files', subCode: errorCode });
if (this.MULTI_FILE) this.multiFileValidationFailure = true;
};
await applyPdfliteCheck(
runIntegrityCheck,
(r) => r.isEmpty === true,
'EMPTY_FILE',
(n) => `${n} file(s) are empty`,
);
await applyPdfliteCheck(
runIntegrityCheck,
(r) => r.isEncrypted === true || r.isPasswordProtected === true || r.isCorrupted === true,
'PASSWORD_PROTECTED',
(n) => `${n} file(s) encrypted, password-protected, or corrupted`,
);
await applyPdfliteCheck(
runAcroformCheck,
(r) => r.hasAcroForm === true,
'ACROFORM_NOT_SUPPORTED',
(n) => `${n} file(s) contain a form; forms are not supported`,
);
await applyPdfliteCheck(
runScannedCheck,
(r) => r.isScanned === true,
'SCANNED_DOCUMENT',
(n) => `${n} file(s) are scanned documents`,
);
if (runPageCountCheck && failed && failed.length > 0) {
const errorInfo = getPageCountErrorCode(failed, results, this.MULTI_FILE, errorMessages);
if (errorInfo?.shouldDispatch && errorInfo.errorCode) {
await this.dispatchErrorToast(errorInfo.errorCode, null, null, false, true, { code: errorInfo.errorCode });
if (errorInfo.returnEmpty) return [];
}
if (errorInfo?.setValidationFailure) this.multiFileValidationFailure = true;
}
return passed;
return remaining;
} catch (error) {
await this.dispatchErrorToast('error_generic', 500, `Exception during PDF validation: ${error.message}`, true);
return files;
Expand Down Expand Up @@ -641,7 +704,7 @@ export default class ActionBinder {
this.MULTI_FILE = files.length > 1;
const prevalidatedFiles = await this.filterFilesWithPdflite(sanitizedFiles);
if (prevalidatedFiles.length === 0) return;
const wordValidatedFiles = this.workflowCfg.enabledFeatures[0] === 'resume-builder'
const wordValidatedFiles = (this.workflowCfg.targetCfg.wordPageCountVerbs || []).includes(this.workflowCfg.enabledFeatures[0])
? await this.validateWordFilePageCount(prevalidatedFiles)
: prevalidatedFiles;
if (wordValidatedFiles.length === 0) return;
Expand Down Expand Up @@ -714,10 +777,8 @@ export default class ActionBinder {
}

isDirectUploadVerb(fileSize) {
const verb = this.workflowCfg.enabledFeatures[0];
const directUploadVerbs = this.workflowCfg.targetCfg.directUploadVerbs || [];
const directUploadMaxSize = this.workflowCfg.targetCfg.directUploadMaxSize || 0;
return directUploadVerbs.includes(verb) && fileSize != null && fileSize <= directUploadMaxSize;
return fileSize != null && fileSize <= directUploadMaxSize;
}

async runProgressBarUpdate(splashLayer) {
Expand Down
5 changes: 5 additions & 0 deletions unitylibs/core/workflow/workflow-acrobat/limits.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
"maxNumPages": 10
}
},
"page-limit-15": {
"pageLimit": {
"maxNumPages": 15
}
},
"max-filesize-5-mb": {
"maxFileSize": 5242880
},
Expand Down
5 changes: 4 additions & 1 deletion unitylibs/core/workflow/workflow-acrobat/target-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
"sendSplunkAnalytics": true,
"verbsWithoutMfuToSfuFallback": ["compress-pdf"],
"awaitFinalizeVerbs": ["word-to-pdf", "ppt-to-pdf", "excel-to-pdf"],
"directUploadVerbs": ["word-to-pdf", "ppt-to-pdf", "excel-to-pdf", "jpg-to-pdf", "png-to-pdf", "heic-to-pdf", "add-comment", "pdf-to-word", "compress-pdf", "pdf-to-image", "fillsign", "pdf-to-excel", "createpdf", "split-pdf", "pdf-to-ppt", "combine-pdf"],
"directUploadMaxSize": 1048576,
"nonpdfMfuFeedbackScreenTypeNonpdf": ["combine-pdf"],
"nonpdfSfuProductScreen": ["word-to-pdf", "jpg-to-pdf", "ppt-to-pdf", "excel-to-pdf", "png-to-pdf", "createpdf", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "heic-to-pdf", "image-to-pdf", "bmp-to-pdf", "gif-to-pdf", "tiff-to-pdf", "indd-to-pdf", "psd-to-pdf", "ai-to-pdf", "quiz-maker", "flashcard-maker", "mindmap-maker", "resume-builder"],
"mfuUploadAllowed": ["combine-pdf", "rotate-pages", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "quiz-maker", "flashcard-maker", "mindmap-maker", "heic-to-pdf"],
"mfuUploadOnlyPdfAllowed": ["combine-pdf"],
"experimentationOn": ["add-comment"],
"pdfIntegrityCheckVerbs": ["gen-presentation-v2", "interactive-report", "stylize"],
"pdfAcroformCheckVerbs": ["stylize"],
"pdfScannedCheckVerbs": ["stylize"],
"wordPageCountVerbs": ["resume-builder"],
"fetchApiConfig": {
"finalizeAsset": {
"retryType": "polling",
Expand Down
3 changes: 3 additions & 0 deletions unitylibs/core/workflow/workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ class WfInitiator {
'flashcard-maker',
'mindmap-maker',
'resume-builder',
'gen-presentation-v2',
'interactive-report',
'stylize',
]),
},
'workflow-ai': {
Expand Down
4 changes: 2 additions & 2 deletions unitylibs/libs/pdflite/dc-pdflite.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ export default class DcPdflite {
});
}

fileDetails(file) {
fileDetails(file, checkScanned = false) {
if (file.type != 'application/pdf') {
throw 'Not a PDF';
}

return this._readFile(file).then(result => {
let ubuf = new Uint8Array(result);
let res = window.pdflite.pdfDetails(ubuf);
let res = window.pdflite.pdfDetails(ubuf, checkScanned);
if (res.error !== undefined) {
throw res.error;
}
Expand Down
2 changes: 1 addition & 1 deletion unitylibs/libs/pdflite/pdfliteWasm.js

Large diffs are not rendered by default.

Loading
Loading