From 0a30d3961be76711ef03b468100ac9a22c156705 Mon Sep 17 00:00:00 2001 From: Jakob Miland Date: Mon, 14 May 2012 20:45:07 +0200 Subject: [PATCH 01/15] Support password and add the relevant l10n strings --- l10n/da/viewer.properties | 1 + l10n/en-US/viewer.properties | 1 + src/api.js | 63 ++++++++++++++++++++++++++++-------- src/core.js | 15 +++++---- src/crypto.js | 6 ++-- src/obj.js | 5 ++- src/util.js | 14 ++++++-- src/worker.js | 25 ++++++++++++-- web/viewer.js | 31 ++++++++++++------ 9 files changed, 122 insertions(+), 39 deletions(-) diff --git a/l10n/da/viewer.properties b/l10n/da/viewer.properties index 6d208db70..e098a7939 100644 --- a/l10n/da/viewer.properties +++ b/l10n/da/viewer.properties @@ -29,3 +29,4 @@ page_of=af {{pageCount}} no_outline=Ingen dokumentoversigt tilgængelig open_file.title=Åbn fil text_annotation_type=[{{type}} Kommentar] +request_password=PDF filen er beskyttet med et kodeord: diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index c8dbe4aba..d0ba6ffac 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -42,3 +42,4 @@ zoom_in_label=Zoom In zoom.title=Zoom thumb_page_title=Page {{page}} thumb_page_canvas=Thumbnail of Page {{page}} +request_password=PDF is protected by a password: diff --git a/src/api.js b/src/api.js index 1efb22caa..cd10ee366 100644 --- a/src/api.js +++ b/src/api.js @@ -7,20 +7,46 @@ * is used, which means it must follow the same origin rules that any XHR does * e.g. No cross domain requests without CORS. * - * @param {string|TypedAray} source Either a url to a PDF is located or a - * typed array (Uint8Array) already populated with data. - * @param {Object} headers An object containing the http headers like this: - * { Authorization: "BASIC XXX" }. + * @param {string|TypedAray|object} source Can be an url to where a PDF is + * located, a typed array (Uint8Array) already populated with data or + * and parameter object with the following possible fields: + * - url - The URL of the PDF. + * - data - A typed array with PDF data. + * - httpHeaders - Basic authentication headers. + * - password - For decrypting password-protected PDFs. + * * @return {Promise} A promise that is resolved with {PDFDocumentProxy} object. */ -PDFJS.getDocument = function getDocument(source, headers) { +PDFJS.getDocument = function getDocument(source) { + var url, data, headers, password, parameters = {}; + if (typeof source === 'string') { + url = params; + } else if (isArrayBuffer(source)) { + data = source; + } else if (typeof source === 'object') { + url = source.url; + data = source.data; + headers = source.httpHeaders; + password = source.password; + parameters.password = password || null; + + if (!url && !data) + error('Invalid parameter array, need either .data or .url'); + } else { + error('Invalid parameter in getDocument, need either Uint8Array, ' + + 'string or a parameter object'); + } + var promise = new PDFJS.Promise(); var transport = new WorkerTransport(promise); - if (typeof source === 'string') { + if (data) { + // assuming the data is array, instantiating directly from it + transport.sendData(data, parameters); + } else if (url) { // fetch url PDFJS.getPdf( { - url: source, + url: url, progress: function getPDFProgress(evt) { if (evt.lengthComputable) promise.progress({ @@ -35,12 +61,10 @@ PDFJS.getDocument = function getDocument(source, headers) { headers: headers }, function getPDFLoad(data) { - transport.sendData(data); + transport.sendData(data, parameters); }); - } else { - // assuming the source is array, instantiating directly from it - transport.sendData(source); } + return promise; }; @@ -122,6 +146,11 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { }); return promise; }, + isEncrypted: function PDFDocumentProxy_isEncrypted() { + var promise = new PDFJS.Promise(); + promise.resolve(this.pdfInfo.encrypted); + return promise; + }, destroy: function PDFDocumentProxy_destroy() { this.transport.destroy(); } @@ -467,6 +496,14 @@ var WorkerTransport = (function WorkerTransportClosure() { this.workerReadyPromise.resolve(pdfDocument); }, this); + messageHandler.on('NeedPassword', function transportPassword(data) { + this.workerReadyPromise.reject(data.exception.message, data.exception); + }, this); + + messageHandler.on('IncorrectPassword', function transportBadPass(data) { + this.workerReadyPromise.reject(data.exception.message, data.exception); + }, this); + messageHandler.on('GetPage', function transportPage(data) { var pageInfo = data.pageInfo; var page = new PDFPageProxy(pageInfo, this); @@ -569,8 +606,8 @@ var WorkerTransport = (function WorkerTransportClosure() { }); }, - sendData: function WorkerTransport_sendData(data) { - this.messageHandler.send('GetDocRequest', data); + sendData: function WorkerTransport_sendData(data, params) { + this.messageHandler.send('GetDocRequest', {data: data, params: params}); }, getPage: function WorkerTransport_getPage(pageNumber, promise) { diff --git a/src/core.js b/src/core.js index 99a8dd161..bd8161691 100644 --- a/src/core.js +++ b/src/core.js @@ -320,19 +320,19 @@ var Page = (function PageClosure() { * `PDFDocument` objects on the main thread created. */ var PDFDocument = (function PDFDocumentClosure() { - function PDFDocument(arg, callback) { + function PDFDocument(arg, password) { if (isStream(arg)) - init.call(this, arg); + init.call(this, arg, password); else if (isArrayBuffer(arg)) - init.call(this, new Stream(arg)); + init.call(this, new Stream(arg), password); else error('PDFDocument: Unknown argument type'); } - function init(stream) { + function init(stream, password) { assertWellFormed(stream.length > 0, 'stream must have data'); this.stream = stream; - this.setup(); + this.setup(password); this.acroForm = this.catalog.catDict.get('AcroForm'); } @@ -423,11 +423,12 @@ var PDFDocument = (function PDFDocumentClosure() { } // May not be a PDF file, continue anyway. }, - setup: function PDFDocument_setup(ownerPassword, userPassword) { + setup: function PDFDocument_setup(password) { this.checkHeader(); var xref = new XRef(this.stream, this.startXRef, - this.mainXRefEntriesOffset); + this.mainXRefEntriesOffset, + password); this.xref = xref; this.catalog = new Catalog(xref); }, diff --git a/src/crypto.js b/src/crypto.js index dcd820554..c86551f36 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -556,7 +556,9 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { var encryptionKey = prepareKeyData(fileIdBytes, passwordBytes, ownerPassword, userPassword, flags, revision, keyLength, encryptMetadata); - if (!encryptionKey && password) { + if (!encryptionKey && !password) { + throw new PasswordException('No password given', 'needpassword'); + } else if (!encryptionKey && password) { // Attempting use the password as an owner password var decodedPassword = decodeUserPassword(passwordBytes, ownerPassword, revision, keyLength); @@ -566,7 +568,7 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { } if (!encryptionKey) - error('incorrect password or encryption data'); + throw new PasswordException('Incorrect Password', 'incorrectpassword'); this.encryptionKey = encryptionKey; diff --git a/src/obj.js b/src/obj.js index 9b99eb8f7..3432ac68d 100644 --- a/src/obj.js +++ b/src/obj.js @@ -298,7 +298,7 @@ var Catalog = (function CatalogClosure() { })(); var XRef = (function XRefClosure() { - function XRef(stream, startXRef, mainXRefEntriesOffset) { + function XRef(stream, startXRef, mainXRefEntriesOffset, password) { this.stream = stream; this.entries = []; this.xrefstms = {}; @@ -311,8 +311,7 @@ var XRef = (function XRefClosure() { var encrypt = trailerDict.get('Encrypt'); if (encrypt) { var fileId = trailerDict.get('ID'); - this.encrypt = new CipherTransformFactory(encrypt, - fileId[0] /*, password */); + this.encrypt = new CipherTransformFactory(encrypt, fileId[0], password); } // get the root dictionary (catalog) object diff --git a/src/util.js b/src/util.js index 140b18cf1..ef40f524b 100644 --- a/src/util.js +++ b/src/util.js @@ -58,6 +58,14 @@ function shadow(obj, prop, value) { return value; } +function PasswordException(msg, code) { + this.name = 'PasswordException'; + this.message = msg; + this.code = code; +} +PasswordException.prototype = new Error(); +PasswordException.constructor = PasswordException; + function bytesToString(bytes) { var str = ''; var length = bytes.length; @@ -456,7 +464,7 @@ var Promise = PDFJS.Promise = (function PromiseClosure() { } this.isResolved = true; - this.data = data || null; + this.data = (typeof data !== 'undefined') ? data : null; var callbacks = this.callbacks; for (var i = 0, ii = callbacks.length; i < ii; i++) { @@ -471,7 +479,7 @@ var Promise = PDFJS.Promise = (function PromiseClosure() { } }, - reject: function Promise_reject(reason) { + reject: function Promise_reject(reason, exception) { if (this.isRejected) { error('A Promise can be rejected only once ' + this.name); } @@ -484,7 +492,7 @@ var Promise = PDFJS.Promise = (function PromiseClosure() { var errbacks = this.errbacks; for (var i = 0, ii = errbacks.length; i < ii; i++) { - errbacks[i].call(null, reason); + errbacks[i].call(null, reason, exception); } }, diff --git a/src/worker.js b/src/worker.js index 25f3f52cd..84a8298a7 100644 --- a/src/worker.js +++ b/src/worker.js @@ -88,14 +88,35 @@ var WorkerMessageHandler = { handler.on('GetDocRequest', function wphSetupDoc(data) { // Create only the model of the PDFDoc, which is enough for // processing the content of the pdf. - pdfModel = new PDFDocument(new Stream(data)); + var pdfData = data.data; + var pdfPassword = data.params.password; + try { + pdfModel = new PDFDocument(new Stream(pdfData), pdfPassword); + } catch (e) { + if (e instanceof PasswordException) { + if (e.code === 'needpassword') { + handler.send('NeedPassword', { + exception: e + }); + } else if (e.code === 'incorrectpassword') { + handler.send('IncorrectPassword', { + exception: e + }); + } + + return; + } else { + throw e; + } + } var doc = { numPages: pdfModel.numPages, fingerprint: pdfModel.getFingerprint(), destinations: pdfModel.catalog.destinations, outline: pdfModel.catalog.documentOutline, info: pdfModel.getDocumentInfo(), - metadata: pdfModel.catalog.metadata + metadata: pdfModel.catalog.metadata, + encrypted: !!pdfModel.xref.encrypt }; handler.send('GetDoc', {pdfInfo: doc}); }); diff --git a/web/viewer.js b/web/viewer.js index dcbfcf14e..d4897b956 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -331,10 +331,15 @@ var PDFView = { return currentPageNumber; }, - open: function pdfViewOpen(url, scale) { - this.url = url; - - document.title = decodeURIComponent(getFileName(url)) || url; + open: function pdfViewOpen(url, scale, password) { + var parameters = {password: password}; + if (typeof url === 'string') { + this.url = url; + document.title = decodeURIComponent(getFileName(url)) || url; + parameters.url = url; + } else if (isArrayBuffer(url)) { + parameters.data = url; + } if (!PDFView.loadingBar) { PDFView.loadingBar = new ProgressBar('#loadingBar', {}); @@ -342,12 +347,23 @@ var PDFView = { var self = this; self.loading = true; - PDFJS.getDocument(url).then( + PDFJS.getDocument(parameters).then( function getDocumentCallback(pdfDocument) { self.load(pdfDocument, scale); self.loading = false; }, function getDocumentError(message, exception) { + if (exception.name === 'PasswordException') { + if (exception.code === 'needpassword') { + var promptString = mozL10n.get('request_password', null, + 'PDF is protected by a password:'); + password = prompt(promptString); + if (password && password.length > 0) { + return PDFView.open(url, scale, password); + } + } + } + var loadingIndicator = document.getElementById('loading'); loadingIndicator.textContent = mozL10n.get('loading_error_indicator', null, 'Error'); @@ -1530,10 +1546,7 @@ window.addEventListener('change', function webViewerChange(evt) { for (var i = 0; i < data.length; i++) uint8Array[i] = data.charCodeAt(i); - // TODO using blob instead? - PDFJS.getDocument(uint8Array).then(function(pdfDocument) { - PDFView.load(pdfDocument); - }); + PDFView.open(uint8Array, 0); }; // Read as a binary string since "readAsArrayBuffer" is not yet From 45d6daa8535c12a21a42734435351583f17bfea5 Mon Sep 17 00:00:00 2001 From: Jakob Miland Date: Mon, 14 May 2012 20:58:56 +0200 Subject: [PATCH 02/15] Wrong variable name, fix --- src/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.js b/src/api.js index cd10ee366..3ac4da565 100644 --- a/src/api.js +++ b/src/api.js @@ -20,7 +20,7 @@ PDFJS.getDocument = function getDocument(source) { var url, data, headers, password, parameters = {}; if (typeof source === 'string') { - url = params; + url = source; } else if (isArrayBuffer(source)) { data = source; } else if (typeof source === 'object') { From 326b739bd18c80d3959d817c5547c1fee1ee9557 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Tue, 15 May 2012 14:47:33 -0500 Subject: [PATCH 03/15] Adds styles for small screens --- web/viewer.css | 44 ++++++++++++++++++++++++++++++++++++++++++++ web/viewer.html | 4 ++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/web/viewer.css b/web/viewer.css index 7b0ce0344..00f8c0ea7 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -927,3 +927,47 @@ canvas { #PDFBug table { font-size: 10px; } + +@media all and (max-width: 770px) { + #scaleSelectContainer, #fileInput, #pageNumberLabel { + display: none; + } + + #sidebarContainer { + top: 33px; + z-index: 100; + } + #sidebarContent { + top: 33px; + background-color: hsla(0,0%,0%,.7); + } + #thumbnailView, #outlineView { + top: 66px; + } + + html[dir='ltr'] #outerContainer.sidebarOpen > #mainContainer { + left: 0px; + } + html[dir='rtl'] #outerContainer.sidebarOpen > #mainContainer { + right: 0px; + } + + #pageNumber { + width: 30px; + } +} + +@media all and (max-width: 500px) { + #viewBookmark { + display: none; + } + + html[dir='ltr'] .outerCenter { + float: left; + left: 180px; + } + html[dir='rtl'] .outerCenter { + float: right; + right: 180px; + } +} diff --git a/web/viewer.html b/web/viewer.html index 0ec05e031..45fb9f040 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -81,7 +81,7 @@ Next - + @@ -119,7 +119,7 @@ Zoom In - +