1/* Copyright 2012 Mozilla Foundation 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16"use strict"; 17 18var EXPORTED_SYMBOLS = ["PdfStreamConverter"]; 19 20const PDFJS_EVENT_ID = "pdf.js.message"; 21const PREF_PREFIX = "pdfjs"; 22const PDF_VIEWER_ORIGIN = "resource://pdf.js"; 23const PDF_VIEWER_WEB_PAGE = "resource://pdf.js/web/viewer.html"; 24const MAX_NUMBER_OF_PREFS = 50; 25const MAX_STRING_PREF_LENGTH = 128; 26const PDF_CONTENT_TYPE = "application/pdf"; 27 28const { XPCOMUtils } = ChromeUtils.import( 29 "resource://gre/modules/XPCOMUtils.jsm" 30); 31const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 32const { AppConstants } = ChromeUtils.import( 33 "resource://gre/modules/AppConstants.jsm" 34); 35 36ChromeUtils.defineModuleGetter( 37 this, 38 "AsyncPrefs", 39 "resource://gre/modules/AsyncPrefs.jsm" 40); 41ChromeUtils.defineModuleGetter( 42 this, 43 "NetUtil", 44 "resource://gre/modules/NetUtil.jsm" 45); 46 47ChromeUtils.defineModuleGetter( 48 this, 49 "NetworkManager", 50 "resource://pdf.js/PdfJsNetwork.jsm" 51); 52 53ChromeUtils.defineModuleGetter( 54 this, 55 "PrivateBrowsingUtils", 56 "resource://gre/modules/PrivateBrowsingUtils.jsm" 57); 58 59ChromeUtils.defineModuleGetter( 60 this, 61 "PdfJsTelemetry", 62 "resource://pdf.js/PdfJsTelemetry.jsm" 63); 64 65ChromeUtils.defineModuleGetter(this, "PdfJs", "resource://pdf.js/PdfJs.jsm"); 66 67ChromeUtils.defineModuleGetter( 68 this, 69 "PdfSandbox", 70 "resource://pdf.js/PdfSandbox.jsm" 71); 72 73XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]); 74 75var Svc = {}; 76XPCOMUtils.defineLazyServiceGetter( 77 Svc, 78 "mime", 79 "@mozilla.org/mime;1", 80 "nsIMIMEService" 81); 82XPCOMUtils.defineLazyServiceGetter( 83 Svc, 84 "handlers", 85 "@mozilla.org/uriloader/handler-service;1", 86 "nsIHandlerService" 87); 88 89XPCOMUtils.defineLazyGetter(this, "gOurBinary", () => { 90 let file = Services.dirsvc.get("XREExeF", Ci.nsIFile); 91 // Make sure to get the .app on macOS 92 if (AppConstants.platform == "macosx") { 93 while (file) { 94 if (/\.app\/?$/i.test(file.leafName)) { 95 break; 96 } 97 file = file.parent; 98 } 99 } 100 return file; 101}); 102 103function getBoolPref(pref, def) { 104 try { 105 return Services.prefs.getBoolPref(pref); 106 } catch (ex) { 107 return def; 108 } 109} 110 111function getIntPref(pref, def) { 112 try { 113 return Services.prefs.getIntPref(pref); 114 } catch (ex) { 115 return def; 116 } 117} 118 119function getStringPref(pref, def) { 120 try { 121 return Services.prefs.getStringPref(pref); 122 } catch (ex) { 123 return def; 124 } 125} 126 127function log(aMsg) { 128 if (!getBoolPref(PREF_PREFIX + ".pdfBugEnabled", false)) { 129 return; 130 } 131 var msg = "PdfStreamConverter.js: " + (aMsg.join ? aMsg.join("") : aMsg); 132 Services.console.logStringMessage(msg); 133 dump(msg + "\n"); 134} 135 136function getDOMWindow(aChannel, aPrincipal) { 137 var requestor = aChannel.notificationCallbacks 138 ? aChannel.notificationCallbacks 139 : aChannel.loadGroup.notificationCallbacks; 140 var win = requestor.getInterface(Ci.nsIDOMWindow); 141 // Ensure the window wasn't navigated to something that is not PDF.js. 142 if (!win.document.nodePrincipal.equals(aPrincipal)) { 143 return null; 144 } 145 return win; 146} 147 148function getActor(window) { 149 try { 150 return window.windowGlobalChild.getActor("Pdfjs"); 151 } catch (ex) { 152 return null; 153 } 154} 155 156function getLocalizedStrings(path) { 157 var stringBundle = Services.strings.createBundle( 158 "chrome://pdf.js/locale/" + path 159 ); 160 161 var map = {}; 162 for (let string of stringBundle.getSimpleEnumeration()) { 163 var key = string.key, 164 property = "textContent"; 165 var i = key.lastIndexOf("."); 166 if (i >= 0) { 167 property = key.substring(i + 1); 168 key = key.substring(0, i); 169 } 170 if (!(key in map)) { 171 map[key] = {}; 172 } 173 map[key][property] = string.value; 174 } 175 return map; 176} 177 178function isValidMatchesCount(data) { 179 if (typeof data !== "object" || data === null) { 180 return false; 181 } 182 const { current, total } = data; 183 if ( 184 typeof total !== "number" || 185 total < 0 || 186 typeof current !== "number" || 187 current < 0 || 188 current > total 189 ) { 190 return false; 191 } 192 return true; 193} 194 195// PDF data storage 196function PdfDataListener(length) { 197 this.length = length; // less than 0, if length is unknown 198 this.buffers = []; 199 this.loaded = 0; 200} 201 202PdfDataListener.prototype = { 203 append: function PdfDataListener_append(chunk) { 204 // In most of the cases we will pass data as we receive it, but at the 205 // beginning of the loading we may accumulate some data. 206 this.buffers.push(chunk); 207 this.loaded += chunk.length; 208 if (this.length >= 0 && this.length < this.loaded) { 209 this.length = -1; // reset the length, server is giving incorrect one 210 } 211 this.onprogress(this.loaded, this.length >= 0 ? this.length : void 0); 212 }, 213 readData: function PdfDataListener_readData() { 214 if (this.buffers.length === 0) { 215 return null; 216 } 217 if (this.buffers.length === 1) { 218 return this.buffers.pop(); 219 } 220 // There are multiple buffers that need to be combined into a single 221 // buffer. 222 let combinedLength = 0; 223 for (let buffer of this.buffers) { 224 combinedLength += buffer.length; 225 } 226 let combinedArray = new Uint8Array(combinedLength); 227 let writeOffset = 0; 228 while (this.buffers.length) { 229 let buffer = this.buffers.shift(); 230 combinedArray.set(buffer, writeOffset); 231 writeOffset += buffer.length; 232 } 233 return combinedArray; 234 }, 235 get isDone() { 236 return !!this.isDataReady; 237 }, 238 finish: function PdfDataListener_finish() { 239 this.isDataReady = true; 240 if (this.oncompleteCallback) { 241 this.oncompleteCallback(this.readData()); 242 } 243 }, 244 error: function PdfDataListener_error(errorCode) { 245 this.errorCode = errorCode; 246 if (this.oncompleteCallback) { 247 this.oncompleteCallback(null, errorCode); 248 } 249 }, 250 onprogress() {}, 251 get oncomplete() { 252 return this.oncompleteCallback; 253 }, 254 set oncomplete(value) { 255 this.oncompleteCallback = value; 256 if (this.isDataReady) { 257 value(this.readData()); 258 } 259 if (this.errorCode) { 260 value(null, this.errorCode); 261 } 262 }, 263}; 264 265/** 266 * All the privileged actions. 267 */ 268class ChromeActions { 269 constructor(domWindow, contentDispositionFilename) { 270 this.domWindow = domWindow; 271 this.contentDispositionFilename = contentDispositionFilename; 272 this.telemetryState = { 273 documentInfo: false, 274 firstPageInfo: false, 275 streamTypesUsed: {}, 276 fontTypesUsed: {}, 277 fallbackErrorsReported: {}, 278 }; 279 this.sandbox = null; 280 this.unloadListener = null; 281 } 282 283 createSandbox(data, sendResponse) { 284 function sendResp(res) { 285 if (sendResponse) { 286 sendResponse(res); 287 } 288 return res; 289 } 290 291 if (!getBoolPref(PREF_PREFIX + ".enableScripting", false)) { 292 return sendResp(false); 293 } 294 295 if (this.sandbox !== null) { 296 return sendResp(true); 297 } 298 299 try { 300 this.sandbox = new PdfSandbox(this.domWindow, data); 301 } catch (err) { 302 // If there's an error here, it means that something is really wrong 303 // on pdf.js side during sandbox initialization phase. 304 Cu.reportError(err); 305 return sendResp(false); 306 } 307 308 this.unloadListener = () => { 309 this.destroySandbox(); 310 }; 311 this.domWindow.addEventListener("unload", this.unloadListener); 312 313 return sendResp(true); 314 } 315 316 dispatchEventInSandbox(event) { 317 if (this.sandbox) { 318 this.sandbox.dispatchEvent(event); 319 } 320 } 321 322 destroySandbox() { 323 if (this.sandbox) { 324 this.domWindow.removeEventListener("unload", this.unloadListener); 325 this.sandbox.destroy(); 326 this.sandbox = null; 327 } 328 } 329 330 isInPrivateBrowsing() { 331 return PrivateBrowsingUtils.isContentWindowPrivate(this.domWindow); 332 } 333 334 getWindowOriginAttributes() { 335 try { 336 return this.domWindow.document.nodePrincipal.originAttributes; 337 } catch (err) { 338 return {}; 339 } 340 } 341 342 download(data, sendResponse) { 343 var self = this; 344 var originalUrl = data.originalUrl; 345 var blobUrl = data.blobUrl || originalUrl; 346 // The data may not be downloaded so we need just retry getting the pdf with 347 // the original url. 348 var originalUri = NetUtil.newURI(originalUrl); 349 var filename = data.filename; 350 if ( 351 typeof filename !== "string" || 352 (!/\.pdf$/i.test(filename) && !data.isAttachment) 353 ) { 354 filename = "document.pdf"; 355 } 356 var blobUri = NetUtil.newURI(blobUrl); 357 358 // If the download was triggered from the ctrl/cmd+s or "Save Page As" 359 // launch the "Save As" dialog. 360 if (data.sourceEventType == "save") { 361 let actor = getActor(this.domWindow); 362 actor.sendAsyncMessage("PDFJS:Parent:saveURL", { 363 blobUrl, 364 filename, 365 }); 366 return; 367 } 368 369 // The download is from the fallback bar or the download button, so trigger 370 // the open dialog to make it easier for users to save in the downloads 371 // folder or launch a different PDF viewer. 372 var extHelperAppSvc = Cc[ 373 "@mozilla.org/uriloader/external-helper-app-service;1" 374 ].getService(Ci.nsIExternalHelperAppService); 375 376 var docIsPrivate = this.isInPrivateBrowsing(); 377 var netChannel = NetUtil.newChannel({ 378 uri: blobUri, 379 loadUsingSystemPrincipal: true, 380 }); 381 if ( 382 "nsIPrivateBrowsingChannel" in Ci && 383 netChannel instanceof Ci.nsIPrivateBrowsingChannel 384 ) { 385 netChannel.setPrivate(docIsPrivate); 386 } 387 NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) { 388 if (!Components.isSuccessCode(aResult)) { 389 if (sendResponse) { 390 sendResponse(true); 391 } 392 return; 393 } 394 // Create a nsIInputStreamChannel so we can set the url on the channel 395 // so the filename will be correct. 396 var channel = Cc[ 397 "@mozilla.org/network/input-stream-channel;1" 398 ].createInstance(Ci.nsIInputStreamChannel); 399 channel.QueryInterface(Ci.nsIChannel); 400 try { 401 // contentDisposition/contentDispositionFilename is readonly before FF18 402 channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT; 403 if (self.contentDispositionFilename && !data.isAttachment) { 404 channel.contentDispositionFilename = self.contentDispositionFilename; 405 } else { 406 channel.contentDispositionFilename = filename; 407 } 408 } catch (e) {} 409 channel.setURI(originalUri); 410 channel.loadInfo = netChannel.loadInfo; 411 channel.contentStream = aInputStream; 412 if ( 413 "nsIPrivateBrowsingChannel" in Ci && 414 channel instanceof Ci.nsIPrivateBrowsingChannel 415 ) { 416 channel.setPrivate(docIsPrivate); 417 } 418 419 var listener = { 420 extListener: null, 421 onStartRequest(aRequest) { 422 var loadContext = self.domWindow.docShell.QueryInterface( 423 Ci.nsILoadContext 424 ); 425 this.extListener = extHelperAppSvc.doContent( 426 data.isAttachment ? "application/octet-stream" : PDF_CONTENT_TYPE, 427 aRequest, 428 loadContext, 429 false 430 ); 431 this.extListener.onStartRequest(aRequest); 432 }, 433 onStopRequest(aRequest, aStatusCode) { 434 if (this.extListener) { 435 this.extListener.onStopRequest(aRequest, aStatusCode); 436 } 437 // Notify the content code we're done downloading. 438 if (sendResponse) { 439 sendResponse(false); 440 } 441 }, 442 onDataAvailable(aRequest, aDataInputStream, aOffset, aCount) { 443 this.extListener.onDataAvailable( 444 aRequest, 445 aDataInputStream, 446 aOffset, 447 aCount 448 ); 449 }, 450 }; 451 452 channel.asyncOpen(listener); 453 }); 454 } 455 456 getLocale() { 457 return Services.locale.requestedLocale || "en-US"; 458 } 459 460 getStrings(data) { 461 try { 462 // Lazy initialization of localizedStrings 463 if (!("localizedStrings" in this)) { 464 this.localizedStrings = getLocalizedStrings("viewer.properties"); 465 } 466 var result = this.localizedStrings[data]; 467 return JSON.stringify(result || null); 468 } catch (e) { 469 log("Unable to retrieve localized strings: " + e); 470 return "null"; 471 } 472 } 473 474 supportsIntegratedFind() { 475 // Integrated find is only supported when we're not in a frame 476 return this.domWindow.windowGlobalChild.browsingContext.parent === null; 477 } 478 479 supportsDocumentFonts() { 480 var prefBrowser = getIntPref("browser.display.use_document_fonts", 1); 481 var prefGfx = getBoolPref("gfx.downloadable_fonts.enabled", true); 482 return !!prefBrowser && prefGfx; 483 } 484 485 supportedMouseWheelZoomModifierKeys() { 486 return { 487 ctrlKey: getIntPref("mousewheel.with_control.action", 3) === 3, 488 metaKey: getIntPref("mousewheel.with_meta.action", 1) === 3, 489 }; 490 } 491 492 isInAutomation() { 493 return Cu.isInAutomation; 494 } 495 496 reportTelemetry(data) { 497 var probeInfo = JSON.parse(data); 498 switch (probeInfo.type) { 499 case "documentInfo": 500 if (!this.telemetryState.documentInfo) { 501 PdfJsTelemetry.onDocumentVersion(probeInfo.version); 502 PdfJsTelemetry.onDocumentGenerator(probeInfo.generator); 503 if (probeInfo.formType) { 504 PdfJsTelemetry.onForm(probeInfo.formType); 505 } 506 this.telemetryState.documentInfo = true; 507 } 508 break; 509 case "pageInfo": 510 if (!this.telemetryState.firstPageInfo) { 511 PdfJsTelemetry.onTimeToView(probeInfo.timestamp); 512 this.telemetryState.firstPageInfo = true; 513 } 514 break; 515 case "documentStats": 516 // documentStats can be called several times for one documents. 517 // if stream/font types are reported, trying not to submit the same 518 // enumeration value multiple times. 519 var documentStats = probeInfo.stats; 520 if (!documentStats || typeof documentStats !== "object") { 521 break; 522 } 523 var i, 524 streamTypes = documentStats.streamTypes, 525 key; 526 var STREAM_TYPE_ID_LIMIT = 20; 527 i = 0; 528 for (key in streamTypes) { 529 if (++i > STREAM_TYPE_ID_LIMIT) { 530 break; 531 } 532 if (!this.telemetryState.streamTypesUsed[key]) { 533 PdfJsTelemetry.onStreamType(key); 534 this.telemetryState.streamTypesUsed[key] = true; 535 } 536 } 537 var fontTypes = documentStats.fontTypes; 538 var FONT_TYPE_ID_LIMIT = 20; 539 i = 0; 540 for (key in fontTypes) { 541 if (++i > FONT_TYPE_ID_LIMIT) { 542 break; 543 } 544 if (!this.telemetryState.fontTypesUsed[key]) { 545 PdfJsTelemetry.onFontType(key); 546 this.telemetryState.fontTypesUsed[key] = true; 547 } 548 } 549 break; 550 case "print": 551 PdfJsTelemetry.onPrint(); 552 break; 553 case "unsupportedFeature": 554 if (!this.telemetryState.fallbackErrorsReported[probeInfo.featureId]) { 555 PdfJsTelemetry.onFallbackError(probeInfo.featureId); 556 this.telemetryState.fallbackErrorsReported[ 557 probeInfo.featureId 558 ] = true; 559 } 560 break; 561 case "tagged": 562 PdfJsTelemetry.onTagged(probeInfo.tagged); 563 break; 564 } 565 } 566 567 /** 568 * @param {Object} args - Object with `featureId` and `url` properties. 569 * @param {function} sendResponse - Callback function. 570 */ 571 fallback(args, sendResponse) { 572 sendResponse(false); 573 } 574 575 updateFindControlState(data) { 576 if (!this.supportsIntegratedFind()) { 577 return; 578 } 579 // Verify what we're sending to the findbar. 580 var result = data.result; 581 var findPrevious = data.findPrevious; 582 var findPreviousType = typeof findPrevious; 583 if ( 584 typeof result !== "number" || 585 result < 0 || 586 result > 3 || 587 (findPreviousType !== "undefined" && findPreviousType !== "boolean") 588 ) { 589 return; 590 } 591 // Allow the `matchesCount` property to be optional, and ensure that 592 // it's valid before including it in the data sent to the findbar. 593 let matchesCount = null; 594 if (isValidMatchesCount(data.matchesCount)) { 595 matchesCount = data.matchesCount; 596 } 597 // Same for the `rawQuery` property. 598 let rawQuery = null; 599 if (typeof data.rawQuery === "string") { 600 rawQuery = data.rawQuery; 601 } 602 603 let actor = getActor(this.domWindow); 604 actor?.sendAsyncMessage("PDFJS:Parent:updateControlState", { 605 result, 606 findPrevious, 607 matchesCount, 608 rawQuery, 609 }); 610 } 611 612 updateFindMatchesCount(data) { 613 if (!this.supportsIntegratedFind()) { 614 return; 615 } 616 // Verify what we're sending to the findbar. 617 if (!isValidMatchesCount(data)) { 618 return; 619 } 620 621 let actor = getActor(this.domWindow); 622 actor?.sendAsyncMessage("PDFJS:Parent:updateMatchesCount", data); 623 } 624 625 setPreferences(prefs, sendResponse) { 626 var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + "."); 627 var numberOfPrefs = 0; 628 var prefValue, prefName; 629 for (var key in prefs) { 630 if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) { 631 log( 632 "setPreferences - Exceeded the maximum number of preferences " + 633 "that is allowed to be set at once." 634 ); 635 break; 636 } else if (!defaultBranch.getPrefType(key)) { 637 continue; 638 } 639 prefValue = prefs[key]; 640 prefName = PREF_PREFIX + "." + key; 641 switch (typeof prefValue) { 642 case "boolean": 643 AsyncPrefs.set(prefName, prefValue); 644 break; 645 case "number": 646 AsyncPrefs.set(prefName, prefValue); 647 break; 648 case "string": 649 if (prefValue.length > MAX_STRING_PREF_LENGTH) { 650 log( 651 "setPreferences - Exceeded the maximum allowed length " + 652 "for a string preference." 653 ); 654 } else { 655 AsyncPrefs.set(prefName, prefValue); 656 } 657 break; 658 } 659 } 660 if (sendResponse) { 661 sendResponse(true); 662 } 663 } 664 665 getPreferences(prefs, sendResponse) { 666 var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + "."); 667 var currentPrefs = {}, 668 numberOfPrefs = 0; 669 var prefValue, prefName; 670 for (var key in prefs) { 671 if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) { 672 log( 673 "getPreferences - Exceeded the maximum number of preferences " + 674 "that is allowed to be fetched at once." 675 ); 676 break; 677 } else if (!defaultBranch.getPrefType(key)) { 678 continue; 679 } 680 prefValue = prefs[key]; 681 prefName = PREF_PREFIX + "." + key; 682 switch (typeof prefValue) { 683 case "boolean": 684 currentPrefs[key] = getBoolPref(prefName, prefValue); 685 break; 686 case "number": 687 currentPrefs[key] = getIntPref(prefName, prefValue); 688 break; 689 case "string": 690 currentPrefs[key] = getStringPref(prefName, prefValue); 691 break; 692 } 693 } 694 let result = JSON.stringify(currentPrefs); 695 if (sendResponse) { 696 sendResponse(result); 697 } 698 return result; 699 } 700} 701 702/** 703 * This is for range requests. 704 */ 705class RangedChromeActions extends ChromeActions { 706 constructor( 707 domWindow, 708 contentDispositionFilename, 709 originalRequest, 710 rangeEnabled, 711 streamingEnabled, 712 dataListener 713 ) { 714 super(domWindow, contentDispositionFilename); 715 this.dataListener = dataListener; 716 this.originalRequest = originalRequest; 717 this.rangeEnabled = rangeEnabled; 718 this.streamingEnabled = streamingEnabled; 719 720 this.pdfUrl = originalRequest.URI.spec; 721 this.contentLength = originalRequest.contentLength; 722 723 // Pass all the headers from the original request through 724 var httpHeaderVisitor = { 725 headers: {}, 726 visitHeader(aHeader, aValue) { 727 if (aHeader === "Range") { 728 // When loading the PDF from cache, firefox seems to set the Range 729 // request header to fetch only the unfetched portions of the file 730 // (e.g. 'Range: bytes=1024-'). However, we want to set this header 731 // manually to fetch the PDF in chunks. 732 return; 733 } 734 this.headers[aHeader] = aValue; 735 }, 736 }; 737 if (originalRequest.visitRequestHeaders) { 738 originalRequest.visitRequestHeaders(httpHeaderVisitor); 739 } 740 741 var self = this; 742 var xhr_onreadystatechange = function xhr_onreadystatechange() { 743 if (this.readyState === 1) { 744 // LOADING 745 var netChannel = this.channel; 746 // override this XMLHttpRequest's OriginAttributes with our cached parent window's 747 // OriginAttributes, as we are currently running under the SystemPrincipal 748 this.setOriginAttributes(self.getWindowOriginAttributes()); 749 if ( 750 "nsIPrivateBrowsingChannel" in Ci && 751 netChannel instanceof Ci.nsIPrivateBrowsingChannel 752 ) { 753 var docIsPrivate = self.isInPrivateBrowsing(); 754 netChannel.setPrivate(docIsPrivate); 755 } 756 } 757 }; 758 var getXhr = function getXhr() { 759 var xhr = new XMLHttpRequest(); 760 xhr.addEventListener("readystatechange", xhr_onreadystatechange); 761 return xhr; 762 }; 763 764 this.networkManager = new NetworkManager(this.pdfUrl, { 765 httpHeaders: httpHeaderVisitor.headers, 766 getXhr, 767 }); 768 769 // If we are in range request mode, this means we manually issued xhr 770 // requests, which we need to abort when we leave the page 771 domWindow.addEventListener("unload", function unload(e) { 772 domWindow.removeEventListener(e.type, unload); 773 self.abortLoading(); 774 }); 775 } 776 777 initPassiveLoading() { 778 let data, done; 779 if (!this.streamingEnabled) { 780 this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); 781 this.originalRequest = null; 782 data = this.dataListener.readData(); 783 done = this.dataListener.isDone; 784 this.dataListener = null; 785 } else { 786 data = this.dataListener.readData(); 787 done = this.dataListener.isDone; 788 789 this.dataListener.onprogress = (loaded, total) => { 790 this.domWindow.postMessage( 791 { 792 pdfjsLoadAction: "progressiveRead", 793 loaded, 794 total, 795 chunk: this.dataListener.readData(), 796 }, 797 PDF_VIEWER_ORIGIN 798 ); 799 }; 800 this.dataListener.oncomplete = () => { 801 if (!done && this.dataListener.isDone) { 802 this.domWindow.postMessage( 803 { 804 pdfjsLoadAction: "progressiveDone", 805 }, 806 PDF_VIEWER_ORIGIN 807 ); 808 } 809 this.dataListener = null; 810 }; 811 } 812 813 this.domWindow.postMessage( 814 { 815 pdfjsLoadAction: "supportsRangedLoading", 816 rangeEnabled: this.rangeEnabled, 817 streamingEnabled: this.streamingEnabled, 818 pdfUrl: this.pdfUrl, 819 length: this.contentLength, 820 data, 821 done, 822 filename: this.contentDispositionFilename, 823 }, 824 PDF_VIEWER_ORIGIN 825 ); 826 827 return true; 828 } 829 830 requestDataRange(args) { 831 if (!this.rangeEnabled) { 832 return; 833 } 834 835 var begin = args.begin; 836 var end = args.end; 837 var domWindow = this.domWindow; 838 // TODO(mack): Support error handler. We're not currently not handling 839 // errors from chrome code for non-range requests, so this doesn't 840 // seem high-pri 841 this.networkManager.requestRange(begin, end, { 842 onDone: function RangedChromeActions_onDone(aArgs) { 843 domWindow.postMessage( 844 { 845 pdfjsLoadAction: "range", 846 begin: aArgs.begin, 847 chunk: aArgs.chunk, 848 }, 849 PDF_VIEWER_ORIGIN 850 ); 851 }, 852 onProgress: function RangedChromeActions_onProgress(evt) { 853 domWindow.postMessage( 854 { 855 pdfjsLoadAction: "rangeProgress", 856 loaded: evt.loaded, 857 }, 858 PDF_VIEWER_ORIGIN 859 ); 860 }, 861 }); 862 } 863 864 abortLoading() { 865 this.networkManager.abortAllRequests(); 866 if (this.originalRequest) { 867 this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); 868 this.originalRequest = null; 869 } 870 this.dataListener = null; 871 } 872} 873 874/** 875 * This is for a single network stream. 876 */ 877class StandardChromeActions extends ChromeActions { 878 constructor( 879 domWindow, 880 contentDispositionFilename, 881 originalRequest, 882 dataListener 883 ) { 884 super(domWindow, contentDispositionFilename); 885 this.originalRequest = originalRequest; 886 this.dataListener = dataListener; 887 } 888 889 initPassiveLoading() { 890 if (!this.dataListener) { 891 return false; 892 } 893 894 this.dataListener.onprogress = (loaded, total) => { 895 this.domWindow.postMessage( 896 { 897 pdfjsLoadAction: "progress", 898 loaded, 899 total, 900 }, 901 PDF_VIEWER_ORIGIN 902 ); 903 }; 904 905 this.dataListener.oncomplete = (data, errorCode) => { 906 this.domWindow.postMessage( 907 { 908 pdfjsLoadAction: "complete", 909 data, 910 errorCode, 911 filename: this.contentDispositionFilename, 912 }, 913 PDF_VIEWER_ORIGIN 914 ); 915 916 this.dataListener = null; 917 this.originalRequest = null; 918 }; 919 920 return true; 921 } 922 923 abortLoading() { 924 if (this.originalRequest) { 925 this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); 926 this.originalRequest = null; 927 } 928 this.dataListener = null; 929 } 930} 931 932/** 933 * Event listener to trigger chrome privileged code. 934 */ 935class RequestListener { 936 constructor(actions) { 937 this.actions = actions; 938 } 939 940 // Receive an event and synchronously or asynchronously responds. 941 receive(event) { 942 var message = event.target; 943 var doc = message.ownerDocument; 944 var action = event.detail.action; 945 var data = event.detail.data; 946 var sync = event.detail.sync; 947 var actions = this.actions; 948 if (!(action in actions)) { 949 log("Unknown action: " + action); 950 return; 951 } 952 var response; 953 if (sync) { 954 response = actions[action].call(this.actions, data); 955 event.detail.response = Cu.cloneInto(response, doc.defaultView); 956 } else { 957 if (!event.detail.responseExpected) { 958 doc.documentElement.removeChild(message); 959 response = null; 960 } else { 961 response = function sendResponse(aResponse) { 962 try { 963 var listener = doc.createEvent("CustomEvent"); 964 let detail = Cu.cloneInto({ response: aResponse }, doc.defaultView); 965 listener.initCustomEvent("pdf.js.response", true, false, detail); 966 return message.dispatchEvent(listener); 967 } catch (e) { 968 // doc is no longer accessible because the requestor is already 969 // gone. unloaded content cannot receive the response anyway. 970 return false; 971 } 972 }; 973 } 974 actions[action].call(this.actions, data, response); 975 } 976 } 977} 978 979function PdfStreamConverter() {} 980 981PdfStreamConverter.prototype = { 982 QueryInterface: ChromeUtils.generateQI([ 983 "nsIStreamConverter", 984 "nsIStreamListener", 985 "nsIRequestObserver", 986 ]), 987 988 /* 989 * This component works as such: 990 * 1. asyncConvertData stores the listener 991 * 2. onStartRequest creates a new channel, streams the viewer 992 * 3. If range requests are supported: 993 * 3.1. Leave the request open until the viewer is ready to switch to 994 * range requests. 995 * 996 * If range rquests are not supported: 997 * 3.1. Read the stream as it's loaded in onDataAvailable to send 998 * to the viewer 999 * 1000 * The convert function just returns the stream, it's just the synchronous 1001 * version of asyncConvertData. 1002 */ 1003 1004 // nsIStreamConverter::convert 1005 convert(aFromStream, aFromType, aToType, aCtxt) { 1006 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 1007 }, 1008 1009 // nsIStreamConverter::asyncConvertData 1010 asyncConvertData(aFromType, aToType, aListener, aCtxt) { 1011 if (aCtxt && aCtxt instanceof Ci.nsIChannel) { 1012 aCtxt.QueryInterface(Ci.nsIChannel); 1013 } 1014 // We need to check if we're supposed to convert here, because not all 1015 // asyncConvertData consumers will call getConvertedType first: 1016 this.getConvertedType(aFromType, aCtxt); 1017 1018 // Store the listener passed to us 1019 this.listener = aListener; 1020 }, 1021 1022 _usableHandler(handlerInfo) { 1023 let { preferredApplicationHandler } = handlerInfo; 1024 if ( 1025 !preferredApplicationHandler || 1026 !(preferredApplicationHandler instanceof Ci.nsILocalHandlerApp) 1027 ) { 1028 return false; 1029 } 1030 preferredApplicationHandler.QueryInterface(Ci.nsILocalHandlerApp); 1031 // We have an app, grab the executable 1032 let { executable } = preferredApplicationHandler; 1033 if (!executable) { 1034 return false; 1035 } 1036 return !executable.equals(gOurBinary); 1037 }, 1038 1039 /* 1040 * Check if the user wants to use PDF.js. Returns true if PDF.js should 1041 * handle PDFs, and false if not. Will always return true on non-parent 1042 * processes. 1043 * 1044 * If the user has selected to open PDFs with a helper app, and we are that 1045 * helper app, or if the user has selected the OS default, and we are that 1046 * OS default, reset the preference back to pdf.js . 1047 * 1048 */ 1049 _validateAndMaybeUpdatePDFPrefs() { 1050 let { processType, PROCESS_TYPE_DEFAULT } = Services.appinfo; 1051 // If we're not in the parent, or are the default, then just say yes. 1052 if (processType != PROCESS_TYPE_DEFAULT || PdfJs.cachedIsDefault()) { 1053 return { shouldOpen: true }; 1054 } 1055 1056 // OK, PDF.js might not be the default. Find out if we've misled the user 1057 // into making Firefox an external handler or if we're the OS default and 1058 // Firefox is set to use the OS default: 1059 let mime = Svc.mime.getFromTypeAndExtension(PDF_CONTENT_TYPE, "pdf"); 1060 // The above might throw errors. We're deliberately letting those bubble 1061 // back up, where they'll tell the stream converter not to use us. 1062 1063 if (!mime) { 1064 // This shouldn't happen, but we can't fix what isn't there. Assume 1065 // we're OK to handle with PDF.js 1066 return { shouldOpen: true }; 1067 } 1068 1069 const { saveToDisk, useHelperApp, useSystemDefault } = Ci.nsIHandlerInfo; 1070 let { preferredAction, alwaysAskBeforeHandling } = mime; 1071 // return this info so getConvertedType can use it. 1072 let rv = { alwaysAskBeforeHandling, shouldOpen: false }; 1073 // If the user has indicated they want to be asked or want to save to 1074 // disk, we shouldn't render inline immediately: 1075 if (alwaysAskBeforeHandling || preferredAction == saveToDisk) { 1076 return rv; 1077 } 1078 // If we have usable helper app info, don't use PDF.js 1079 if (preferredAction == useHelperApp && this._usableHandler(mime)) { 1080 return rv; 1081 } 1082 // If we want the OS default and that's not Firefox, don't use PDF.js 1083 if (preferredAction == useSystemDefault && !mime.isCurrentAppOSDefault()) { 1084 return rv; 1085 } 1086 rv.shouldOpen = true; 1087 // Log that we're doing this to help debug issues if people end up being 1088 // surprised by this behaviour. 1089 Cu.reportError("Found unusable PDF preferences. Fixing back to PDF.js"); 1090 1091 mime.preferredAction = Ci.nsIHandlerInfo.handleInternally; 1092 mime.alwaysAskBeforeHandling = false; 1093 Svc.handlers.store(mime); 1094 return true; 1095 }, 1096 1097 getConvertedType(aFromType, aChannel) { 1098 const HTML = "text/html"; 1099 let channelURI = aChannel?.URI; 1100 // We can be invoked for application/octet-stream; check if we want the 1101 // channel first: 1102 if (aFromType != "application/pdf") { 1103 let ext = channelURI?.QueryInterface(Ci.nsIURL).fileExtension; 1104 let isPDF = ext.toLowerCase() == "pdf"; 1105 let browsingContext = aChannel?.loadInfo.targetBrowsingContext; 1106 let toplevelOctetStream = 1107 aFromType == "application/octet-stream" && 1108 browsingContext && 1109 !browsingContext.parent; 1110 if ( 1111 !isPDF || 1112 !toplevelOctetStream || 1113 !getBoolPref(PREF_PREFIX + ".handleOctetStream", false) 1114 ) { 1115 throw new Components.Exception( 1116 "Ignore PDF.js for this download.", 1117 Cr.NS_ERROR_FAILURE 1118 ); 1119 } 1120 // fall through, this appears to be a pdf. 1121 } 1122 1123 let { 1124 alwaysAskBeforeHandling, 1125 shouldOpen, 1126 } = this._validateAndMaybeUpdatePDFPrefs(); 1127 1128 if (shouldOpen) { 1129 return HTML; 1130 } 1131 // Hm, so normally, no pdfjs. However... if this is a file: channel there 1132 // are some edge-cases. 1133 if (channelURI?.schemeIs("file")) { 1134 // If we're loaded with system principal, we were likely handed the PDF 1135 // by the OS or directly from the URL bar. Assume we should load it: 1136 let triggeringPrincipal = aChannel.loadInfo?.triggeringPrincipal; 1137 if (triggeringPrincipal?.isSystemPrincipal) { 1138 return HTML; 1139 } 1140 1141 // If we're loading from a file: link, load it in PDF.js unless the user 1142 // has told us they always want to open/save PDFs. 1143 // This is because handing off the choice to open in Firefox itself 1144 // through the dialog doesn't work properly and making it work is 1145 // non-trivial (see https://bugzilla.mozilla.org/show_bug.cgi?id=1680147#c3 ) 1146 // - and anyway, opening the file is what we do for *all* 1147 // other file types we handle internally (and users can then use other UI 1148 // to save or open it with other apps from there). 1149 if (triggeringPrincipal?.schemeIs("file") && alwaysAskBeforeHandling) { 1150 return HTML; 1151 } 1152 } 1153 1154 throw new Components.Exception("Can't use PDF.js", Cr.NS_ERROR_FAILURE); 1155 }, 1156 1157 // nsIStreamListener::onDataAvailable 1158 onDataAvailable(aRequest, aInputStream, aOffset, aCount) { 1159 if (!this.dataListener) { 1160 return; 1161 } 1162 1163 var binaryStream = this.binaryStream; 1164 binaryStream.setInputStream(aInputStream); 1165 let chunk = new ArrayBuffer(aCount); 1166 binaryStream.readArrayBuffer(aCount, chunk); 1167 this.dataListener.append(new Uint8Array(chunk)); 1168 }, 1169 1170 // nsIRequestObserver::onStartRequest 1171 onStartRequest(aRequest) { 1172 // Setup the request so we can use it below. 1173 var isHttpRequest = false; 1174 try { 1175 aRequest.QueryInterface(Ci.nsIHttpChannel); 1176 isHttpRequest = true; 1177 } catch (e) {} 1178 1179 var rangeRequest = false; 1180 var streamRequest = false; 1181 if (isHttpRequest) { 1182 var contentEncoding = "identity"; 1183 try { 1184 contentEncoding = aRequest.getResponseHeader("Content-Encoding"); 1185 } catch (e) {} 1186 1187 var acceptRanges; 1188 try { 1189 acceptRanges = aRequest.getResponseHeader("Accept-Ranges"); 1190 } catch (e) {} 1191 1192 var hash = aRequest.URI.ref; 1193 var isPDFBugEnabled = getBoolPref(PREF_PREFIX + ".pdfBugEnabled", false); 1194 rangeRequest = 1195 contentEncoding === "identity" && 1196 acceptRanges === "bytes" && 1197 aRequest.contentLength >= 0 && 1198 !getBoolPref(PREF_PREFIX + ".disableRange", false) && 1199 (!isPDFBugEnabled || !hash.toLowerCase().includes("disablerange=true")); 1200 streamRequest = 1201 contentEncoding === "identity" && 1202 aRequest.contentLength >= 0 && 1203 !getBoolPref(PREF_PREFIX + ".disableStream", false) && 1204 (!isPDFBugEnabled || 1205 !hash.toLowerCase().includes("disablestream=true")); 1206 } 1207 1208 aRequest.QueryInterface(Ci.nsIChannel); 1209 1210 aRequest.QueryInterface(Ci.nsIWritablePropertyBag); 1211 1212 var contentDispositionFilename; 1213 try { 1214 contentDispositionFilename = aRequest.contentDispositionFilename; 1215 } catch (e) {} 1216 1217 // Change the content type so we don't get stuck in a loop. 1218 aRequest.setProperty("contentType", aRequest.contentType); 1219 aRequest.contentType = "text/html"; 1220 if (isHttpRequest) { 1221 // We trust PDF viewer, using no CSP 1222 aRequest.setResponseHeader("Content-Security-Policy", "", false); 1223 aRequest.setResponseHeader( 1224 "Content-Security-Policy-Report-Only", 1225 "", 1226 false 1227 ); 1228 // The viewer does not need to handle HTTP Refresh header. 1229 aRequest.setResponseHeader("Refresh", "", false); 1230 } 1231 1232 PdfJsTelemetry.onViewerIsUsed(); 1233 PdfJsTelemetry.onDocumentSize(aRequest.contentLength); 1234 1235 // Creating storage for PDF data 1236 var contentLength = aRequest.contentLength; 1237 this.dataListener = new PdfDataListener(contentLength); 1238 this.binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( 1239 Ci.nsIBinaryInputStream 1240 ); 1241 1242 // Create a new channel that is viewer loaded as a resource. 1243 var channel = NetUtil.newChannel({ 1244 uri: PDF_VIEWER_WEB_PAGE, 1245 loadUsingSystemPrincipal: true, 1246 }); 1247 1248 var listener = this.listener; 1249 var dataListener = this.dataListener; 1250 // Proxy all the request observer calls, when it gets to onStopRequest 1251 // we can get the dom window. We also intentionally pass on the original 1252 // request(aRequest) below so we don't overwrite the original channel and 1253 // trigger an assertion. 1254 var proxy = { 1255 onStartRequest(request) { 1256 listener.onStartRequest(aRequest); 1257 }, 1258 onDataAvailable(request, inputStream, offset, count) { 1259 listener.onDataAvailable(aRequest, inputStream, offset, count); 1260 }, 1261 onStopRequest(request, statusCode) { 1262 var domWindow = getDOMWindow(channel, resourcePrincipal); 1263 if (!Components.isSuccessCode(statusCode) || !domWindow) { 1264 // The request may have been aborted and the document may have been 1265 // replaced with something that is not PDF.js, abort attaching. 1266 listener.onStopRequest(aRequest, statusCode); 1267 return; 1268 } 1269 var actions; 1270 if (rangeRequest || streamRequest) { 1271 actions = new RangedChromeActions( 1272 domWindow, 1273 contentDispositionFilename, 1274 aRequest, 1275 rangeRequest, 1276 streamRequest, 1277 dataListener 1278 ); 1279 } else { 1280 actions = new StandardChromeActions( 1281 domWindow, 1282 contentDispositionFilename, 1283 aRequest, 1284 dataListener 1285 ); 1286 } 1287 var requestListener = new RequestListener(actions); 1288 domWindow.document.addEventListener( 1289 PDFJS_EVENT_ID, 1290 function(event) { 1291 requestListener.receive(event); 1292 }, 1293 false, 1294 true 1295 ); 1296 1297 let actor = getActor(domWindow); 1298 actor?.init(actions.supportsIntegratedFind()); 1299 1300 listener.onStopRequest(aRequest, statusCode); 1301 1302 if (domWindow.windowGlobalChild.browsingContext.parent) { 1303 // This will need to be changed when fission supports object/embed (bug 1614524) 1304 var isObjectEmbed = domWindow.frameElement 1305 ? domWindow.frameElement.tagName == "OBJECT" || 1306 domWindow.frameElement.tagName == "EMBED" 1307 : false; 1308 PdfJsTelemetry.onEmbed(isObjectEmbed); 1309 } 1310 }, 1311 }; 1312 1313 // Keep the URL the same so the browser sees it as the same. 1314 channel.originalURI = aRequest.URI; 1315 channel.loadGroup = aRequest.loadGroup; 1316 channel.loadInfo.originAttributes = aRequest.loadInfo.originAttributes; 1317 1318 // We can use the resource principal when data is fetched by the chrome, 1319 // e.g. useful for NoScript. Make make sure we reuse the origin attributes 1320 // from the request channel to keep isolation consistent. 1321 var uri = NetUtil.newURI(PDF_VIEWER_WEB_PAGE); 1322 var resourcePrincipal = Services.scriptSecurityManager.createContentPrincipal( 1323 uri, 1324 aRequest.loadInfo.originAttributes 1325 ); 1326 // Remember the principal we would have had before we mess with it. 1327 let originalPrincipal = Services.scriptSecurityManager.getChannelResultPrincipal( 1328 aRequest 1329 ); 1330 aRequest.owner = resourcePrincipal; 1331 aRequest.setProperty("noPDFJSPrincipal", originalPrincipal); 1332 1333 channel.asyncOpen(proxy); 1334 }, 1335 1336 // nsIRequestObserver::onStopRequest 1337 onStopRequest(aRequest, aStatusCode) { 1338 if (!this.dataListener) { 1339 // Do nothing 1340 return; 1341 } 1342 1343 if (Components.isSuccessCode(aStatusCode)) { 1344 this.dataListener.finish(); 1345 } else { 1346 this.dataListener.error(aStatusCode); 1347 } 1348 delete this.dataListener; 1349 delete this.binaryStream; 1350 }, 1351}; 1352