diff --git a/src/core/obj.js b/src/core/obj.js index 9bb26fc5d..f05d373a2 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -15,9 +15,9 @@ import { bytesToString, createPromiseCapability, createValidAbsoluteUrl, FormatError, - info, InvalidPDFException, isBool, isString, MissingDataException, shadow, - stringToPDFString, stringToUTF8String, toRomanNumerals, unreachable, warn, - XRefParseException + info, InvalidPDFException, isBool, isNum, isString, MissingDataException, + PermissionFlag, shadow, stringToPDFString, stringToUTF8String, + toRomanNumerals, unreachable, warn, XRefParseException } from '../shared/util'; import { Dict, isCmd, isDict, isName, isRef, isRefsEqual, isStream, Ref, RefSet, @@ -177,6 +177,48 @@ class Catalog { return (root.items.length > 0 ? root.items : null); } + get permissions() { + let permissions = null; + try { + permissions = this._readPermissions(); + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + warn('Unable to read permissions.'); + } + return shadow(this, 'permissions', permissions); + } + + /** + * @private + */ + _readPermissions() { + const encrypt = this.xref.trailer.get('Encrypt'); + if (!isDict(encrypt)) { + return null; + } + + let flags = encrypt.get('P'); + if (!isNum(flags)) { + return null; + } + + // PDF integer objects are represented internally in signed 2's complement + // form. Therefore, convert the signed decimal integer to a signed 2's + // complement binary integer so we can use regular bitwise operations on it. + flags += 2 ** 32; + + const permissions = []; + for (const key in PermissionFlag) { + const value = PermissionFlag[key]; + if (flags & value) { + permissions.push(value); + } + } + return permissions; + } + get numPages() { const obj = this.toplevelPagesDict.get('Count'); if (!Number.isInteger(obj)) { diff --git a/src/core/worker.js b/src/core/worker.js index 454cff51a..8bfc9c459 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -703,6 +703,10 @@ var WorkerMessageHandler = { } ); + handler.on('GetPermissions', function(data) { + return pdfManager.ensureCatalog('permissions'); + }); + handler.on('GetMetadata', function wphSetupGetMetadata(data) { return Promise.all([pdfManager.ensureDoc('documentInfo'), diff --git a/src/display/api.js b/src/display/api.js index 9f34e916f..9e58df398 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -691,6 +691,15 @@ class PDFDocumentProxy { return this._transport.getOutline(); } + /** + * @return {Promise} A promise that is resolved with an {Array} that contains + * the permission flags for the PDF document, or `null` when + * no permissions are present in the PDF file. + */ + getPermissions() { + return this._transport.getPermissions(); + } + /** * @return {Promise} A promise that is resolved with an {Object} that has * `info` and `metadata` properties. `info` is an {Object} filled with @@ -2141,6 +2150,10 @@ class WorkerTransport { return this.messageHandler.sendWithPromise('GetOutline', null); } + getPermissions() { + return this.messageHandler.sendWithPromise('GetPermissions', null); + } + getMetadata() { return this.messageHandler.sendWithPromise('GetMetadata', null). then((results) => { diff --git a/src/shared/util.js b/src/shared/util.js index 80115a102..b823e6bdc 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -25,6 +25,18 @@ const NativeImageDecoding = { DISPLAY: 'display', }; +// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification. +const PermissionFlag = { + PRINT: 0x04, + MODIFY_CONTENTS: 0x08, + COPY: 0x10, + MODIFY_ANNOTATIONS: 0x20, + FILL_INTERACTIVE_FORMS: 0x100, + COPY_FOR_ACCESSIBILITY: 0x200, + ASSEMBLE: 0x400, + PRINT_HIGH_QUALITY: 0x800, +}; + var TextRenderingMode = { FILL: 0, STROKE: 1, @@ -1014,6 +1026,7 @@ export { NativeImageDecoding, PasswordException, PasswordResponses, + PermissionFlag, StreamType, TextRenderingMode, UnexpectedResponseException, diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 59475f4e8..1d1e66114 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -325,3 +325,6 @@ !transparent.pdf !xobject-image.pdf !ccitt_EndOfBlock_false.pdf +!issue9972-1.pdf +!issue9972-2.pdf +!issue9972-3.pdf diff --git a/test/pdfs/issue9972-1.pdf b/test/pdfs/issue9972-1.pdf new file mode 100644 index 000000000..4ce8d3ff1 Binary files /dev/null and b/test/pdfs/issue9972-1.pdf differ diff --git a/test/pdfs/issue9972-2.pdf b/test/pdfs/issue9972-2.pdf new file mode 100644 index 000000000..381eae506 Binary files /dev/null and b/test/pdfs/issue9972-2.pdf differ diff --git a/test/pdfs/issue9972-3.pdf b/test/pdfs/issue9972-3.pdf new file mode 100644 index 000000000..a5c9d1912 Binary files /dev/null and b/test/pdfs/issue9972-3.pdf differ diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 35f415f2a..380b951a3 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -18,7 +18,8 @@ import { } from './test_utils'; import { createPromiseCapability, FontType, InvalidPDFException, MissingPDFException, - OPS, PasswordException, PasswordResponses, StreamType, stringToBytes + OPS, PasswordException, PasswordResponses, PermissionFlag, StreamType, + stringToBytes } from '../../src/shared/util'; import { DOMCanvasFactory, RenderingCancelledException, StatTimer @@ -822,6 +823,63 @@ describe('api', function() { done.fail(reason); }); }); + + it('gets non-existent permissions', function(done) { + doc.getPermissions().then(function(permissions) { + expect(permissions).toEqual(null); + + done(); + }).catch(function(reason) { + done.fail(reason); + }); + }); + + it('gets permissions', function (done) { + // Editing not allowed. + const loadingTask0 = + getDocument(buildGetDocumentParams('issue9972-1.pdf')); + const promise0 = loadingTask0.promise.then(function(pdfDocument) { + return pdfDocument.getPermissions(); + }); + + // Printing not allowed. + const loadingTask1 = + getDocument(buildGetDocumentParams('issue9972-2.pdf')); + const promise1 = loadingTask1.promise.then(function(pdfDocument) { + return pdfDocument.getPermissions(); + }); + + // Copying not allowed. + const loadingTask2 = + getDocument(buildGetDocumentParams('issue9972-3.pdf')); + const promise2 = loadingTask2.promise.then(function(pdfDocument) { + return pdfDocument.getPermissions(); + }); + + const totalPermissionCount = Object.keys(PermissionFlag).length; + Promise.all([promise0, promise1, promise2]).then(function(permissions) { + expect(permissions[0].length).toEqual(totalPermissionCount - 1); + expect(permissions[0].includes(PermissionFlag.MODIFY_CONTENTS)) + .toBeFalsy(); + + expect(permissions[1].length).toEqual(totalPermissionCount - 2); + expect(permissions[1].includes(PermissionFlag.PRINT)).toBeFalsy(); + expect(permissions[1].includes(PermissionFlag.PRINT_HIGH_QUALITY)) + .toBeFalsy(); + + expect(permissions[2].length).toEqual(totalPermissionCount - 1); + expect(permissions[2].includes(PermissionFlag.COPY)).toBeFalsy(); + + Promise.all([ + loadingTask0.destroy(), + loadingTask1.destroy(), + loadingTask2.destroy(), + ]).then(done); + }).catch(function(reason) { + done.fail(reason); + }); + }); + it('gets metadata', function(done) { var promise = doc.getMetadata(); promise.then(function({ info, metadata, contentDispositionFilename, }) {