gecko-dev/browser/extensions/pdfjs/content/PdfStreamConverter.jsm

1029 строки
32 KiB
JavaScript

/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
var EXPORTED_SYMBOLS = ["PdfStreamConverter"];
const PDFJS_EVENT_ID = "pdf.js.message";
const PREF_PREFIX = "pdfjs";
const PDF_VIEWER_ORIGIN = "resource://pdf.js";
const PDF_VIEWER_WEB_PAGE = "resource://pdf.js/web/viewer.html";
const MAX_NUMBER_OF_PREFS = 50;
const MAX_STRING_PREF_LENGTH = 128;
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
ChromeUtils.defineModuleGetter(this, "NetworkManager",
"resource://pdf.js/PdfJsNetwork.jsm");
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
ChromeUtils.defineModuleGetter(this, "PdfJsTelemetry",
"resource://pdf.js/PdfJsTelemetry.jsm");
ChromeUtils.defineModuleGetter(this, "PdfjsContentUtils",
"resource://pdf.js/PdfjsContentUtils.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]);
var Svc = {};
XPCOMUtils.defineLazyServiceGetter(Svc, "mime",
"@mozilla.org/mime;1",
"nsIMIMEService");
function getBoolPref(pref, def) {
try {
return Services.prefs.getBoolPref(pref);
} catch (ex) {
return def;
}
}
function getIntPref(pref, def) {
try {
return Services.prefs.getIntPref(pref);
} catch (ex) {
return def;
}
}
function getStringPref(pref, def) {
try {
return Services.prefs.getStringPref(pref);
} catch (ex) {
return def;
}
}
function log(aMsg) {
if (!getBoolPref(PREF_PREFIX + ".pdfBugEnabled", false)) {
return;
}
var msg = "PdfStreamConverter.js: " + (aMsg.join ? aMsg.join("") : aMsg);
Services.console.logStringMessage(msg);
dump(msg + "\n");
}
function getDOMWindow(aChannel, aPrincipal) {
var requestor = aChannel.notificationCallbacks ?
aChannel.notificationCallbacks :
aChannel.loadGroup.notificationCallbacks;
var win = requestor.getInterface(Ci.nsIDOMWindow);
// Ensure the window wasn't navigated to something that is not PDF.js.
if (!win.document.nodePrincipal.equals(aPrincipal)) {
return null;
}
return win;
}
function getLocalizedStrings(path) {
var stringBundle =
Services.strings.createBundle("chrome://pdf.js/locale/" + path);
var map = {};
for (let string of stringBundle.getSimpleEnumeration()) {
var key = string.key, property = "textContent";
var i = key.lastIndexOf(".");
if (i >= 0) {
property = key.substring(i + 1);
key = key.substring(0, i);
}
if (!(key in map)) {
map[key] = {};
}
map[key][property] = string.value;
}
return map;
}
function getLocalizedString(strings, id, property) {
property = property || "textContent";
if (id in strings) {
return strings[id][property];
}
return id;
}
function isValidMatchesCount(data) {
if (typeof data !== "object" || data === null) {
return false;
}
const {current, total} = data;
if ((typeof total !== "number" || total < 0) ||
(typeof current !== "number" || current < 0 || current > total)) {
return false;
}
return true;
}
// PDF data storage
function PdfDataListener(length) {
this.length = length; // less than 0, if length is unknown
this.buffers = [];
this.loaded = 0;
}
PdfDataListener.prototype = {
append: function PdfDataListener_append(chunk) {
// In most of the cases we will pass data as we receive it, but at the
// beginning of the loading we may accumulate some data.
this.buffers.push(chunk);
this.loaded += chunk.length;
if (this.length >= 0 && this.length < this.loaded) {
this.length = -1; // reset the length, server is giving incorrect one
}
this.onprogress(this.loaded, this.length >= 0 ? this.length : void 0);
},
readData: function PdfDataListener_readData() {
if (this.buffers.length === 0) {
return null;
}
if (this.buffers.length === 1) {
return this.buffers.pop();
}
// There are multiple buffers that need to be combined into a single
// buffer.
let combinedLength = 0;
for (let buffer of this.buffers) {
combinedLength += buffer.length;
}
let combinedArray = new Uint8Array(combinedLength);
let writeOffset = 0;
while (this.buffers.length) {
let buffer = this.buffers.shift();
combinedArray.set(buffer, writeOffset);
writeOffset += buffer.length;
}
return combinedArray;
},
get isDone() {
return !!this.isDataReady;
},
finish: function PdfDataListener_finish() {
this.isDataReady = true;
if (this.oncompleteCallback) {
this.oncompleteCallback(this.readData());
}
},
error: function PdfDataListener_error(errorCode) {
this.errorCode = errorCode;
if (this.oncompleteCallback) {
this.oncompleteCallback(null, errorCode);
}
},
onprogress() {},
get oncomplete() {
return this.oncompleteCallback;
},
set oncomplete(value) {
this.oncompleteCallback = value;
if (this.isDataReady) {
value(this.readData());
}
if (this.errorCode) {
value(null, this.errorCode);
}
},
};
/**
* All the privileged actions.
*/
class ChromeActions {
constructor(domWindow, contentDispositionFilename) {
this.domWindow = domWindow;
this.contentDispositionFilename = contentDispositionFilename;
this.telemetryState = {
documentInfo: false,
firstPageInfo: false,
streamTypesUsed: [],
fontTypesUsed: [],
startAt: Date.now(),
};
}
isInPrivateBrowsing() {
return PrivateBrowsingUtils.isContentWindowPrivate(this.domWindow);
}
download(data, sendResponse) {
var self = this;
var originalUrl = data.originalUrl;
var blobUrl = data.blobUrl || originalUrl;
// The data may not be downloaded so we need just retry getting the pdf with
// the original url.
var originalUri = NetUtil.newURI(originalUrl);
var filename = data.filename;
if (typeof filename !== "string" ||
(!/\.pdf$/i.test(filename) && !data.isAttachment)) {
filename = "document.pdf";
}
var blobUri = NetUtil.newURI(blobUrl);
var extHelperAppSvc =
Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
getService(Ci.nsIExternalHelperAppService);
var docIsPrivate = this.isInPrivateBrowsing();
var netChannel = NetUtil.newChannel({
uri: blobUri,
loadUsingSystemPrincipal: true,
});
if ("nsIPrivateBrowsingChannel" in Ci &&
netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
netChannel.setPrivate(docIsPrivate);
}
NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) {
if (!Components.isSuccessCode(aResult)) {
if (sendResponse) {
sendResponse(true);
}
return;
}
// Create a nsIInputStreamChannel so we can set the url on the channel
// so the filename will be correct.
var channel = Cc["@mozilla.org/network/input-stream-channel;1"].
createInstance(Ci.nsIInputStreamChannel);
channel.QueryInterface(Ci.nsIChannel);
try {
// contentDisposition/contentDispositionFilename is readonly before FF18
channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
if (self.contentDispositionFilename && !data.isAttachment) {
channel.contentDispositionFilename = self.contentDispositionFilename;
} else {
channel.contentDispositionFilename = filename;
}
} catch (e) {}
channel.setURI(originalUri);
channel.loadInfo = netChannel.loadInfo;
channel.contentStream = aInputStream;
if ("nsIPrivateBrowsingChannel" in Ci &&
channel instanceof Ci.nsIPrivateBrowsingChannel) {
channel.setPrivate(docIsPrivate);
}
var listener = {
extListener: null,
onStartRequest(aRequest) {
var loadContext = self.domWindow.docShell
.QueryInterface(Ci.nsILoadContext);
this.extListener = extHelperAppSvc.doContent(
(data.isAttachment ? "application/octet-stream" :
"application/pdf"),
aRequest, loadContext, false);
this.extListener.onStartRequest(aRequest);
},
onStopRequest(aRequest, aStatusCode) {
if (this.extListener) {
this.extListener.onStopRequest(aRequest, aStatusCode);
}
// Notify the content code we're done downloading.
if (sendResponse) {
sendResponse(false);
}
},
onDataAvailable(aRequest, aDataInputStream, aOffset, aCount) {
this.extListener.onDataAvailable(aRequest, aDataInputStream,
aOffset, aCount);
},
};
channel.asyncOpen(listener);
});
}
getLocale() {
return Services.locale.requestedLocale || "en-US";
}
getStrings(data) {
try {
// Lazy initialization of localizedStrings
if (!("localizedStrings" in this)) {
this.localizedStrings = getLocalizedStrings("viewer.properties");
}
var result = this.localizedStrings[data];
return JSON.stringify(result || null);
} catch (e) {
log("Unable to retrieve localized strings: " + e);
return "null";
}
}
supportsIntegratedFind() {
// Integrated find is only supported when we're not in a frame
return this.domWindow.frameElement === null;
}
supportsDocumentFonts() {
var prefBrowser = getIntPref("browser.display.use_document_fonts", 1);
var prefGfx = getBoolPref("gfx.downloadable_fonts.enabled", true);
return (!!prefBrowser && prefGfx);
}
supportsDocumentColors() {
return getIntPref("browser.display.document_color_use", 0) !== 2;
}
supportedMouseWheelZoomModifierKeys() {
return {
ctrlKey: getIntPref("mousewheel.with_control.action", 3) === 3,
metaKey: getIntPref("mousewheel.with_meta.action", 1) === 3,
};
}
reportTelemetry(data) {
var probeInfo = JSON.parse(data);
switch (probeInfo.type) {
case "documentInfo":
if (!this.telemetryState.documentInfo) {
PdfJsTelemetry.onDocumentVersion(probeInfo.version | 0);
PdfJsTelemetry.onDocumentGenerator(probeInfo.generator | 0);
if (probeInfo.formType) {
PdfJsTelemetry.onForm(probeInfo.formType === "acroform");
}
this.telemetryState.documentInfo = true;
}
break;
case "pageInfo":
if (!this.telemetryState.firstPageInfo) {
var duration = Date.now() - this.telemetryState.startAt;
PdfJsTelemetry.onTimeToView(duration);
this.telemetryState.firstPageInfo = true;
}
break;
case "documentStats":
// documentStats can be called several times for one documents.
// if stream/font types are reported, trying not to submit the same
// enumeration value multiple times.
var documentStats = probeInfo.stats;
if (!documentStats || typeof documentStats !== "object") {
break;
}
var i, streamTypes = documentStats.streamTypes;
if (Array.isArray(streamTypes)) {
var STREAM_TYPE_ID_LIMIT = 20;
for (i = 0; i < STREAM_TYPE_ID_LIMIT; i++) {
if (streamTypes[i] &&
!this.telemetryState.streamTypesUsed[i]) {
PdfJsTelemetry.onStreamType(i);
this.telemetryState.streamTypesUsed[i] = true;
}
}
}
var fontTypes = documentStats.fontTypes;
if (Array.isArray(fontTypes)) {
var FONT_TYPE_ID_LIMIT = 20;
for (i = 0; i < FONT_TYPE_ID_LIMIT; i++) {
if (fontTypes[i] &&
!this.telemetryState.fontTypesUsed[i]) {
PdfJsTelemetry.onFontType(i);
this.telemetryState.fontTypesUsed[i] = true;
}
}
}
break;
case "print":
PdfJsTelemetry.onPrint();
break;
}
}
/**
* @param {Object} args - Object with `featureId` and `url` properties.
* @param {function} sendResponse - Callback function.
*/
fallback(args, sendResponse) {
var featureId = args.featureId;
var domWindow = this.domWindow;
var strings = getLocalizedStrings("chrome.properties");
var message;
if (featureId === "forms") {
message = getLocalizedString(strings, "unsupported_feature_forms");
} else {
message = getLocalizedString(strings, "unsupported_feature");
}
PdfJsTelemetry.onFallback();
PdfjsContentUtils.displayWarning(domWindow, message,
getLocalizedString(strings, "open_with_different_viewer"),
getLocalizedString(strings, "open_with_different_viewer", "accessKey"));
let winmm = domWindow.docShell.messageManager;
winmm.addMessageListener("PDFJS:Child:fallbackDownload",
function fallbackDownload(msg) {
let data = msg.data;
sendResponse(data.download);
winmm.removeMessageListener("PDFJS:Child:fallbackDownload",
fallbackDownload);
});
}
updateFindControlState(data) {
if (!this.supportsIntegratedFind()) {
return;
}
// Verify what we're sending to the findbar.
var result = data.result;
var findPrevious = data.findPrevious;
var findPreviousType = typeof findPrevious;
if ((typeof result !== "number" || result < 0 || result > 3) ||
(findPreviousType !== "undefined" && findPreviousType !== "boolean")) {
return;
}
// Allow the `matchesCount` property to be optional, and ensure that
// it's valid before including it in the data sent to the findbar.
let matchesCount = null;
if (isValidMatchesCount(data.matchesCount)) {
matchesCount = data.matchesCount;
}
var winmm = this.domWindow.docShell.messageManager;
winmm.sendAsyncMessage("PDFJS:Parent:updateControlState", {
result, findPrevious, matchesCount,
});
}
updateFindMatchesCount(data) {
if (!this.supportsIntegratedFind()) {
return;
}
// Verify what we're sending to the findbar.
if (!isValidMatchesCount(data)) {
return;
}
const winmm = this.domWindow.docShell.messageManager;
winmm.sendAsyncMessage("PDFJS:Parent:updateMatchesCount", data);
}
setPreferences(prefs, sendResponse) {
var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + ".");
var numberOfPrefs = 0;
var prefValue, prefName;
for (var key in prefs) {
if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
log("setPreferences - Exceeded the maximum number of preferences " +
"that is allowed to be set at once.");
break;
} else if (!defaultBranch.getPrefType(key)) {
continue;
}
prefValue = prefs[key];
prefName = (PREF_PREFIX + "." + key);
switch (typeof prefValue) {
case "boolean":
PdfjsContentUtils.setBoolPref(prefName, prefValue);
break;
case "number":
PdfjsContentUtils.setIntPref(prefName, prefValue);
break;
case "string":
if (prefValue.length > MAX_STRING_PREF_LENGTH) {
log("setPreferences - Exceeded the maximum allowed length " +
"for a string preference.");
} else {
PdfjsContentUtils.setStringPref(prefName, prefValue);
}
break;
}
}
if (sendResponse) {
sendResponse(true);
}
}
getPreferences(prefs, sendResponse) {
var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + ".");
var currentPrefs = {}, numberOfPrefs = 0;
var prefValue, prefName;
for (var key in prefs) {
if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
log("getPreferences - Exceeded the maximum number of preferences " +
"that is allowed to be fetched at once.");
break;
} else if (!defaultBranch.getPrefType(key)) {
continue;
}
prefValue = prefs[key];
prefName = (PREF_PREFIX + "." + key);
switch (typeof prefValue) {
case "boolean":
currentPrefs[key] = getBoolPref(prefName, prefValue);
break;
case "number":
currentPrefs[key] = getIntPref(prefName, prefValue);
break;
case "string":
currentPrefs[key] = getStringPref(prefName, prefValue);
break;
}
}
let result = JSON.stringify(currentPrefs);
if (sendResponse) {
sendResponse(result);
}
return result;
}
}
/**
* This is for range requests.
*/
class RangedChromeActions extends ChromeActions {
constructor(domWindow, contentDispositionFilename, originalRequest,
rangeEnabled, streamingEnabled, dataListener) {
super(domWindow, contentDispositionFilename);
this.dataListener = dataListener;
this.originalRequest = originalRequest;
this.rangeEnabled = rangeEnabled;
this.streamingEnabled = streamingEnabled;
this.pdfUrl = originalRequest.URI.spec;
this.contentLength = originalRequest.contentLength;
// Pass all the headers from the original request through
var httpHeaderVisitor = {
headers: {},
visitHeader(aHeader, aValue) {
if (aHeader === "Range") {
// When loading the PDF from cache, firefox seems to set the Range
// request header to fetch only the unfetched portions of the file
// (e.g. 'Range: bytes=1024-'). However, we want to set this header
// manually to fetch the PDF in chunks.
return;
}
this.headers[aHeader] = aValue;
},
};
if (originalRequest.visitRequestHeaders) {
originalRequest.visitRequestHeaders(httpHeaderVisitor);
}
var self = this;
var xhr_onreadystatechange = function xhr_onreadystatechange() {
if (this.readyState === 1) { // LOADING
var netChannel = this.channel;
if ("nsIPrivateBrowsingChannel" in Ci &&
netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
var docIsPrivate = self.isInPrivateBrowsing();
netChannel.setPrivate(docIsPrivate);
}
}
};
var getXhr = function getXhr() {
var xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", xhr_onreadystatechange);
return xhr;
};
this.networkManager = new NetworkManager(this.pdfUrl, {
httpHeaders: httpHeaderVisitor.headers,
getXhr,
});
// If we are in range request mode, this means we manually issued xhr
// requests, which we need to abort when we leave the page
domWindow.addEventListener("unload", function unload(e) {
domWindow.removeEventListener(e.type, unload);
self.abortLoading();
});
}
initPassiveLoading() {
let data, done;
if (!this.streamingEnabled) {
this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
this.originalRequest = null;
data = this.dataListener.readData();
done = this.dataListener.isDone;
this.dataListener = null;
} else {
data = this.dataListener.readData();
done = this.dataListener.isDone;
this.dataListener.onprogress = (loaded, total) => {
this.domWindow.postMessage({
pdfjsLoadAction: "progressiveRead",
loaded,
total,
chunk: this.dataListener.readData(),
}, PDF_VIEWER_ORIGIN);
};
this.dataListener.oncomplete = () => {
if (!done && this.dataListener.isDone) {
this.domWindow.postMessage({
pdfjsLoadAction: "progressiveDone",
}, PDF_VIEWER_ORIGIN);
}
this.dataListener = null;
};
}
this.domWindow.postMessage({
pdfjsLoadAction: "supportsRangedLoading",
rangeEnabled: this.rangeEnabled,
streamingEnabled: this.streamingEnabled,
pdfUrl: this.pdfUrl,
length: this.contentLength,
data,
done,
}, PDF_VIEWER_ORIGIN);
return true;
}
requestDataRange(args) {
if (!this.rangeEnabled) {
return;
}
var begin = args.begin;
var end = args.end;
var domWindow = this.domWindow;
// TODO(mack): Support error handler. We're not currently not handling
// errors from chrome code for non-range requests, so this doesn't
// seem high-pri
this.networkManager.requestRange(begin, end, {
onDone: function RangedChromeActions_onDone(aArgs) {
domWindow.postMessage({
pdfjsLoadAction: "range",
begin: aArgs.begin,
chunk: aArgs.chunk,
}, PDF_VIEWER_ORIGIN);
},
onProgress: function RangedChromeActions_onProgress(evt) {
domWindow.postMessage({
pdfjsLoadAction: "rangeProgress",
loaded: evt.loaded,
}, PDF_VIEWER_ORIGIN);
},
});
}
abortLoading() {
this.networkManager.abortAllRequests();
if (this.originalRequest) {
this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
this.originalRequest = null;
}
this.dataListener = null;
}
}
/**
* This is for a single network stream.
*/
class StandardChromeActions extends ChromeActions {
constructor(domWindow, contentDispositionFilename, originalRequest,
dataListener) {
super(domWindow, contentDispositionFilename);
this.originalRequest = originalRequest;
this.dataListener = dataListener;
}
initPassiveLoading() {
if (!this.dataListener) {
return false;
}
this.dataListener.onprogress = (loaded, total) => {
this.domWindow.postMessage({
pdfjsLoadAction: "progress",
loaded,
total,
}, PDF_VIEWER_ORIGIN);
};
this.dataListener.oncomplete = (data, errorCode) => {
this.domWindow.postMessage({
pdfjsLoadAction: "complete",
data,
errorCode,
}, PDF_VIEWER_ORIGIN);
this.dataListener = null;
this.originalRequest = null;
};
return true;
}
abortLoading() {
if (this.originalRequest) {
this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
this.originalRequest = null;
}
this.dataListener = null;
}
}
/**
* Event listener to trigger chrome privileged code.
*/
class RequestListener {
constructor(actions) {
this.actions = actions;
}
// Receive an event and synchronously or asynchronously responds.
receive(event) {
var message = event.target;
var doc = message.ownerDocument;
var action = event.detail.action;
var data = event.detail.data;
var sync = event.detail.sync;
var actions = this.actions;
if (!(action in actions)) {
log("Unknown action: " + action);
return;
}
var response;
if (sync) {
response = actions[action].call(this.actions, data);
event.detail.response = Cu.cloneInto(response, doc.defaultView);
} else {
if (!event.detail.responseExpected) {
doc.documentElement.removeChild(message);
response = null;
} else {
response = function sendResponse(aResponse) {
try {
var listener = doc.createEvent("CustomEvent");
let detail = Cu.cloneInto({ response: aResponse },
doc.defaultView);
listener.initCustomEvent("pdf.js.response", true, false, detail);
return message.dispatchEvent(listener);
} catch (e) {
// doc is no longer accessible because the requestor is already
// gone. unloaded content cannot receive the response anyway.
return false;
}
};
}
actions[action].call(this.actions, data, response);
}
}
}
/**
* Forwards events from the eventElement to the contentWindow only if the
* content window matches the currently selected browser window.
*/
class FindEventManager {
constructor(contentWindow) {
this.contentWindow = contentWindow;
this.winmm = contentWindow.docShell.messageManager;
}
bind() {
this.contentWindow.addEventListener("unload", (evt) => {
this.unbind();
}, {once: true});
// We cannot directly attach listeners to for the find events
// since the FindBar is in the parent process. Instead we're
// asking the PdfjsChromeUtils to do it for us and forward
// all the find events to us.
this.winmm.sendAsyncMessage("PDFJS:Parent:addEventListener");
this.winmm.addMessageListener("PDFJS:Child:handleEvent", this);
}
receiveMessage(msg) {
var detail = msg.data.detail;
var type = msg.data.type;
var contentWindow = this.contentWindow;
detail = Cu.cloneInto(detail, contentWindow);
var forward = contentWindow.document.createEvent("CustomEvent");
forward.initCustomEvent(type, true, true, detail);
contentWindow.dispatchEvent(forward);
}
unbind() {
this.winmm.sendAsyncMessage("PDFJS:Parent:removeEventListener");
this.winmm.removeMessageListener("PDFJS:Child:handleEvent", this);
}
}
function PdfStreamConverter() {
}
PdfStreamConverter.prototype = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIStreamConverter, Ci.nsIStreamListener, Ci.nsIRequestObserver]),
/*
* This component works as such:
* 1. asyncConvertData stores the listener
* 2. onStartRequest creates a new channel, streams the viewer
* 3. If range requests are supported:
* 3.1. Leave the request open until the viewer is ready to switch to
* range requests.
*
* If range rquests are not supported:
* 3.1. Read the stream as it's loaded in onDataAvailable to send
* to the viewer
*
* The convert function just returns the stream, it's just the synchronous
* version of asyncConvertData.
*/
// nsIStreamConverter::convert
convert(aFromStream, aFromType, aToType, aCtxt) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
// nsIStreamConverter::asyncConvertData
asyncConvertData(aFromType, aToType, aListener, aCtxt) {
// Store the listener passed to us
this.listener = aListener;
},
// nsIStreamListener::onDataAvailable
onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
if (!this.dataListener) {
return;
}
var binaryStream = this.binaryStream;
binaryStream.setInputStream(aInputStream);
let chunk = new ArrayBuffer(aCount);
binaryStream.readArrayBuffer(aCount, chunk);
this.dataListener.append(new Uint8Array(chunk));
},
// nsIRequestObserver::onStartRequest
onStartRequest(aRequest) {
// Setup the request so we can use it below.
var isHttpRequest = false;
try {
aRequest.QueryInterface(Ci.nsIHttpChannel);
isHttpRequest = true;
} catch (e) {}
var rangeRequest = false;
var streamRequest = false;
if (isHttpRequest) {
var contentEncoding = "identity";
try {
contentEncoding = aRequest.getResponseHeader("Content-Encoding");
} catch (e) {}
var acceptRanges;
try {
acceptRanges = aRequest.getResponseHeader("Accept-Ranges");
} catch (e) {}
var hash = aRequest.URI.ref;
var isPDFBugEnabled = getBoolPref(PREF_PREFIX + ".pdfBugEnabled", false);
rangeRequest = contentEncoding === "identity" &&
acceptRanges === "bytes" &&
aRequest.contentLength >= 0 &&
!getBoolPref(PREF_PREFIX + ".disableRange", false) &&
(!isPDFBugEnabled ||
!hash.toLowerCase().includes("disablerange=true"));
streamRequest = contentEncoding === "identity" &&
aRequest.contentLength >= 0 &&
!getBoolPref(PREF_PREFIX + ".disableStream", false) &&
(!isPDFBugEnabled ||
!hash.toLowerCase().includes("disablestream=true"));
}
aRequest.QueryInterface(Ci.nsIChannel);
aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
var contentDispositionFilename;
try {
contentDispositionFilename = aRequest.contentDispositionFilename;
} catch (e) {}
// Change the content type so we don't get stuck in a loop.
aRequest.setProperty("contentType", aRequest.contentType);
aRequest.contentType = "text/html";
if (isHttpRequest) {
// We trust PDF viewer, using no CSP
aRequest.setResponseHeader("Content-Security-Policy", "", false);
aRequest.setResponseHeader("Content-Security-Policy-Report-Only", "",
false);
// The viewer does not need to handle HTTP Refresh header.
aRequest.setResponseHeader("Refresh", "", false);
}
PdfJsTelemetry.onViewerIsUsed();
PdfJsTelemetry.onDocumentSize(aRequest.contentLength);
// Creating storage for PDF data
var contentLength = aRequest.contentLength;
this.dataListener = new PdfDataListener(contentLength);
this.binaryStream = Cc["@mozilla.org/binaryinputstream;1"]
.createInstance(Ci.nsIBinaryInputStream);
// Create a new channel that is viewer loaded as a resource.
var channel = NetUtil.newChannel({
uri: PDF_VIEWER_WEB_PAGE,
loadUsingSystemPrincipal: true,
});
var listener = this.listener;
var dataListener = this.dataListener;
// Proxy all the request observer calls, when it gets to onStopRequest
// we can get the dom window. We also intentionally pass on the original
// request(aRequest) below so we don't overwrite the original channel and
// trigger an assertion.
var proxy = {
onStartRequest(request) {
listener.onStartRequest(aRequest);
},
onDataAvailable(request, inputStream, offset, count) {
listener.onDataAvailable(aRequest, inputStream,
offset, count);
},
onStopRequest(request, statusCode) {
var domWindow = getDOMWindow(channel, resourcePrincipal);
if (!Components.isSuccessCode(statusCode) || !domWindow) {
// The request may have been aborted and the document may have been
// replaced with something that is not PDF.js, abort attaching.
listener.onStopRequest(aRequest, statusCode);
return;
}
var actions;
if (rangeRequest || streamRequest) {
actions = new RangedChromeActions(
domWindow, contentDispositionFilename, aRequest,
rangeRequest, streamRequest, dataListener);
} else {
actions = new StandardChromeActions(
domWindow, contentDispositionFilename, aRequest, dataListener);
}
var requestListener = new RequestListener(actions);
domWindow.document.addEventListener(PDFJS_EVENT_ID, function(event) {
requestListener.receive(event);
}, false, true);
if (actions.supportsIntegratedFind()) {
var findEventManager = new FindEventManager(domWindow);
findEventManager.bind();
}
listener.onStopRequest(aRequest, statusCode);
if (domWindow.frameElement) {
var isObjectEmbed = domWindow.frameElement.tagName !== "IFRAME" ||
domWindow.frameElement.className === "previewPluginContentFrame";
PdfJsTelemetry.onEmbed(isObjectEmbed);
}
},
};
// Keep the URL the same so the browser sees it as the same.
channel.originalURI = aRequest.URI;
channel.loadGroup = aRequest.loadGroup;
channel.loadInfo.originAttributes = aRequest.loadInfo.originAttributes;
// We can use the resource principal when data is fetched by the chrome,
// e.g. useful for NoScript. Make make sure we reuse the origin attributes
// from the request channel to keep isolation consistent.
var uri = NetUtil.newURI(PDF_VIEWER_WEB_PAGE);
var resourcePrincipal =
Services.scriptSecurityManager.createCodebasePrincipal(uri,
aRequest.loadInfo.originAttributes);
aRequest.owner = resourcePrincipal;
channel.asyncOpen(proxy);
},
// nsIRequestObserver::onStopRequest
onStopRequest(aRequest, aStatusCode) {
if (!this.dataListener) {
// Do nothing
return;
}
if (Components.isSuccessCode(aStatusCode)) {
this.dataListener.finish();
} else {
this.dataListener.error(aStatusCode);
}
delete this.dataListener;
delete this.binaryStream;
},
};