|
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 }; |