browser/extensions/pdfjs/content/PdfStreamConverter.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:a7e99983b34d
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
3 /* Copyright 2012 Mozilla Foundation
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 /* jshint esnext:true */
18 /* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils,
19 dump, NetworkManager, PdfJsTelemetry */
20
21 'use strict';
22
23 var EXPORTED_SYMBOLS = ['PdfStreamConverter'];
24
25 const Cc = Components.classes;
26 const Ci = Components.interfaces;
27 const Cr = Components.results;
28 const Cu = Components.utils;
29 // True only if this is the version of pdf.js that is included with firefox.
30 const MOZ_CENTRAL = JSON.parse('true');
31 const PDFJS_EVENT_ID = 'pdf.js.message';
32 const PDF_CONTENT_TYPE = 'application/pdf';
33 const PREF_PREFIX = 'pdfjs';
34 const PDF_VIEWER_WEB_PAGE = 'resource://pdf.js/web/viewer.html';
35 const MAX_NUMBER_OF_PREFS = 50;
36 const MAX_STRING_PREF_LENGTH = 128;
37
38 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
39 Cu.import('resource://gre/modules/Services.jsm');
40 Cu.import('resource://gre/modules/NetUtil.jsm');
41
42 XPCOMUtils.defineLazyModuleGetter(this, 'NetworkManager',
43 'resource://pdf.js/network.js');
44
45 XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
46 'resource://gre/modules/PrivateBrowsingUtils.jsm');
47
48 XPCOMUtils.defineLazyModuleGetter(this, 'PdfJsTelemetry',
49 'resource://pdf.js/PdfJsTelemetry.jsm');
50
51 var Svc = {};
52 XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
53 '@mozilla.org/mime;1',
54 'nsIMIMEService');
55
56 function getContainingBrowser(domWindow) {
57 return domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
58 .getInterface(Ci.nsIWebNavigation)
59 .QueryInterface(Ci.nsIDocShell)
60 .chromeEventHandler;
61 }
62
63 function getChromeWindow(domWindow) {
64 return getContainingBrowser(domWindow).ownerDocument.defaultView;
65 }
66
67 function getFindBar(domWindow) {
68 var browser = getContainingBrowser(domWindow);
69 try {
70 var tabbrowser = browser.getTabBrowser();
71 var tab = tabbrowser._getTabForBrowser(browser);
72 return tabbrowser.getFindBar(tab);
73 } catch (e) {
74 // FF22 has no _getTabForBrowser, and FF24 has no getFindBar
75 var chromeWindow = browser.ownerDocument.defaultView;
76 return chromeWindow.gFindBar;
77 }
78 }
79
80 function setBoolPref(pref, value) {
81 Services.prefs.setBoolPref(pref, value);
82 }
83
84 function getBoolPref(pref, def) {
85 try {
86 return Services.prefs.getBoolPref(pref);
87 } catch (ex) {
88 return def;
89 }
90 }
91
92 function setIntPref(pref, value) {
93 Services.prefs.setIntPref(pref, value);
94 }
95
96 function getIntPref(pref, def) {
97 try {
98 return Services.prefs.getIntPref(pref);
99 } catch (ex) {
100 return def;
101 }
102 }
103
104 function setStringPref(pref, value) {
105 var str = Cc['@mozilla.org/supports-string;1']
106 .createInstance(Ci.nsISupportsString);
107 str.data = value;
108 Services.prefs.setComplexValue(pref, Ci.nsISupportsString, str);
109 }
110
111 function getStringPref(pref, def) {
112 try {
113 return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
114 } catch (ex) {
115 return def;
116 }
117 }
118
119 function log(aMsg) {
120 if (!getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false))
121 return;
122 var msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
123 Services.console.logStringMessage(msg);
124 dump(msg + '\n');
125 }
126
127 function getDOMWindow(aChannel) {
128 var requestor = aChannel.notificationCallbacks ?
129 aChannel.notificationCallbacks :
130 aChannel.loadGroup.notificationCallbacks;
131 var win = requestor.getInterface(Components.interfaces.nsIDOMWindow);
132 return win;
133 }
134
135 function getLocalizedStrings(path) {
136 var stringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
137 getService(Ci.nsIStringBundleService).
138 createBundle('chrome://pdf.js/locale/' + path);
139
140 var map = {};
141 var enumerator = stringBundle.getSimpleEnumeration();
142 while (enumerator.hasMoreElements()) {
143 var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
144 var key = string.key, property = 'textContent';
145 var i = key.lastIndexOf('.');
146 if (i >= 0) {
147 property = key.substring(i + 1);
148 key = key.substring(0, i);
149 }
150 if (!(key in map))
151 map[key] = {};
152 map[key][property] = string.value;
153 }
154 return map;
155 }
156 function getLocalizedString(strings, id, property) {
157 property = property || 'textContent';
158 if (id in strings)
159 return strings[id][property];
160 return id;
161 }
162
163 // PDF data storage
164 function PdfDataListener(length) {
165 this.length = length; // less than 0, if length is unknown
166 this.data = new Uint8Array(length >= 0 ? length : 0x10000);
167 this.loaded = 0;
168 }
169
170 PdfDataListener.prototype = {
171 append: function PdfDataListener_append(chunk) {
172 var willBeLoaded = this.loaded + chunk.length;
173 if (this.length >= 0 && this.length < willBeLoaded) {
174 this.length = -1; // reset the length, server is giving incorrect one
175 }
176 if (this.length < 0 && this.data.length < willBeLoaded) {
177 // data length is unknown and new chunk will not fit in the existing
178 // buffer, resizing the buffer by doubling the its last length
179 var newLength = this.data.length;
180 for (; newLength < willBeLoaded; newLength *= 2) {}
181 var newData = new Uint8Array(newLength);
182 newData.set(this.data);
183 this.data = newData;
184 }
185 this.data.set(chunk, this.loaded);
186 this.loaded = willBeLoaded;
187 this.onprogress(this.loaded, this.length >= 0 ? this.length : void(0));
188 },
189 getData: function PdfDataListener_getData() {
190 var data = this.data;
191 if (this.loaded != data.length)
192 data = data.subarray(0, this.loaded);
193 delete this.data; // releasing temporary storage
194 return data;
195 },
196 finish: function PdfDataListener_finish() {
197 this.isDataReady = true;
198 if (this.oncompleteCallback) {
199 this.oncompleteCallback(this.getData());
200 }
201 },
202 error: function PdfDataListener_error(errorCode) {
203 this.errorCode = errorCode;
204 if (this.oncompleteCallback) {
205 this.oncompleteCallback(null, errorCode);
206 }
207 },
208 onprogress: function() {},
209 get oncomplete() {
210 return this.oncompleteCallback;
211 },
212 set oncomplete(value) {
213 this.oncompleteCallback = value;
214 if (this.isDataReady) {
215 value(this.getData());
216 }
217 if (this.errorCode) {
218 value(null, this.errorCode);
219 }
220 }
221 };
222
223 // All the priviledged actions.
224 function ChromeActions(domWindow, contentDispositionFilename) {
225 this.domWindow = domWindow;
226 this.contentDispositionFilename = contentDispositionFilename;
227 this.telemetryState = {
228 documentInfo: false,
229 firstPageInfo: false,
230 streamTypesUsed: [],
231 startAt: Date.now()
232 };
233 }
234
235 ChromeActions.prototype = {
236 isInPrivateBrowsing: function() {
237 return PrivateBrowsingUtils.isWindowPrivate(this.domWindow);
238 },
239 download: function(data, sendResponse) {
240 var self = this;
241 var originalUrl = data.originalUrl;
242 // The data may not be downloaded so we need just retry getting the pdf with
243 // the original url.
244 var originalUri = NetUtil.newURI(data.originalUrl);
245 var filename = data.filename;
246 if (typeof filename !== 'string' ||
247 (!/\.pdf$/i.test(filename) && !data.isAttachment)) {
248 filename = 'document.pdf';
249 }
250 var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri;
251 var extHelperAppSvc =
252 Cc['@mozilla.org/uriloader/external-helper-app-service;1'].
253 getService(Ci.nsIExternalHelperAppService);
254 var frontWindow = Cc['@mozilla.org/embedcomp/window-watcher;1'].
255 getService(Ci.nsIWindowWatcher).activeWindow;
256
257 var docIsPrivate = this.isInPrivateBrowsing();
258 var netChannel = NetUtil.newChannel(blobUri);
259 if ('nsIPrivateBrowsingChannel' in Ci &&
260 netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
261 netChannel.setPrivate(docIsPrivate);
262 }
263 NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) {
264 if (!Components.isSuccessCode(aResult)) {
265 if (sendResponse)
266 sendResponse(true);
267 return;
268 }
269 // Create a nsIInputStreamChannel so we can set the url on the channel
270 // so the filename will be correct.
271 var channel = Cc['@mozilla.org/network/input-stream-channel;1'].
272 createInstance(Ci.nsIInputStreamChannel);
273 channel.QueryInterface(Ci.nsIChannel);
274 try {
275 // contentDisposition/contentDispositionFilename is readonly before FF18
276 channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
277 if (self.contentDispositionFilename) {
278 channel.contentDispositionFilename = self.contentDispositionFilename;
279 } else {
280 channel.contentDispositionFilename = filename;
281 }
282 } catch (e) {}
283 channel.setURI(originalUri);
284 channel.contentStream = aInputStream;
285 if ('nsIPrivateBrowsingChannel' in Ci &&
286 channel instanceof Ci.nsIPrivateBrowsingChannel) {
287 channel.setPrivate(docIsPrivate);
288 }
289
290 var listener = {
291 extListener: null,
292 onStartRequest: function(aRequest, aContext) {
293 this.extListener = extHelperAppSvc.doContent((data.isAttachment ? '' :
294 'application/pdf'),
295 aRequest, frontWindow, false);
296 this.extListener.onStartRequest(aRequest, aContext);
297 },
298 onStopRequest: function(aRequest, aContext, aStatusCode) {
299 if (this.extListener)
300 this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
301 // Notify the content code we're done downloading.
302 if (sendResponse)
303 sendResponse(false);
304 },
305 onDataAvailable: function(aRequest, aContext, aInputStream, aOffset,
306 aCount) {
307 this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
308 aOffset, aCount);
309 }
310 };
311
312 channel.asyncOpen(listener, null);
313 });
314 },
315 getLocale: function() {
316 return getStringPref('general.useragent.locale', 'en-US');
317 },
318 getStrings: function(data) {
319 try {
320 // Lazy initialization of localizedStrings
321 if (!('localizedStrings' in this))
322 this.localizedStrings = getLocalizedStrings('viewer.properties');
323
324 var result = this.localizedStrings[data];
325 return JSON.stringify(result || null);
326 } catch (e) {
327 log('Unable to retrive localized strings: ' + e);
328 return 'null';
329 }
330 },
331 pdfBugEnabled: function() {
332 return getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false);
333 },
334 supportsIntegratedFind: function() {
335 // Integrated find is only supported when we're not in a frame
336 if (this.domWindow.frameElement !== null) {
337 return false;
338 }
339 // ... and when the new find events code exists.
340 var findBar = getFindBar(this.domWindow);
341 return findBar && ('updateControlState' in findBar);
342 },
343 supportsDocumentFonts: function() {
344 var prefBrowser = getIntPref('browser.display.use_document_fonts', 1);
345 var prefGfx = getBoolPref('gfx.downloadable_fonts.enabled', true);
346 return (!!prefBrowser && prefGfx);
347 },
348 supportsDocumentColors: function() {
349 return getBoolPref('browser.display.use_document_colors', true);
350 },
351 reportTelemetry: function (data) {
352 var probeInfo = JSON.parse(data);
353 switch (probeInfo.type) {
354 case 'documentInfo':
355 if (!this.telemetryState.documentInfo) {
356 PdfJsTelemetry.onDocumentVersion(probeInfo.version | 0);
357 PdfJsTelemetry.onDocumentGenerator(probeInfo.generator | 0);
358 if (probeInfo.formType) {
359 PdfJsTelemetry.onForm(probeInfo.formType === 'acroform');
360 }
361 this.telemetryState.documentInfo = true;
362 }
363 break;
364 case 'pageInfo':
365 if (!this.telemetryState.firstPageInfo) {
366 var duration = Date.now() - this.telemetryState.startAt;
367 PdfJsTelemetry.onTimeToView(duration);
368 this.telemetryState.firstPageInfo = true;
369 }
370 break;
371 case 'streamInfo':
372 if (!Array.isArray(probeInfo.streamTypes)) {
373 break;
374 }
375 for (var i = 0; i < probeInfo.streamTypes.length; i++) {
376 var streamTypeId = probeInfo.streamTypes[i] | 0;
377 if (streamTypeId >= 0 && streamTypeId < 10 &&
378 !this.telemetryState.streamTypesUsed[streamTypeId]) {
379 PdfJsTelemetry.onStreamType(streamTypeId);
380 this.telemetryState.streamTypesUsed[streamTypeId] = true;
381 }
382 }
383 break;
384 }
385 },
386 fallback: function(args, sendResponse) {
387 var featureId = args.featureId;
388 var url = args.url;
389
390 var self = this;
391 var domWindow = this.domWindow;
392 var strings = getLocalizedStrings('chrome.properties');
393 var message;
394 if (featureId === 'forms') {
395 message = getLocalizedString(strings, 'unsupported_feature_forms');
396 } else {
397 message = getLocalizedString(strings, 'unsupported_feature');
398 }
399
400 PdfJsTelemetry.onFallback();
401
402 var notificationBox = null;
403 try {
404 // Based on MDN's "Working with windows in chrome code"
405 var mainWindow = domWindow
406 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
407 .getInterface(Components.interfaces.nsIWebNavigation)
408 .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
409 .rootTreeItem
410 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
411 .getInterface(Components.interfaces.nsIDOMWindow);
412 var browser = mainWindow.gBrowser
413 .getBrowserForDocument(domWindow.top.document);
414 notificationBox = mainWindow.gBrowser.getNotificationBox(browser);
415 } catch (e) {
416 log('Unable to get a notification box for the fallback message');
417 return;
418 }
419
420 // Flag so we don't call the response callback twice, since if the user
421 // clicks open with different viewer both the button callback and
422 // eventCallback will be called.
423 var sentResponse = false;
424 var buttons = [{
425 label: getLocalizedString(strings, 'open_with_different_viewer'),
426 accessKey: getLocalizedString(strings, 'open_with_different_viewer',
427 'accessKey'),
428 callback: function() {
429 sentResponse = true;
430 sendResponse(true);
431 }
432 }];
433 notificationBox.appendNotification(message, 'pdfjs-fallback', null,
434 notificationBox.PRIORITY_INFO_LOW,
435 buttons,
436 function eventsCallback(eventType) {
437 // Currently there is only one event "removed" but if there are any other
438 // added in the future we still only care about removed at the moment.
439 if (eventType !== 'removed')
440 return;
441 // Don't send a response again if we already responded when the button was
442 // clicked.
443 if (!sentResponse)
444 sendResponse(false);
445 });
446 },
447 updateFindControlState: function(data) {
448 if (!this.supportsIntegratedFind())
449 return;
450 // Verify what we're sending to the findbar.
451 var result = data.result;
452 var findPrevious = data.findPrevious;
453 var findPreviousType = typeof findPrevious;
454 if ((typeof result !== 'number' || result < 0 || result > 3) ||
455 (findPreviousType !== 'undefined' && findPreviousType !== 'boolean')) {
456 return;
457 }
458 getFindBar(this.domWindow).updateControlState(result, findPrevious);
459 },
460 setPreferences: function(prefs, sendResponse) {
461 var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.');
462 var numberOfPrefs = 0;
463 var prefValue, prefName;
464 for (var key in prefs) {
465 if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
466 log('setPreferences - Exceeded the maximum number of preferences ' +
467 'that is allowed to be set at once.');
468 break;
469 } else if (!defaultBranch.getPrefType(key)) {
470 continue;
471 }
472 prefValue = prefs[key];
473 prefName = (PREF_PREFIX + '.' + key);
474 switch (typeof prefValue) {
475 case 'boolean':
476 setBoolPref(prefName, prefValue);
477 break;
478 case 'number':
479 setIntPref(prefName, prefValue);
480 break;
481 case 'string':
482 if (prefValue.length > MAX_STRING_PREF_LENGTH) {
483 log('setPreferences - Exceeded the maximum allowed length ' +
484 'for a string preference.');
485 } else {
486 setStringPref(prefName, prefValue);
487 }
488 break;
489 }
490 }
491 if (sendResponse) {
492 sendResponse(true);
493 }
494 },
495 getPreferences: function(prefs, sendResponse) {
496 var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.');
497 var currentPrefs = {}, numberOfPrefs = 0;
498 var prefValue, prefName;
499 for (var key in prefs) {
500 if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
501 log('getPreferences - Exceeded the maximum number of preferences ' +
502 'that is allowed to be fetched at once.');
503 break;
504 } else if (!defaultBranch.getPrefType(key)) {
505 continue;
506 }
507 prefValue = prefs[key];
508 prefName = (PREF_PREFIX + '.' + key);
509 switch (typeof prefValue) {
510 case 'boolean':
511 currentPrefs[key] = getBoolPref(prefName, prefValue);
512 break;
513 case 'number':
514 currentPrefs[key] = getIntPref(prefName, prefValue);
515 break;
516 case 'string':
517 currentPrefs[key] = getStringPref(prefName, prefValue);
518 break;
519 }
520 }
521 if (sendResponse) {
522 sendResponse(JSON.stringify(currentPrefs));
523 } else {
524 return JSON.stringify(currentPrefs);
525 }
526 }
527 };
528
529 var RangedChromeActions = (function RangedChromeActionsClosure() {
530 /**
531 * This is for range requests
532 */
533 function RangedChromeActions(
534 domWindow, contentDispositionFilename, originalRequest,
535 dataListener) {
536
537 ChromeActions.call(this, domWindow, contentDispositionFilename);
538 this.dataListener = dataListener;
539 this.originalRequest = originalRequest;
540
541 this.pdfUrl = originalRequest.URI.spec;
542 this.contentLength = originalRequest.contentLength;
543
544 // Pass all the headers from the original request through
545 var httpHeaderVisitor = {
546 headers: {},
547 visitHeader: function(aHeader, aValue) {
548 if (aHeader === 'Range') {
549 // When loading the PDF from cache, firefox seems to set the Range
550 // request header to fetch only the unfetched portions of the file
551 // (e.g. 'Range: bytes=1024-'). However, we want to set this header
552 // manually to fetch the PDF in chunks.
553 return;
554 }
555 this.headers[aHeader] = aValue;
556 }
557 };
558 originalRequest.visitRequestHeaders(httpHeaderVisitor);
559
560 var self = this;
561 var xhr_onreadystatechange = function xhr_onreadystatechange() {
562 if (this.readyState === 1) { // LOADING
563 var netChannel = this.channel;
564 if ('nsIPrivateBrowsingChannel' in Ci &&
565 netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
566 var docIsPrivate = self.isInPrivateBrowsing();
567 netChannel.setPrivate(docIsPrivate);
568 }
569 }
570 };
571 var getXhr = function getXhr() {
572 const XMLHttpRequest = Components.Constructor(
573 '@mozilla.org/xmlextras/xmlhttprequest;1');
574 var xhr = new XMLHttpRequest();
575 xhr.addEventListener('readystatechange', xhr_onreadystatechange);
576 return xhr;
577 };
578
579 this.networkManager = new NetworkManager(this.pdfUrl, {
580 httpHeaders: httpHeaderVisitor.headers,
581 getXhr: getXhr
582 });
583
584 // If we are in range request mode, this means we manually issued xhr
585 // requests, which we need to abort when we leave the page
586 domWindow.addEventListener('unload', function unload(e) {
587 self.networkManager.abortAllRequests();
588 domWindow.removeEventListener(e.type, unload);
589 });
590 }
591
592 RangedChromeActions.prototype = Object.create(ChromeActions.prototype);
593 var proto = RangedChromeActions.prototype;
594 proto.constructor = RangedChromeActions;
595
596 proto.initPassiveLoading = function RangedChromeActions_initPassiveLoading() {
597 this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
598 this.originalRequest = null;
599 this.domWindow.postMessage({
600 pdfjsLoadAction: 'supportsRangedLoading',
601 pdfUrl: this.pdfUrl,
602 length: this.contentLength,
603 data: this.dataListener.getData()
604 }, '*');
605 this.dataListener = null;
606
607 return true;
608 };
609
610 proto.requestDataRange = function RangedChromeActions_requestDataRange(args) {
611 var begin = args.begin;
612 var end = args.end;
613 var domWindow = this.domWindow;
614 // TODO(mack): Support error handler. We're not currently not handling
615 // errors from chrome code for non-range requests, so this doesn't
616 // seem high-pri
617 this.networkManager.requestRange(begin, end, {
618 onDone: function RangedChromeActions_onDone(args) {
619 domWindow.postMessage({
620 pdfjsLoadAction: 'range',
621 begin: args.begin,
622 chunk: args.chunk
623 }, '*');
624 },
625 onProgress: function RangedChromeActions_onProgress(evt) {
626 domWindow.postMessage({
627 pdfjsLoadAction: 'rangeProgress',
628 loaded: evt.loaded,
629 }, '*');
630 }
631 });
632 };
633
634 return RangedChromeActions;
635 })();
636
637 var StandardChromeActions = (function StandardChromeActionsClosure() {
638
639 /**
640 * This is for a single network stream
641 */
642 function StandardChromeActions(domWindow, contentDispositionFilename,
643 dataListener) {
644
645 ChromeActions.call(this, domWindow, contentDispositionFilename);
646 this.dataListener = dataListener;
647 }
648
649 StandardChromeActions.prototype = Object.create(ChromeActions.prototype);
650 var proto = StandardChromeActions.prototype;
651 proto.constructor = StandardChromeActions;
652
653 proto.initPassiveLoading =
654 function StandardChromeActions_initPassiveLoading() {
655
656 if (!this.dataListener) {
657 return false;
658 }
659
660 var self = this;
661
662 this.dataListener.onprogress = function ChromeActions_dataListenerProgress(
663 loaded, total) {
664 self.domWindow.postMessage({
665 pdfjsLoadAction: 'progress',
666 loaded: loaded,
667 total: total
668 }, '*');
669 };
670
671 this.dataListener.oncomplete = function ChromeActions_dataListenerComplete(
672 data, errorCode) {
673 self.domWindow.postMessage({
674 pdfjsLoadAction: 'complete',
675 data: data,
676 errorCode: errorCode
677 }, '*');
678
679 delete self.dataListener;
680 };
681
682 return true;
683 };
684
685 return StandardChromeActions;
686 })();
687
688 // Event listener to trigger chrome privedged code.
689 function RequestListener(actions) {
690 this.actions = actions;
691 }
692 // Receive an event and synchronously or asynchronously responds.
693 RequestListener.prototype.receive = function(event) {
694 var message = event.target;
695 var doc = message.ownerDocument;
696 var action = event.detail.action;
697 var data = event.detail.data;
698 var sync = event.detail.sync;
699 var actions = this.actions;
700 if (!(action in actions)) {
701 log('Unknown action: ' + action);
702 return;
703 }
704 if (sync) {
705 var response = actions[action].call(this.actions, data);
706 var detail = event.detail;
707 detail.__exposedProps__ = {response: 'r'};
708 detail.response = response;
709 } else {
710 var response;
711 if (!event.detail.callback) {
712 doc.documentElement.removeChild(message);
713 response = null;
714 } else {
715 response = function sendResponse(response) {
716 try {
717 var listener = doc.createEvent('CustomEvent');
718 listener.initCustomEvent('pdf.js.response', true, false,
719 {response: response,
720 __exposedProps__: {response: 'r'}});
721 return message.dispatchEvent(listener);
722 } catch (e) {
723 // doc is no longer accessible because the requestor is already
724 // gone. unloaded content cannot receive the response anyway.
725 return false;
726 }
727 };
728 }
729 actions[action].call(this.actions, data, response);
730 }
731 };
732
733 // Forwards events from the eventElement to the contentWindow only if the
734 // content window matches the currently selected browser window.
735 function FindEventManager(eventElement, contentWindow, chromeWindow) {
736 this.types = ['find',
737 'findagain',
738 'findhighlightallchange',
739 'findcasesensitivitychange'];
740 this.chromeWindow = chromeWindow;
741 this.contentWindow = contentWindow;
742 this.eventElement = eventElement;
743 }
744
745 FindEventManager.prototype.bind = function() {
746 var unload = function(e) {
747 this.unbind();
748 this.contentWindow.removeEventListener(e.type, unload);
749 }.bind(this);
750 this.contentWindow.addEventListener('unload', unload);
751
752 for (var i = 0; i < this.types.length; i++) {
753 var type = this.types[i];
754 this.eventElement.addEventListener(type, this, true);
755 }
756 };
757
758 FindEventManager.prototype.handleEvent = function(e) {
759 var chromeWindow = this.chromeWindow;
760 var contentWindow = this.contentWindow;
761 // Only forward the events if they are for our dom window.
762 if (chromeWindow.gBrowser.selectedBrowser.contentWindow === contentWindow) {
763 var detail = e.detail;
764 detail.__exposedProps__ = {
765 query: 'r',
766 caseSensitive: 'r',
767 highlightAll: 'r',
768 findPrevious: 'r'
769 };
770 var forward = contentWindow.document.createEvent('CustomEvent');
771 forward.initCustomEvent(e.type, true, true, detail);
772 contentWindow.dispatchEvent(forward);
773 e.preventDefault();
774 }
775 };
776
777 FindEventManager.prototype.unbind = function() {
778 for (var i = 0; i < this.types.length; i++) {
779 var type = this.types[i];
780 this.eventElement.removeEventListener(type, this, true);
781 }
782 };
783
784 function PdfStreamConverter() {
785 }
786
787 PdfStreamConverter.prototype = {
788
789 // properties required for XPCOM registration:
790 classID: Components.ID('{d0c5195d-e798-49d4-b1d3-9324328b2291}'),
791 classDescription: 'pdf.js Component',
792 contractID: '@mozilla.org/streamconv;1?from=application/pdf&to=*/*',
793
794 QueryInterface: XPCOMUtils.generateQI([
795 Ci.nsISupports,
796 Ci.nsIStreamConverter,
797 Ci.nsIStreamListener,
798 Ci.nsIRequestObserver
799 ]),
800
801 /*
802 * This component works as such:
803 * 1. asyncConvertData stores the listener
804 * 2. onStartRequest creates a new channel, streams the viewer
805 * 3. If range requests are supported:
806 * 3.1. Leave the request open until the viewer is ready to switch to
807 * range requests.
808 *
809 * If range rquests are not supported:
810 * 3.1. Read the stream as it's loaded in onDataAvailable to send
811 * to the viewer
812 *
813 * The convert function just returns the stream, it's just the synchronous
814 * version of asyncConvertData.
815 */
816
817 // nsIStreamConverter::convert
818 convert: function(aFromStream, aFromType, aToType, aCtxt) {
819 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
820 },
821
822 // nsIStreamConverter::asyncConvertData
823 asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
824 // Store the listener passed to us
825 this.listener = aListener;
826 },
827
828 // nsIStreamListener::onDataAvailable
829 onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
830 if (!this.dataListener) {
831 return;
832 }
833
834 var binaryStream = this.binaryStream;
835 binaryStream.setInputStream(aInputStream);
836 var chunk = binaryStream.readByteArray(aCount);
837 this.dataListener.append(chunk);
838 },
839
840 // nsIRequestObserver::onStartRequest
841 onStartRequest: function(aRequest, aContext) {
842 // Setup the request so we can use it below.
843 var isHttpRequest = false;
844 try {
845 aRequest.QueryInterface(Ci.nsIHttpChannel);
846 isHttpRequest = true;
847 } catch (e) {}
848
849 var rangeRequest = false;
850 if (isHttpRequest) {
851 var contentEncoding = 'identity';
852 try {
853 contentEncoding = aRequest.getResponseHeader('Content-Encoding');
854 } catch (e) {}
855
856 var acceptRanges;
857 try {
858 acceptRanges = aRequest.getResponseHeader('Accept-Ranges');
859 } catch (e) {}
860
861 var hash = aRequest.URI.ref;
862 rangeRequest = contentEncoding === 'identity' &&
863 acceptRanges === 'bytes' &&
864 aRequest.contentLength >= 0 &&
865 hash.indexOf('disableRange=true') < 0;
866 }
867
868 aRequest.QueryInterface(Ci.nsIChannel);
869
870 aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
871
872 var contentDispositionFilename;
873 try {
874 contentDispositionFilename = aRequest.contentDispositionFilename;
875 } catch (e) {}
876
877 // Change the content type so we don't get stuck in a loop.
878 aRequest.setProperty('contentType', aRequest.contentType);
879 aRequest.contentType = 'text/html';
880 if (isHttpRequest) {
881 // We trust PDF viewer, using no CSP
882 aRequest.setResponseHeader('Content-Security-Policy', '', false);
883 aRequest.setResponseHeader('Content-Security-Policy-Report-Only', '',
884 false);
885 aRequest.setResponseHeader('X-Content-Security-Policy', '', false);
886 aRequest.setResponseHeader('X-Content-Security-Policy-Report-Only', '',
887 false);
888 }
889
890 PdfJsTelemetry.onViewerIsUsed();
891 PdfJsTelemetry.onDocumentSize(aRequest.contentLength);
892
893
894 // Creating storage for PDF data
895 var contentLength = aRequest.contentLength;
896 this.dataListener = new PdfDataListener(contentLength);
897 this.binaryStream = Cc['@mozilla.org/binaryinputstream;1']
898 .createInstance(Ci.nsIBinaryInputStream);
899
900 // Create a new channel that is viewer loaded as a resource.
901 var ioService = Services.io;
902 var channel = ioService.newChannel(
903 PDF_VIEWER_WEB_PAGE, null, null);
904
905 var listener = this.listener;
906 var dataListener = this.dataListener;
907 // Proxy all the request observer calls, when it gets to onStopRequest
908 // we can get the dom window. We also intentionally pass on the original
909 // request(aRequest) below so we don't overwrite the original channel and
910 // trigger an assertion.
911 var proxy = {
912 onStartRequest: function(request, context) {
913 listener.onStartRequest(aRequest, context);
914 },
915 onDataAvailable: function(request, context, inputStream, offset, count) {
916 listener.onDataAvailable(aRequest, context, inputStream, offset, count);
917 },
918 onStopRequest: function(request, context, statusCode) {
919 // We get the DOM window here instead of before the request since it
920 // may have changed during a redirect.
921 var domWindow = getDOMWindow(channel);
922 var actions;
923 if (rangeRequest) {
924 actions = new RangedChromeActions(
925 domWindow, contentDispositionFilename, aRequest, dataListener);
926 } else {
927 actions = new StandardChromeActions(
928 domWindow, contentDispositionFilename, dataListener);
929 }
930 var requestListener = new RequestListener(actions);
931 domWindow.addEventListener(PDFJS_EVENT_ID, function(event) {
932 requestListener.receive(event);
933 }, false, true);
934 if (actions.supportsIntegratedFind()) {
935 var chromeWindow = getChromeWindow(domWindow);
936 var findBar = getFindBar(domWindow);
937 var findEventManager = new FindEventManager(findBar,
938 domWindow,
939 chromeWindow);
940 findEventManager.bind();
941 }
942 listener.onStopRequest(aRequest, context, statusCode);
943 }
944 };
945
946 // Keep the URL the same so the browser sees it as the same.
947 channel.originalURI = aRequest.URI;
948 channel.loadGroup = aRequest.loadGroup;
949
950 // We can use resource principal when data is fetched by the chrome
951 // e.g. useful for NoScript
952 var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1']
953 .getService(Ci.nsIScriptSecurityManager);
954 var uri = ioService.newURI(PDF_VIEWER_WEB_PAGE, null, null);
955 // FF16 and below had getCodebasePrincipal, it was replaced by
956 // getNoAppCodebasePrincipal (bug 758258).
957 var resourcePrincipal = 'getNoAppCodebasePrincipal' in securityManager ?
958 securityManager.getNoAppCodebasePrincipal(uri) :
959 securityManager.getCodebasePrincipal(uri);
960 aRequest.owner = resourcePrincipal;
961 channel.asyncOpen(proxy, aContext);
962 },
963
964 // nsIRequestObserver::onStopRequest
965 onStopRequest: function(aRequest, aContext, aStatusCode) {
966 if (!this.dataListener) {
967 // Do nothing
968 return;
969 }
970
971 if (Components.isSuccessCode(aStatusCode))
972 this.dataListener.finish();
973 else
974 this.dataListener.error(aStatusCode);
975 delete this.dataListener;
976 delete this.binaryStream;
977 }
978 };

mercurial