Skip to content

Commit

Permalink
feat(firefox): support downloads
Browse files Browse the repository at this point in the history
  • Loading branch information
yury-s committed Apr 7, 2020
1 parent cba9041 commit 58bfc95
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 26 deletions.
13 changes: 13 additions & 0 deletions juggler/Helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ class Helper {
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) {
if (!status)
return null;
Expand Down
10 changes: 1 addition & 9 deletions juggler/NetworkObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,7 @@ class NetworkObserver {
}

_getBrowserForChannel(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) { }
let loadContext = helper.getLoadContext(httpChannel);
if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
return;
return loadContext.topFrameElement;
Expand Down
100 changes: 100 additions & 0 deletions juggler/TargetRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,88 @@ const ALL_PERMISSIONS = [
'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 {
constructor() {
EventEmitter.decorate(this);
Expand Down Expand Up @@ -150,6 +232,9 @@ class TargetRegistry {
onTabCloseListener({ target: tab });
},
});

const extHelperAppSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].getService(Ci.nsIExternalHelperAppService);
extHelperAppSvc.setDownloadInterceptor(new DownloadInterceptor(this));
}

defaultContext() {
Expand Down Expand Up @@ -223,6 +308,18 @@ class TargetRegistry {
targetForBrowser(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 {
Expand All @@ -238,6 +335,7 @@ class PageTarget {
this._url = '';
this._openerId = opener ? opener.id() : undefined;
this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager);
this._httpChannelIds = new Set();

const navigationListener = {
QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]),
Expand Down Expand Up @@ -555,6 +653,8 @@ function setViewportSizeForBrowser(viewportSize, browser) {
TargetRegistry.Events = {
TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
DownloadCreated: Symbol('TargetRegistry.Events.DownloadCreated'),
DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'),
};

var EXPORTED_SYMBOLS = ['TargetRegistry'];
Expand Down
16 changes: 1 addition & 15 deletions juggler/content/NetworkMonitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class NetworkMonitor {
if (!(channel instanceof Ci.nsIHttpChannel))
return;
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
const loadContext = getLoadContext(httpChannel);
const loadContext = helper.getLoadContext(httpChannel);
if (!loadContext)
return;
const window = loadContext.associatedWindow;
Expand All @@ -43,20 +43,6 @@ class NetworkMonitor {
}
}

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'];
this.NetworkMonitor = NetworkMonitor;

10 changes: 10 additions & 0 deletions juggler/protocol/BrowserHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class BrowserHandler {
this._eventListeners = [
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.DownloadCreated, this._onDownloadCreated.bind(this)),
helper.on(this._targetRegistry, TargetRegistry.Events.DownloadFinished, this._onDownloadFinished.bind(this)),
];
}

Expand Down Expand Up @@ -105,6 +107,14 @@ class BrowserHandler {
});
}

_onDownloadCreated(downloadInfo) {
this._session.emitEvent('Browser.downloadCreated', downloadInfo);
}

_onDownloadFinished(downloadInfo) {
this._session.emitEvent('Browser.downloadFinished', downloadInfo);
}

async newPage({browserContextId}) {
const targetId = await this._targetRegistry.newPage({browserContextId});
return {targetId};
Expand Down
19 changes: 19 additions & 0 deletions juggler/protocol/Protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ browserTypes.Geolocation = {
accuracy: t.Optional(t.Number),
};

browserTypes.DownloadOptions = {
behavior: t.Optional(t.Enum(['saveToDisk', 'cancel'])),
downloadsDir: t.Optional(t.String),
};

const pageTypes = {};
pageTypes.DOMPoint = {
x: t.Number,
Expand Down Expand Up @@ -183,6 +188,7 @@ networkTypes.SecurityDetails = {
validTo: t.Number,
};


const Browser = {
targets: ['browser'],

Expand All @@ -197,6 +203,18 @@ const Browser = {
sessionId: 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: {
Expand All @@ -215,6 +233,7 @@ const Browser = {
viewport: t.Optional(pageTypes.Viewport),
locale: t.Optional(t.String),
timezoneId: t.Optional(t.String),
downloadOptions: t.Optional(browserTypes.DownloadOptions),
},
returns: {
browserContextId: t.String,
Expand Down
65 changes: 64 additions & 1 deletion uriloader/exthandler/nsExternalHelperAppService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 58bfc95

Please sign in to comment.