browser(firefox): support downloads (#1683)

This commit is contained in:
Yury Semikhatsky 2020-04-06 23:34:30 -07:00 committed by GitHub
parent 889cf8f7b6
commit 7b2736b4c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 346 additions and 37 deletions

View File

@ -1 +1 @@
1072 1073

View File

@ -1148,10 +1148,10 @@ index 25c5b01fc54c8d45da8ceb7cf6ba163bee3c5361..490c5ce49cd9b5f804df59abbfb0450f
void internalResyncICUDefaultTimeZone(); void internalResyncICUDefaultTimeZone();
diff --git a/juggler/Helper.js b/juggler/Helper.js diff --git a/juggler/Helper.js b/juggler/Helper.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..862c680198bbb503a5f04c19bdb8fdf2cd8c9cef index 0000000000000000000000000000000000000000..b8e6649fb91be6cd72b000426fb4d58216745c4f
--- /dev/null --- /dev/null
+++ b/juggler/Helper.js +++ b/juggler/Helper.js
@@ -0,0 +1,102 @@ @@ -0,0 +1,115 @@
+const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); +const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ +
@ -1195,6 +1195,19 @@ index 0000000000000000000000000000000000000000..862c680198bbb503a5f04c19bdb8fdf2
+ return string.substring(1, string.length - 1); + return string.substring(1, string.length - 1);
+ } + }
+ +
+ getLoadContext(httpChannel) {
+ let loadContext = null;
+ try {
+ if (httpChannel.notificationCallbacks)
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) {}
+ try {
+ if (!loadContext && httpChannel.loadGroup)
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) { }
+ return loadContext;
+ }
+
+ getNetworkErrorStatusText(status) { + getNetworkErrorStatusText(status) {
+ if (!status) + if (!status)
+ return null; + return null;
@ -1256,10 +1269,10 @@ index 0000000000000000000000000000000000000000..862c680198bbb503a5f04c19bdb8fdf2
+ +
diff --git a/juggler/NetworkObserver.js b/juggler/NetworkObserver.js diff --git a/juggler/NetworkObserver.js b/juggler/NetworkObserver.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..2e3a2c60b5c5052a85ad1a6712d46587fe00838b index 0000000000000000000000000000000000000000..052f893eb0e984914ac59f8cb24580449dc12a66
--- /dev/null --- /dev/null
+++ b/juggler/NetworkObserver.js +++ b/juggler/NetworkObserver.js
@@ -0,0 +1,768 @@ @@ -0,0 +1,760 @@
+"use strict"; +"use strict";
+ +
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
@ -1462,15 +1475,7 @@ index 0000000000000000000000000000000000000000..2e3a2c60b5c5052a85ad1a6712d46587
+ } + }
+ +
+ _getBrowserForChannel(httpChannel) { + _getBrowserForChannel(httpChannel) {
+ let loadContext = null; + let loadContext = helper.getLoadContext(httpChannel);
+ try {
+ if (httpChannel.notificationCallbacks)
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) {}
+ try {
+ if (!loadContext && httpChannel.loadGroup)
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) { }
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) + if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
+ return; + return;
+ return loadContext.topFrameElement; + return loadContext.topFrameElement;
@ -2166,10 +2171,10 @@ index 0000000000000000000000000000000000000000..ba34976ad05e7f5f1a99777f76ac08b1
+this.SimpleChannel = SimpleChannel; +this.SimpleChannel = SimpleChannel;
diff --git a/juggler/TargetRegistry.js b/juggler/TargetRegistry.js diff --git a/juggler/TargetRegistry.js b/juggler/TargetRegistry.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..345aa8b0ebbd9d1e8c9c79913bd7ec3fbb1cc768 index 0000000000000000000000000000000000000000..27cfe133cab5dc4b1218c0e5624b7eb2f8bc08e3
--- /dev/null --- /dev/null
+++ b/juggler/TargetRegistry.js +++ b/juggler/TargetRegistry.js
@@ -0,0 +1,561 @@ @@ -0,0 +1,661 @@
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
+const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
@ -2197,6 +2202,88 @@ index 0000000000000000000000000000000000000000..345aa8b0ebbd9d1e8c9c79913bd7ec3f
+ 'desktop-notification', + 'desktop-notification',
+]; +];
+ +
+class DownloadInterceptor {
+ constructor(registry) {
+ this._registry = registry
+ this._handlerToUuid = new Map();
+ helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request');
+ }
+
+ _onRequest(httpChannel, topic) {
+ let loadContext = helper.getLoadContext(httpChannel);
+ if (!loadContext)
+ return;
+ if (!loadContext.topFrameElement)
+ return;
+ const target = this._registry.targetForBrowser(loadContext.topFrameElement);
+ if (!target)
+ return;
+ target._httpChannelIds.add(httpChannel.channelId);
+ }
+
+ //
+ // nsIDownloadInterceptor implementation.
+ //
+ interceptDownloadRequest(externalAppHandler, request, outFile) {
+ const httpChannel = request.QueryInterface(Ci.nsIHttpChannel);
+ if (!httpChannel)
+ return false;
+ if (!httpChannel.loadInfo)
+ return false;
+ const userContextId = httpChannel.loadInfo.originAttributes.userContextId;
+ const browserContext = this._registry._userContextIdToBrowserContext.get(userContextId);
+ const options = browserContext.options.downloadOptions;
+ if (!options)
+ return false;
+
+ const pageTarget = this._registry._targetForChannel(httpChannel);
+ if (!pageTarget)
+ return false;
+
+ const uuid = helper.generateId();
+ let file = null;
+ if (options.behavior === 'saveToDisk') {
+ file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(options.downloadsDir);
+ file.append(uuid);
+
+ try {
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ } catch (e) {
+ dump(`interceptDownloadRequest failed to create file: ${e}\n`);
+ return false;
+ }
+ }
+ outFile.value = file;
+ this._handlerToUuid.set(externalAppHandler, uuid);
+ const downloadInfo = {
+ uuid,
+ browserContextId: browserContext.browserContextId,
+ pageTargetId: pageTarget.id(),
+ url: httpChannel.URI.spec,
+ suggestedFileName: externalAppHandler.suggestedFileName,
+ };
+ this._registry.emit(TargetRegistry.Events.DownloadCreated, downloadInfo);
+ return true;
+ }
+
+ onDownloadComplete(externalAppHandler, canceled, errorName) {
+ const uuid = this._handlerToUuid.get(externalAppHandler);
+ if (!uuid)
+ return;
+ this._handlerToUuid.delete(externalAppHandler);
+ const downloadInfo = {
+ uuid,
+ };
+ if (errorName === 'NS_BINDING_ABORTED') {
+ downloadInfo.canceled = true;
+ } else {
+ downloadInfo.error = errorName;
+ }
+ this._registry.emit(TargetRegistry.Events.DownloadFinished, downloadInfo);
+ }
+}
+
+class TargetRegistry { +class TargetRegistry {
+ constructor() { + constructor() {
+ EventEmitter.decorate(this); + EventEmitter.decorate(this);
@ -2322,6 +2409,9 @@ index 0000000000000000000000000000000000000000..345aa8b0ebbd9d1e8c9c79913bd7ec3f
+ onTabCloseListener({ target: tab }); + onTabCloseListener({ target: tab });
+ }, + },
+ }); + });
+
+ const extHelperAppSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].getService(Ci.nsIExternalHelperAppService);
+ extHelperAppSvc.setDownloadInterceptor(new DownloadInterceptor(this));
+ } + }
+ +
+ defaultContext() { + defaultContext() {
@ -2395,6 +2485,18 @@ index 0000000000000000000000000000000000000000..345aa8b0ebbd9d1e8c9c79913bd7ec3f
+ targetForBrowser(browser) { + targetForBrowser(browser) {
+ return this._browserToTarget.get(browser); + return this._browserToTarget.get(browser);
+ } + }
+
+ _targetForChannel(httpChannel) {
+ let loadContext = helper.getLoadContext(httpChannel);
+ if (loadContext)
+ return this.targetForBrowser(loadContext.topFrameElement);
+ const channelId = httpChannel.channelId;
+ for (const target of this._browserToTarget.values()) {
+ if (target._httpChannelIds.has(channelId))
+ return target;
+ }
+ return null;
+ }
+} +}
+ +
+class PageTarget { +class PageTarget {
@ -2410,6 +2512,7 @@ index 0000000000000000000000000000000000000000..345aa8b0ebbd9d1e8c9c79913bd7ec3f
+ this._url = ''; + this._url = '';
+ this._openerId = opener ? opener.id() : undefined; + this._openerId = opener ? opener.id() : undefined;
+ this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager); + this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager);
+ this._httpChannelIds = new Set();
+ +
+ const navigationListener = { + const navigationListener = {
+ QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]), + QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]),
@ -2727,6 +2830,8 @@ index 0000000000000000000000000000000000000000..345aa8b0ebbd9d1e8c9c79913bd7ec3f
+TargetRegistry.Events = { +TargetRegistry.Events = {
+ TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'), + TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
+ TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'), + TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
+ DownloadCreated: Symbol('TargetRegistry.Events.DownloadCreated'),
+ DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'),
+}; +};
+ +
+var EXPORTED_SYMBOLS = ['TargetRegistry']; +var EXPORTED_SYMBOLS = ['TargetRegistry'];
@ -3316,10 +3421,10 @@ index 0000000000000000000000000000000000000000..5a1df2837d70531a670163b7c8601088
+ +
diff --git a/juggler/content/NetworkMonitor.js b/juggler/content/NetworkMonitor.js diff --git a/juggler/content/NetworkMonitor.js b/juggler/content/NetworkMonitor.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..be70ea364f9534bb3b344f64970366c32e8c11be index 0000000000000000000000000000000000000000..155d0770ddf704728829272a41a31ce8c9509a25
--- /dev/null --- /dev/null
+++ b/juggler/content/NetworkMonitor.js +++ b/juggler/content/NetworkMonitor.js
@@ -0,0 +1,62 @@ @@ -0,0 +1,48 @@
+"use strict"; +"use strict";
+const Ci = Components.interfaces; +const Ci = Components.interfaces;
+const Cr = Components.results; +const Cr = Components.results;
@ -3343,7 +3448,7 @@ index 0000000000000000000000000000000000000000..be70ea364f9534bb3b344f64970366c3
+ if (!(channel instanceof Ci.nsIHttpChannel)) + if (!(channel instanceof Ci.nsIHttpChannel))
+ return; + return;
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ const loadContext = getLoadContext(httpChannel); + const loadContext = helper.getLoadContext(httpChannel);
+ if (!loadContext) + if (!loadContext)
+ return; + return;
+ const window = loadContext.associatedWindow; + const window = loadContext.associatedWindow;
@ -3365,20 +3470,6 @@ index 0000000000000000000000000000000000000000..be70ea364f9534bb3b344f64970366c3
+ } + }
+} +}
+ +
+function getLoadContext(httpChannel) {
+ let loadContext = null;
+ try {
+ if (httpChannel.notificationCallbacks)
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) {}
+ try {
+ if (!loadContext && httpChannel.loadGroup)
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) { }
+ return loadContext;
+}
+
+
+var EXPORTED_SYMBOLS = ['NetworkMonitor']; +var EXPORTED_SYMBOLS = ['NetworkMonitor'];
+this.NetworkMonitor = NetworkMonitor; +this.NetworkMonitor = NetworkMonitor;
+ +
@ -5362,10 +5453,10 @@ index 0000000000000000000000000000000000000000..bf37558bccc48f4d90eadc971c1eb3e4
+this.AccessibilityHandler = AccessibilityHandler; +this.AccessibilityHandler = AccessibilityHandler;
diff --git a/juggler/protocol/BrowserHandler.js b/juggler/protocol/BrowserHandler.js diff --git a/juggler/protocol/BrowserHandler.js b/juggler/protocol/BrowserHandler.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..97e88dd582090971d122064b8a131096a317b6be index 0000000000000000000000000000000000000000..21ea3ef1451d02b36f884cf3c6ef3df81bd1a9b9
--- /dev/null --- /dev/null
+++ b/juggler/protocol/BrowserHandler.js +++ b/juggler/protocol/BrowserHandler.js
@@ -0,0 +1,189 @@ @@ -0,0 +1,199 @@
+"use strict"; +"use strict";
+ +
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
@ -5408,6 +5499,8 @@ index 0000000000000000000000000000000000000000..97e88dd582090971d122064b8a131096
+ this._eventListeners = [ + this._eventListeners = [
+ helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)), + helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)),
+ helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)), + helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
+ helper.on(this._targetRegistry, TargetRegistry.Events.DownloadCreated, this._onDownloadCreated.bind(this)),
+ helper.on(this._targetRegistry, TargetRegistry.Events.DownloadFinished, this._onDownloadFinished.bind(this)),
+ ]; + ];
+ } + }
+ +
@ -5473,6 +5566,14 @@ index 0000000000000000000000000000000000000000..97e88dd582090971d122064b8a131096
+ }); + });
+ } + }
+ +
+ _onDownloadCreated(downloadInfo) {
+ this._session.emitEvent('Browser.downloadCreated', downloadInfo);
+ }
+
+ _onDownloadFinished(downloadInfo) {
+ this._session.emitEvent('Browser.downloadFinished', downloadInfo);
+ }
+
+ async newPage({browserContextId}) { + async newPage({browserContextId}) {
+ const targetId = await this._targetRegistry.newPage({browserContextId}); + const targetId = await this._targetRegistry.newPage({browserContextId});
+ return {targetId}; + return {targetId};
@ -6361,10 +6462,10 @@ index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07
+this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; +this.EXPORTED_SYMBOLS = ['t', 'checkScheme'];
diff --git a/juggler/protocol/Protocol.js b/juggler/protocol/Protocol.js diff --git a/juggler/protocol/Protocol.js b/juggler/protocol/Protocol.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..6e187212618130bc716a0fd0121ed0dd23d35770 index 0000000000000000000000000000000000000000..ae13d7ad1ce2a9776121ffcfcf7e68c5118e6e5c
--- /dev/null --- /dev/null
+++ b/juggler/protocol/Protocol.js +++ b/juggler/protocol/Protocol.js
@@ -0,0 +1,781 @@ @@ -0,0 +1,800 @@
+const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); +const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js');
+ +
+// Protocol-specific types. +// Protocol-specific types.
@ -6409,6 +6510,11 @@ index 0000000000000000000000000000000000000000..6e187212618130bc716a0fd0121ed0dd
+ accuracy: t.Optional(t.Number), + accuracy: t.Optional(t.Number),
+}; +};
+ +
+browserTypes.DownloadOptions = {
+ behavior: t.Optional(t.Enum(['saveToDisk', 'cancel'])),
+ downloadsDir: t.Optional(t.String),
+};
+
+const pageTypes = {}; +const pageTypes = {};
+pageTypes.DOMPoint = { +pageTypes.DOMPoint = {
+ x: t.Number, + x: t.Number,
@ -6550,6 +6656,7 @@ index 0000000000000000000000000000000000000000..6e187212618130bc716a0fd0121ed0dd
+ validTo: t.Number, + validTo: t.Number,
+}; +};
+ +
+
+const Browser = { +const Browser = {
+ targets: ['browser'], + targets: ['browser'],
+ +
@ -6564,6 +6671,18 @@ index 0000000000000000000000000000000000000000..6e187212618130bc716a0fd0121ed0dd
+ sessionId: t.String, + sessionId: t.String,
+ targetId: t.String, + targetId: t.String,
+ }, + },
+ 'downloadCreated': {
+ uuid: t.String,
+ browserContextId: t.String,
+ pageTargetId: t.String,
+ url: t.String,
+ suggestedFileName: t.String,
+ },
+ 'downloadFinished': {
+ uuid: t.String,
+ canceled: t.Optional(t.Boolean),
+ error: t.Optional(t.String),
+ },
+ }, + },
+ +
+ methods: { + methods: {
@ -6582,6 +6701,7 @@ index 0000000000000000000000000000000000000000..6e187212618130bc716a0fd0121ed0dd
+ viewport: t.Optional(pageTypes.Viewport), + viewport: t.Optional(pageTypes.Viewport),
+ locale: t.Optional(t.String), + locale: t.Optional(t.String),
+ timezoneId: t.Optional(t.String), + timezoneId: t.Optional(t.String),
+ downloadOptions: t.Optional(browserTypes.DownloadOptions),
+ }, + },
+ returns: { + returns: {
+ browserContextId: t.String, + browserContextId: t.String,
@ -7515,3 +7635,192 @@ index 87701f8d2cfee8bd84acd28c62b3be4989c9474c..ae1aa85c019cb21d4f7e79c35e8afe72
+ in nsIURI aLocation, + in nsIURI aLocation,
+ [optional] in unsigned long aFlags); + [optional] in unsigned long aFlags);
}; };
diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp
index a9a0f395bd6afcb995b786f83f1ba56ff0d56b00..b134791c28101ed4fd34004e23ee674691333964 100644
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -100,6 +100,7 @@
#include "mozilla/Components.h"
#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ErrorNames.h"
#include "mozilla/Preferences.h"
#include "mozilla/ipc/URIUtils.h"
@@ -841,6 +842,12 @@ NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
return NS_OK;
}
+NS_IMETHODIMP nsExternalHelperAppService::SetDownloadInterceptor(
+ nsIDownloadInterceptor* interceptor) {
+ mInterceptor = interceptor;
+ return NS_OK;
+}
+
nsresult nsExternalHelperAppService::GetFileTokenForPath(
const char16_t* aPlatformAppPath, nsIFile** aFile) {
nsDependentString platformAppPath(aPlatformAppPath);
@@ -1407,7 +1414,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
// Strip off the ".part" from mTempLeafName
mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
+ return CreateSaverForTempFile();
+}
+
+nsresult nsExternalAppHandler::CreateSaverForTempFile() {
MOZ_ASSERT(!mSaver, "Output file initialization called more than once!");
+ nsresult rv;
mSaver =
do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
@@ -1567,7 +1579,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
return NS_OK;
}
- rv = SetUpTempFile(aChannel);
+ bool isIntercepted = false;
+ nsCOMPtr<nsIDownloadInterceptor> interceptor = mExtProtSvc->mInterceptor;
+ if (interceptor) {
+ nsCOMPtr<nsIFile> fileToUse;
+ rv = interceptor->InterceptDownloadRequest(this, request, getter_AddRefs(fileToUse), &isIntercepted);
+ if (!NS_SUCCEEDED(rv)) {
+ LOG((" failed to call nsIDowloadInterceptor.interceptDownloadRequest"));
+ return rv;
+ }
+ if (isIntercepted) {
+ LOG((" request interceped by nsIDowloadInterceptor"));
+ if (fileToUse) {
+ mTempFile = fileToUse;
+ rv = mTempFile->GetLeafName(mTempLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ Cancel(NS_BINDING_ABORTED);
+ return NS_OK;
+ }
+ }
+ }
+
+ // Temp file is the final destination when download is intercepted. In that
+ // case we only need to create saver (and not create transfer later). Not creating
+ // mTransfer also cuts off all downloads handling logic in the js compoenents and
+ // browser UI.
+ if (isIntercepted)
+ rv = CreateSaverForTempFile();
+ else
+ rv = SetUpTempFile(aChannel);
if (NS_FAILED(rv)) {
nsresult transferError = rv;
@@ -1615,6 +1656,11 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk);
nsAutoCString MIMEType;
mMimeInfo->GetMIMEType(MIMEType);
+
+ if (isIntercepted) {
+ return NS_OK;
+ }
+
if (alwaysAsk) {
// But we *don't* ask if this mimeInfo didn't come from
// our user configuration datastore and the user has said
@@ -2015,6 +2061,15 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
NotifyTransfer(aStatus);
}
+ if (!mCanceled) {
+ nsCOMPtr<nsIDownloadInterceptor> interceptor = mExtProtSvc->mInterceptor;
+ if (interceptor) {
+ nsCString noError;
+ nsresult rv = interceptor->OnDownloadComplete(this, noError);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to call nsIDowloadInterceptor.OnDownloadComplete");
+ }
+ }
+
return NS_OK;
}
@@ -2385,6 +2440,14 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
}
}
+ nsCOMPtr<nsIDownloadInterceptor> interceptor = mExtProtSvc->mInterceptor;
+ if (interceptor) {
+ nsCString errorName;
+ GetErrorName(aReason, errorName);
+ nsresult rv = interceptor->OnDownloadComplete(this, errorName);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed notify nsIDowloadInterceptor about cancel");
+ }
+
// Break our reference cycle with the helper app dialog (set up in
// OnStartRequest)
mDialog = nullptr;
diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h
index f3d18ce83858b9cb876091ea04fcf6f079f5471c..abb0be752aeceae3e396d65b92d0b3eac215a2e9 100644
--- a/uriloader/exthandler/nsExternalHelperAppService.h
+++ b/uriloader/exthandler/nsExternalHelperAppService.h
@@ -189,6 +189,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService,
mozilla::dom::BrowsingContext* aContentContext, bool aForceSave,
nsIInterfaceRequestor* aWindowContext,
nsIStreamListener** aStreamListener);
+
+ nsCOMPtr<nsIDownloadInterceptor> mInterceptor;
};
/**
@@ -366,6 +368,9 @@ class nsExternalAppHandler final : public nsIStreamListener,
* Upon successful return, both mTempFile and mSaver will be valid.
*/
nsresult SetUpTempFile(nsIChannel* aChannel);
+
+ nsresult CreateSaverForTempFile();
+
/**
* When we download a helper app, we are going to retarget all load
* notifications into our own docloader and load group instead of
diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl
index 8a55c1bd666c4f7a032863f1527a2315830643c5..c8bfff858079216798e0c71cc757e67466ad4ce1 100644
--- a/uriloader/exthandler/nsIExternalHelperAppService.idl
+++ b/uriloader/exthandler/nsIExternalHelperAppService.idl
@@ -6,6 +6,7 @@
#include "nsICancelable.idl"
+interface nsIHelperAppLauncher;
interface nsIURI;
interface nsIRequest;
interface nsIStreamListener;
@@ -20,6 +21,17 @@ webidl BrowsingContext;
class nsExternalAppHandler;
%}
+/**
+ * Interceptor interface used by Juggler.
+ */
+[scriptable, uuid(9a20e9b0-75d0-11ea-bc55-0242ac130003)]
+interface nsIDownloadInterceptor : nsISupports
+{
+ bool interceptDownloadRequest(in nsIHelperAppLauncher aHandler, in nsIRequest aRequest, out nsIFile file);
+
+ void onDownloadComplete(in nsIHelperAppLauncher aHandler, in ACString aErrorName);
+};
+
/**
* The external helper app service is used for finding and launching
* platform specific external applications for a given mime content type.
@@ -49,7 +61,7 @@ interface nsIExternalHelperAppService : nsISupports
in nsIInterfaceRequestor aContentContext,
in boolean aForceSave,
[optional] in nsIInterfaceRequestor aWindowContext);
-
+
/**
* Binds an external helper application to a stream listener. The caller
* should pump data into the returned stream listener. When the OnStopRequest
@@ -82,6 +94,7 @@ interface nsIExternalHelperAppService : nsISupports
boolean applyDecodingForExtension(in AUTF8String aExtension,
in ACString aEncodingType);
+ void setDownloadInterceptor(in nsIDownloadInterceptor interceptor);
};
/**