Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
Downloads: "resource://gre/modules/Downloads.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});
const HELPER_APP_DIALOG_HANDLER_CID = Services.uuid.generateUUID();
const HELPER_APP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1";
async function computeTargetFile(
aLauncher,
aDefaultFileName,
aSuggestedFileExtension
) {
let targetDir;
const browsingContextId = aLauncher.browsingContextId;
if (browsingContextId) {
try {
const bc = BrowsingContext.get(browsingContextId);
const folderOverride = bc?.top?.downloadFolderOverride;
if (folderOverride) {
targetDir = folderOverride;
}
} catch (e) {
// BrowsingContext may have been destroyed.
}
}
if (!targetDir) {
targetDir = await lazy.Downloads.getPreferredDownloadsDirectory();
}
let leafName;
if (aDefaultFileName) {
leafName = lazy.DownloadPaths.sanitize(aDefaultFileName, {
compressWhitespaces: true,
});
}
if (!leafName) {
const ext = aSuggestedFileExtension || "";
leafName = "unnamed" + ext;
}
const dir = new lazy.FileUtils.File(targetDir);
dir.append(leafName);
return lazy.DownloadPaths.createNiceUniqueFile(dir);
}
/**
* The HelperAppDialogHandler can override the default component factory for
* nsIHelperAppLauncherDialog to silently save downloads without showing UI
* when WebDriver controls download behavior.
* See the main implementation in https://searchfox.org/firefox-main/source/toolkit/mozapps/downloads/HelperAppDlg.sys.mjs.
*/
class HelperAppDialogHandlerClass {
#callers;
#originalCID;
#registeredFactory;
#registrar;
constructor() {
this.#registeredFactory = null;
this.#registrar = Components.manager.QueryInterface(
Ci.nsIComponentRegistrar
);
this.#originalCID = this.#registrar.contractIDToCID(
HELPER_APP_DIALOG_CONTRACT_ID
);
this.#callers = new Set();
}
/**
* Stop intercepting helper app launcher dialogs on behalf of the provided
* caller. Dialogs will only be restored once all callers have called
* restoreDialogs.
*
* @param {object} caller
*/
restoreDialogs(caller) {
if (!this.#callers.has(caller)) {
return;
}
this.#callers.delete(caller);
if (!this.#registeredFactory) {
return;
}
if (this.#callers.size) {
return;
}
this.#registrar.unregisterFactory(
HELPER_APP_DIALOG_HANDLER_CID,
this.#registeredFactory
);
this.#registeredFactory = null;
this.#registrar.registerFactory(
this.#originalCID,
"",
HELPER_APP_DIALOG_CONTRACT_ID,
null
);
}
/**
* Intercept all helper app launcher dialogs by registering a custom factory
* that silently saves downloads instead of showing UI.
*
* @param {object} caller
*/
interceptDialogs(caller) {
this.#callers.add(caller);
if (this.#registeredFactory) {
return;
}
this.#registeredFactory = {
createInstance(iid) {
const proxy = {
show(aLauncher, _aContext, _aReason) {
aLauncher.promptForSaveDestination();
},
promptForSaveToFileAsync(
aLauncher,
_aContext,
aDefaultFileName,
aSuggestedFileExtension,
_aForcePrompt
) {
computeTargetFile(
aLauncher,
aDefaultFileName,
aSuggestedFileExtension
).then(
file => aLauncher.saveDestinationAvailable(file),
() => aLauncher.saveDestinationAvailable(null)
);
},
QueryInterface: ChromeUtils.generateQI([
"nsIHelperAppLauncherDialog",
]),
};
return proxy.QueryInterface(iid);
},
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
};
this.#registrar.registerFactory(
HELPER_APP_DIALOG_HANDLER_CID,
"WebDriver HelperAppLauncherDialog handler",
HELPER_APP_DIALOG_CONTRACT_ID,
this.#registeredFactory
);
}
}
// Expose a singleton shared by all WebDriver sessions.
export const HelperAppDialogHandler = new HelperAppDialogHandlerClass();