fx-compat: Fix PDF and style interception

This commit is contained in:
Abe Jellinek 2023-04-14 11:43:11 -04:00
parent 0612a9e6f5
commit 893ad2bf62
3 changed files with 128 additions and 130 deletions

View file

@ -1157,8 +1157,7 @@ Zotero.Attachments = new function(){
Zotero.debug(`downloadPDFViaBrowser: Sniffing a PDF loaded at ${name}`);
// try the browser
try {
channelBrowser = channel.notificationCallbacks.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell).chromeEventHandler;
channelBrowser = channel.notificationCallbacks.getInterface(Ci.nsILoadContext).topFrameElement;
}
catch (e) {}
if (channelBrowser) {
@ -1167,8 +1166,8 @@ Zotero.Attachments = new function(){
else {
// try the document for the load group
try {
channelBrowser = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell).chromeEventHandler;
channelBrowser = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext)
.topFrameElement;
}
catch(e) {}
if (channelBrowser) {
@ -1191,21 +1190,18 @@ Zotero.Attachments = new function(){
};
try {
Zotero.MIMETypeHandler.addHandlers("application/pdf", pdfMIMETypeHandler, true);
function noop() {};
hiddenBrowser = Zotero.HTTP.loadDocuments([url], noop, noop, noop, true, options.cookieSandbox);
hiddenBrowser = await HiddenBrowser.create(url, {
requireSuccessfulStatus: true,
cookieSandbox: options.cookieSandbox,
});
let onLoadTimeoutDeferred = Zotero.Promise.defer();
let currentUrl = "";
hiddenBrowser.addProgressListener({
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIWebProgressListener,
Components.interfaces.nsISupportsWeakReference]),
onProgressChange: noop,
onStateChange: noop,
onStatusChange: noop,
onSecurityChange: noop,
hiddenBrowser.webProgress.addProgressListener({
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
async onLocationChange() {
let url = hiddenBrowser.contentDocument.location.href;
let url = hiddenBrowser.currentURI.spec;
if (currentUrl) {
Zotero.debug(`downloadPDFViaBrowser: A JS redirect occurred to ${hiddenBrowser.contentDocument.location.href}`);
Zotero.debug(`downloadPDFViaBrowser: A JS redirect occurred to ${url}`);
}
currentUrl = url;
Zotero.debug(`downloadPDFViaBrowser: Page with potential JS redirect loaded, giving it ${onLoadTimeout}ms to process`);
@ -1215,7 +1211,7 @@ Zotero.Attachments = new function(){
onLoadTimeoutDeferred.reject(new Error(`downloadPDFViaBrowser: Loading PDF via browser timed out on the JS challenge page after ${onLoadTimeout}ms`));
}
}
});
}, Ci.nsIWebProgress.NOTIFY_LOCATION);
await Zotero.Promise.race([
onLoadTimeoutDeferred.promise,
Zotero.Promise.delay(downloadTimeout).then(() => {
@ -1238,7 +1234,7 @@ Zotero.Attachments = new function(){
finally {
Zotero.MIMETypeHandler.removeHandlers('application/pdf', pdfMIMETypeHandler);
if (hiddenBrowser) {
Zotero.Browser.deleteHiddenBrowser(hiddenBrowser);
HiddenBrowser.destroy(hiddenBrowser);
}
}
};
@ -2978,7 +2974,7 @@ Zotero.Attachments = new function(){
* Determines if a given document is an instance of PDFJS
* @return {Boolean}
*/
this.isPDFJS = function(doc) {
this.isPDFJSDocument = function(doc) {
// pdf.js HACK
// This may no longer be necessary (as of Fx 23)
if(doc.contentType === "text/html") {
@ -2992,6 +2988,16 @@ Zotero.Attachments = new function(){
}
return false;
}
/**
* Determines if a given Browser is displaying an instance of PDFJS
* @return {Boolean}
*/
this.isPDFJSBrowser = function (browser) {
// https://searchfox.org/mozilla-esr102/rev/f78d456e055a41106be086c501b271385a973961/browser/base/content/browser.js#5518
return browser.contentPrincipal?.spec == "resource://pdf.js/web/viewer.html";
};
this.linkModeToName = function (linkMode) {

View file

@ -23,18 +23,35 @@
***** END LICENSE BLOCK *****
*/
const ArrayBufferInputStream = Components.Constructor(
"@mozilla.org/io/arraybuffer-input-stream;1",
"nsIArrayBufferInputStream"
);
const BinaryInputStream = Components.Constructor(
"@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream"
);
const StorageStream = Components.Constructor(
"@mozilla.org/storagestream;1",
"nsIStorageStream",
"init"
);
const BufferedOutputStream = Components.Constructor(
"@mozilla.org/network/buffered-output-stream;1",
"nsIBufferedOutputStream",
"init"
);
Zotero.MIMETypeHandler = new function () {
var _typeHandlers, _ignoreContentDispositionTypes, _observers;
/**
* Registers URIContentListener to handle MIME types
* Registers nsIObserver to handle MIME types
*/
this.init = function() {
Zotero.debug("Registering URIContentListener");
// register our nsIURIContentListener and nsIObserver
Components.classes["@mozilla.org/uriloader;1"].
getService(Components.interfaces.nsIURILoader).
registerContentListener(_URIContentListener);
Zotero.debug("Registering nsIObserver");
// register our nsIObserver
Components.classes["@mozilla.org/observer-service;1"].
getService(Components.interfaces.nsIObserverService).
addObserver(_Observer, "http-on-examine-response", false);
@ -61,16 +78,16 @@ Zotero.MIMETypeHandler = new function () {
var data = await Zotero.Utilities.Internal.blobToText(blob);
try {
await Zotero.Styles.install(data, origin, true);
// Close styles page in basic viewer after installing a style
win?.close();
return true;
}
catch (e) {
Zotero.logError(e);
(new Zotero.Exception.Alert("styles.install.unexpectedError",
origin, "styles.install.title", e)).present();
}
// Close styles page in basic viewer after installing a style
if (win) {
win.close();
}
return false;
}
}, true);
};
@ -134,81 +151,64 @@ Zotero.MIMETypeHandler = new function () {
/**
* Called to observe a page load
*/
var _Observer = new function() {
this.observe = function(channel) {
if(Zotero.isConnector) return;
var _Observer = {
observe(channel) {
channel.QueryInterface(Components.interfaces.nsIRequest);
if(channel.loadFlags & Components.interfaces.nsIHttpChannel.LOAD_DOCUMENT_URI) {
channel.QueryInterface(Components.interfaces.nsIHttpChannel);
try {
// remove content-disposition headers for EndNote, etc.
var contentType = channel.getResponseHeader("Content-Type").toLowerCase();
for (let handledType of _ignoreContentDispositionTypes) {
if (contentType.startsWith(handledType)) {
channel.setResponseHeader("Content-Disposition", "inline", false);
break;
}
// https://searchfox.org/mozilla-esr102/rev/f78d456e055a41106be086c501b271385a973961/netwerk/base/nsIChannel.idl#209-211
if (!channel.isDocument) {
return;
}
channel.QueryInterface(Components.interfaces.nsIHttpChannel);
channel.QueryInterface(Components.interfaces.nsITraceableChannel);
try {
// remove content-disposition headers for EndNote, etc.
var contentType = channel.getResponseHeader("Content-Type").toLowerCase();
for (let handledType of _ignoreContentDispositionTypes) {
if (contentType.startsWith(handledType)) {
channel.setResponseHeader("Content-Disposition", "inline", false);
break;
}
} catch(e) {}
for (let observer of _observers) {
}
}
catch (e) {
// getResponseHeader() throws if header is not set; ignore
}
try {
if (contentType && _typeHandlers[contentType]) {
// Replace listener entirely
// #setNewListener() contract wants us to pass through events to the original (eventually),
// but we will not
let originalListener = channel.setNewListener(new _StreamListener(channel, contentType));
// Make it look like the connection ended, so the original listener can clean up
originalListener.onStopRequest(channel, Cr.NS_BINDING_ABORTED);
}
}
catch (e) {
Zotero.logError(e);
}
for (let observer of _observers) {
try {
observer(channel);
}
catch (e) {
Zotero.logError(e);
}
}
}
}
var _URIContentListener = new function() {
/**
* Standard QI definition
*/
this.QueryInterface = function(iid) {
if (iid.equals(Components.interfaces.nsISupports)
|| iid.equals(Components.interfaces.nsISupportsWeakReference)
|| iid.equals(Components.interfaces.nsIURIContentListener)) {
return this;
}
throw Components.results.NS_ERROR_NO_INTERFACE;
}
/**
* Called to see if we can handle a content type
*/
this.canHandleContent = this.isPreferred = function(contentType, isContentPreferred, desiredContentType) {
if(Zotero.isConnector) return false;
return !!_typeHandlers[contentType.toLowerCase()];
}
/**
* Called to begin handling a content type
*/
this.doContent = function(contentType, isContentPreferred, request, contentHandler) {
Zotero.debug("MIMETypeHandler: handling "+contentType+" from " + request.name);
contentHandler.value = new _StreamListener(request, contentType.toLowerCase());
return false;
}
/**
* Called so that we could stop a load before it happened if we wanted to
*/
this.onStartURIOpen = function(URI) {
return true;
}
}
};
/**
* @class Implements nsIStreamListener and nsIRequestObserver interfaces to download MIME types
* @class _StreamListener Implements nsIStreamListener and nsIRequestObserver interfaces to download MIME types
* we've registered ourself as the handler for
* @param {nsIRequest} request The request to handle
* @param {String} contenType The content type being handled
*/
var _StreamListener = function(request, contentType) {
var _StreamListener = function (request, contentType) {
this._request = request;
this._contentType = contentType
this._storageStream = null;
this._outputStream = null;
this._binaryInputStream = null;
this._contentType = contentType;
}
/**
@ -223,10 +223,11 @@ Zotero.MIMETypeHandler = new function () {
throw Components.results.NS_ERROR_NO_INTERFACE;
}
/**
* Called when the request is started
*/
_StreamListener.prototype.onStartRequest = async function(channel) {
this._onStartRequestCalled = true;
this._dataBuffer = new StorageStream(4096, 0xffffffff);
this._stream = new BufferedOutputStream(this._dataBuffer.getOutputStream(0), 8192);
try {
if (!_typeHandlers[this._contentType]) return;
for (let handlers of _typeHandlers[this._contentType]) {
@ -245,44 +246,36 @@ Zotero.MIMETypeHandler = new function () {
Zotero.logError(e);
}
}
/**
* Called when there's data available; we collect this data and keep it until the request is
* done
*/
_StreamListener.prototype.onDataAvailable = function(request, inputStream, offset, count) {
Zotero.debug(count + " bytes available");
if (!this._storageStream) {
this._storageStream = Components.classes["@mozilla.org/storagestream;1"].
createInstance(Components.interfaces.nsIStorageStream);
this._storageStream.init(16384, 4294967295, null); // PR_UINT32_MAX
this._outputStream = this._storageStream.getOutputStream(0);
this._binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"].
createInstance(Components.interfaces.nsIBinaryInputStream);
this._binaryInputStream.setInputStream(inputStream);
_StreamListener.prototype.onDataAvailable = async function (channel, inputStream, offset, count) {
if (!this._onStartRequestCalled) {
await this.onStartRequest(channel);
}
var bytes = this._binaryInputStream.readBytes(count);
this._outputStream.write(bytes, count);
}
this._stream.writeFrom(inputStream, count);
};
/**
* Called when the request is done
*/
_StreamListener.prototype.onStopRequest = async function (channel, status) {
_StreamListener.prototype.onStopRequest = async function (channel, statusCode) {
if (!this._onStartRequestCalled) {
await this.onStartRequest(channel);
}
Zotero.debug("charset is " + channel.contentCharset);
var inputStream = this._storageStream.newInputStream(0);
var stream = Components.classes["@mozilla.org/binaryinputstream;1"]
.createInstance(Components.interfaces.nsIBinaryInputStream);
stream.setInputStream(inputStream);
let buffer = new ArrayBuffer(this._storageStream.length);
this._stream.close();
this._stream = null;
if (!Components.isSuccessCode(statusCode)) {
throw Components.Exception('Failed to load', statusCode);
}
let stream = new BinaryInputStream(this._dataBuffer.newInputStream(0));
let buffer = new ArrayBuffer(this._dataBuffer.length);
stream.readArrayBuffer(buffer.byteLength, buffer);
stream.close();
inputStream.close();
let blob = new (Zotero.getMainWindow()).Blob([buffer], { type: this._contentType });
let blob = new Blob([buffer], { type: this._contentType });
var handled = false;
try {
@ -306,26 +299,25 @@ Zotero.MIMETypeHandler = new function () {
Zotero.logError(e);
}
if (handled === false) {
if (!handled) {
// Handle using nsIExternalHelperAppService
let externalHelperAppService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]
.getService(Components.interfaces.nsIExternalHelperAppService);
let frontWindow = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
.getService(Components.interfaces.nsIWindowWatcher).activeWindow;
let inputStream = this._storageStream.newInputStream(0);
let inputStream = new ArrayBufferInputStream();
inputStream.setData(buffer, 0, buffer.byteLength);
let streamListener = externalHelperAppService.doContent(
this._contentType, this._request, frontWindow, null
);
if (streamListener) {
streamListener.onStartRequest(channel);
streamListener.onDataAvailable(
this._request, inputStream, 0, this._storageStream.length
this._request, inputStream, 0, buffer.byteLength
);
streamListener.onStopRequest(channel, status);
streamListener.onStopRequest(channel, statusCode);
}
}
this._storageStream.close();
};
}

View file

@ -4147,7 +4147,7 @@ var ZoteroPane = new function()
if (itemType == 'temporaryPDFHack') {
itemType = null;
var isPDF = false;
if (doc.title.indexOf('application/pdf') != -1 || Zotero.Attachments.isPDFJS(doc)
if (doc.title.indexOf('application/pdf') != -1 || Zotero.Attachments.isPDFJSDocument(doc)
|| doc.contentType == 'application/pdf') {
isPDF = true;
}