browser/extensions/pdfjs/content/PdfStreamConverter.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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 */
    21 'use strict';
    23 var EXPORTED_SYMBOLS = ['PdfStreamConverter'];
    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;
    38 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    39 Cu.import('resource://gre/modules/Services.jsm');
    40 Cu.import('resource://gre/modules/NetUtil.jsm');
    42 XPCOMUtils.defineLazyModuleGetter(this, 'NetworkManager',
    43   'resource://pdf.js/network.js');
    45 XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
    46   'resource://gre/modules/PrivateBrowsingUtils.jsm');
    48 XPCOMUtils.defineLazyModuleGetter(this, 'PdfJsTelemetry',
    49   'resource://pdf.js/PdfJsTelemetry.jsm');
    51 var Svc = {};
    52 XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
    53                                    '@mozilla.org/mime;1',
    54                                    'nsIMIMEService');
    56 function getContainingBrowser(domWindow) {
    57   return domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
    58                   .getInterface(Ci.nsIWebNavigation)
    59                   .QueryInterface(Ci.nsIDocShell)
    60                   .chromeEventHandler;
    61 }
    63 function getChromeWindow(domWindow) {
    64   return getContainingBrowser(domWindow).ownerDocument.defaultView;
    65 }
    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 }
    80 function setBoolPref(pref, value) {
    81   Services.prefs.setBoolPref(pref, value);
    82 }
    84 function getBoolPref(pref, def) {
    85   try {
    86     return Services.prefs.getBoolPref(pref);
    87   } catch (ex) {
    88     return def;
    89   }
    90 }
    92 function setIntPref(pref, value) {
    93   Services.prefs.setIntPref(pref, value);
    94 }
    96 function getIntPref(pref, def) {
    97   try {
    98     return Services.prefs.getIntPref(pref);
    99   } catch (ex) {
   100     return def;
   101   }
   102 }
   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 }
   111 function getStringPref(pref, def) {
   112   try {
   113     return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
   114   } catch (ex) {
   115     return def;
   116   }
   117 }
   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 }
   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 }
   135 function getLocalizedStrings(path) {
   136   var stringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
   137       getService(Ci.nsIStringBundleService).
   138       createBundle('chrome://pdf.js/locale/' + path);
   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 }
   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 }
   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 };
   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 }
   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;
   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       }
   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       };
   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');
   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;
   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     }
   400     PdfJsTelemetry.onFallback();
   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     }
   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 };
   529 var RangedChromeActions = (function RangedChromeActionsClosure() {
   530   /**
   531    * This is for range requests
   532    */
   533   function RangedChromeActions(
   534               domWindow, contentDispositionFilename, originalRequest,
   535               dataListener) {
   537     ChromeActions.call(this, domWindow, contentDispositionFilename);
   538     this.dataListener = dataListener;
   539     this.originalRequest = originalRequest;
   541     this.pdfUrl = originalRequest.URI.spec;
   542     this.contentLength = originalRequest.contentLength;
   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);
   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     };
   579     this.networkManager = new NetworkManager(this.pdfUrl, {
   580       httpHeaders: httpHeaderVisitor.headers,
   581       getXhr: getXhr
   582     });
   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   }
   592   RangedChromeActions.prototype = Object.create(ChromeActions.prototype);
   593   var proto = RangedChromeActions.prototype;
   594   proto.constructor = RangedChromeActions;
   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;
   607     return true;
   608   };
   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   };
   634   return RangedChromeActions;
   635 })();
   637 var StandardChromeActions = (function StandardChromeActionsClosure() {
   639   /**
   640    * This is for a single network stream
   641    */
   642   function StandardChromeActions(domWindow, contentDispositionFilename,
   643                                  dataListener) {
   645     ChromeActions.call(this, domWindow, contentDispositionFilename);
   646     this.dataListener = dataListener;
   647   }
   649   StandardChromeActions.prototype = Object.create(ChromeActions.prototype);
   650   var proto = StandardChromeActions.prototype;
   651   proto.constructor = StandardChromeActions;
   653   proto.initPassiveLoading =
   654       function StandardChromeActions_initPassiveLoading() {
   656     if (!this.dataListener) {
   657       return false;
   658     }
   660     var self = this;
   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     };
   671     this.dataListener.oncomplete = function ChromeActions_dataListenerComplete(
   672                                       data, errorCode) {
   673       self.domWindow.postMessage({
   674         pdfjsLoadAction: 'complete',
   675         data: data,
   676         errorCode: errorCode
   677       }, '*');
   679       delete self.dataListener;
   680     };
   682     return true;
   683   };
   685   return StandardChromeActions;
   686 })();
   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 };
   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 }
   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);
   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 };
   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 };
   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 };
   784 function PdfStreamConverter() {
   785 }
   787 PdfStreamConverter.prototype = {
   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=*/*',
   794   QueryInterface: XPCOMUtils.generateQI([
   795       Ci.nsISupports,
   796       Ci.nsIStreamConverter,
   797       Ci.nsIStreamListener,
   798       Ci.nsIRequestObserver
   799   ]),
   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    */
   817   // nsIStreamConverter::convert
   818   convert: function(aFromStream, aFromType, aToType, aCtxt) {
   819     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   820   },
   822   // nsIStreamConverter::asyncConvertData
   823   asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
   824     // Store the listener passed to us
   825     this.listener = aListener;
   826   },
   828   // nsIStreamListener::onDataAvailable
   829   onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
   830     if (!this.dataListener) {
   831       return;
   832     }
   834     var binaryStream = this.binaryStream;
   835     binaryStream.setInputStream(aInputStream);
   836     var chunk = binaryStream.readByteArray(aCount);
   837     this.dataListener.append(chunk);
   838   },
   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) {}
   849     var rangeRequest = false;
   850     if (isHttpRequest) {
   851       var contentEncoding = 'identity';
   852       try {
   853         contentEncoding = aRequest.getResponseHeader('Content-Encoding');
   854       } catch (e) {}
   856       var acceptRanges;
   857       try {
   858         acceptRanges = aRequest.getResponseHeader('Accept-Ranges');
   859       } catch (e) {}
   861       var hash = aRequest.URI.ref;
   862       rangeRequest = contentEncoding === 'identity' &&
   863                      acceptRanges === 'bytes' &&
   864                      aRequest.contentLength >= 0 &&
   865                      hash.indexOf('disableRange=true') < 0;
   866     }
   868     aRequest.QueryInterface(Ci.nsIChannel);
   870     aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
   872     var contentDispositionFilename;
   873     try {
   874       contentDispositionFilename = aRequest.contentDispositionFilename;
   875     } catch (e) {}
   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     }
   890     PdfJsTelemetry.onViewerIsUsed();
   891     PdfJsTelemetry.onDocumentSize(aRequest.contentLength);
   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);
   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);
   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     };
   946     // Keep the URL the same so the browser sees it as the same.
   947     channel.originalURI = aRequest.URI;
   948     channel.loadGroup = aRequest.loadGroup;
   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   },
   964   // nsIRequestObserver::onStopRequest
   965   onStopRequest: function(aRequest, aContext, aStatusCode) {
   966     if (!this.dataListener) {
   967       // Do nothing
   968       return;
   969     }
   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