summaryrefslogtreecommitdiffstats
path: root/browser/components/shell/ShellService.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/shell/ShellService.sys.mjs')
-rw-r--r--browser/components/shell/ShellService.sys.mjs572
1 files changed, 572 insertions, 0 deletions
diff --git a/browser/components/shell/ShellService.sys.mjs b/browser/components/shell/ShellService.sys.mjs
new file mode 100644
index 0000000000..b871040db1
--- /dev/null
+++ b/browser/components/shell/ShellService.sys.mjs
@@ -0,0 +1,572 @@
+/* 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 file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
+ WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "XreDirProvider",
+ "@mozilla.org/xre/directory-provider;1",
+ "nsIXREDirProvider"
+);
+
+XPCOMUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ let consoleOptions = {
+ // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+ // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
+ maxLogLevel: "error",
+ maxLogLevelPref: "browser.shell.loglevel",
+ prefix: "ShellService",
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+/**
+ * Internal functionality to save and restore the docShell.allow* properties.
+ */
+let ShellServiceInternal = {
+ /**
+ * Used to determine whether or not to offer "Set as desktop background"
+ * functionality. Even if shell service is available it is not
+ * guaranteed that it is able to set the background for every desktop
+ * which is especially true for Linux with its many different desktop
+ * environments.
+ */
+ get canSetDesktopBackground() {
+ if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
+ return true;
+ }
+
+ if (AppConstants.platform == "linux") {
+ if (this.shellService) {
+ let linuxShellService = this.shellService.QueryInterface(
+ Ci.nsIGNOMEShellService
+ );
+ return linuxShellService.canSetDesktopBackground;
+ }
+ }
+
+ return false;
+ },
+
+ isDefaultBrowserOptOut() {
+ if (AppConstants.platform == "win") {
+ let optOutValue = lazy.WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Mozilla\\Firefox",
+ "DefaultBrowserOptOut"
+ );
+ lazy.WindowsRegistry.removeRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Mozilla\\Firefox",
+ "DefaultBrowserOptOut"
+ );
+ if (optOutValue == "True") {
+ Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", false);
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Used to determine whether or not to show a "Set Default Browser"
+ * query dialog. This attribute is true if the application is starting
+ * up and "browser.shell.checkDefaultBrowser" is true, otherwise it
+ * is false.
+ */
+ _checkedThisSession: false,
+ get shouldCheckDefaultBrowser() {
+ // If we've already checked, the browser has been started and this is a
+ // new window open, and we don't want to check again.
+ if (this._checkedThisSession) {
+ return false;
+ }
+
+ if (!Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser")) {
+ return false;
+ }
+
+ if (this.isDefaultBrowserOptOut()) {
+ return false;
+ }
+
+ return true;
+ },
+
+ set shouldCheckDefaultBrowser(shouldCheck) {
+ Services.prefs.setBoolPref(
+ "browser.shell.checkDefaultBrowser",
+ !!shouldCheck
+ );
+ },
+
+ isDefaultBrowser(startupCheck, forAllTypes) {
+ // If this is the first browser window, maintain internal state that we've
+ // checked this session (so that subsequent window opens don't show the
+ // default browser dialog).
+ if (startupCheck) {
+ this._checkedThisSession = true;
+ }
+ if (this.shellService) {
+ return this.shellService.isDefaultBrowser(forAllTypes);
+ }
+ return false;
+ },
+
+ /*
+ * Invoke the Windows Default Browser agent with the given options.
+ *
+ * Separated for easy stubbing in tests.
+ */
+ _callExternalDefaultBrowserAgent(options = {}) {
+ const wdba = Services.dirsvc.get("XREExeF", Ci.nsIFile);
+ wdba.leafName = "default-browser-agent.exe";
+ return lazy.Subprocess.call({
+ ...options,
+ command: options.command || wdba.path,
+ });
+ },
+
+ /*
+ * Check if UserChoice is impossible.
+ *
+ * Separated for easy stubbing in tests.
+ *
+ * @return string telemetry result like "Err*", or null if UserChoice
+ * is possible.
+ */
+ _userChoiceImpossibleTelemetryResult() {
+ if (!ShellService.checkAllProgIDsExist()) {
+ return "ErrProgID";
+ }
+ if (!ShellService.checkBrowserUserChoiceHashes()) {
+ return "ErrHash";
+ }
+ return null;
+ },
+
+ /*
+ * Accommodate `setDefaultPDFHandlerOnlyReplaceBrowsers` feature.
+ * @return true if Firefox should set itself as default PDF handler, false
+ * otherwise.
+ */
+ _shouldSetDefaultPDFHandler() {
+ if (
+ !lazy.NimbusFeatures.shellService.getVariable(
+ "setDefaultPDFHandlerOnlyReplaceBrowsers"
+ )
+ ) {
+ return true;
+ }
+
+ const handler = this.getDefaultPDFHandler();
+ if (handler === null) {
+ // We only get an exception when something went really wrong. Fail
+ // safely: don't set Firefox as default PDF handler.
+ lazy.log.warn(
+ "Could not determine default PDF handler: not setting Firefox as " +
+ "default PDF handler!"
+ );
+ return false;
+ }
+
+ if (!handler.registered) {
+ lazy.log.debug(
+ "Current default PDF handler has no registered association; " +
+ "should set as default PDF handler."
+ );
+ return true;
+ }
+
+ if (handler.knownBrowser) {
+ lazy.log.debug(
+ "Current default PDF handler progID matches known browser; should " +
+ "set as default PDF handler."
+ );
+ return true;
+ }
+
+ lazy.log.debug(
+ "Current default PDF handler progID does not match known browser " +
+ "prefix; should not set as default PDF handler."
+ );
+ return false;
+ },
+
+ getDefaultPDFHandler() {
+ const knownBrowserPrefixes = [
+ "AppXq0fevzme2pys62n3e0fbqa7peapykr8v", // Edge before Blink, per https://stackoverflow.com/a/32724723.
+ "Brave", // For "BraveFile".
+ "Chrome", // For "ChromeHTML".
+ "Firefox", // For "FirefoxHTML-*" or "FirefoxPDF-*". Need to take from other installations of Firefox!
+ "IE", // Best guess.
+ "MSEdge", // For "MSEdgePDF". Edgium.
+ "Opera", // For "OperaStable", presumably varying with channel.
+ "Yandex", // For "YandexPDF.IHKFKZEIOKEMR6BGF62QXCRIKM", presumably varying with installation.
+ ];
+
+ let currentProgID = "";
+ try {
+ // Returns the empty string when no association is registered, in
+ // which case the prefix matching will fail and we'll set Firefox as
+ // the default PDF handler.
+ currentProgID = this.queryCurrentDefaultHandlerFor(".pdf");
+ } catch (e) {
+ // We only get an exception when something went really wrong. Fail
+ // safely: don't set Firefox as default PDF handler.
+ lazy.log.warn("Failed to queryCurrentDefaultHandlerFor:");
+ return null;
+ }
+
+ if (currentProgID == "") {
+ return { registered: false, knownBrowser: false };
+ }
+
+ const knownBrowserPrefix = knownBrowserPrefixes.find(it =>
+ currentProgID.startsWith(it)
+ );
+
+ if (knownBrowserPrefix) {
+ lazy.log.debug(`Found known browser prefix: ${knownBrowserPrefix}`);
+ }
+
+ return {
+ registered: true,
+ knownBrowser: !!knownBrowserPrefix,
+ };
+ },
+
+ /*
+ * Set the default browser through the UserChoice registry keys on Windows.
+ *
+ * NOTE: This does NOT open the System Settings app for manual selection
+ * in case of failure. If that is desired, catch the exception and call
+ * setDefaultBrowser().
+ *
+ * @return Promise, resolves when successful, rejects with Error on failure.
+ */
+ async setAsDefaultUserChoice() {
+ if (AppConstants.platform != "win") {
+ throw new Error("Windows-only");
+ }
+
+ lazy.log.info("Setting Firefox as default using UserChoice");
+
+ // We launch the WDBA to handle the registry writes, see
+ // SetDefaultBrowserUserChoice() in
+ // toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp.
+ // This is external in case an overzealous antimalware product decides to
+ // quarrantine any program that writes UserChoice, though this has not
+ // occurred during extensive testing.
+
+ let telemetryResult = "ErrOther";
+
+ try {
+ telemetryResult =
+ this._userChoiceImpossibleTelemetryResult() ?? "ErrOther";
+ if (telemetryResult == "ErrProgID") {
+ throw new Error("checkAllProgIDsExist() failed");
+ }
+ if (telemetryResult == "ErrHash") {
+ throw new Error("checkBrowserUserChoiceHashes() failed");
+ }
+
+ const aumi = lazy.XreDirProvider.getInstallHash();
+
+ telemetryResult = "ErrLaunchExe";
+ const exeArgs = ["set-default-browser-user-choice", aumi];
+ if (
+ lazy.NimbusFeatures.shellService.getVariable("setDefaultPDFHandler")
+ ) {
+ if (this._shouldSetDefaultPDFHandler()) {
+ lazy.log.info("Setting Firefox as default PDF handler");
+ exeArgs.push(".pdf", "FirefoxPDF");
+ } else {
+ lazy.log.info("Not setting Firefox as default PDF handler");
+ }
+ }
+ const exeProcess = await this._callExternalDefaultBrowserAgent({
+ arguments: exeArgs,
+ });
+ telemetryResult = "ErrOther";
+ await this._handleWDBAResult(exeProcess);
+ telemetryResult = "Success";
+ } catch (ex) {
+ if (ex instanceof WDBAError) {
+ telemetryResult = ex.telemetryResult;
+ }
+
+ throw ex;
+ } finally {
+ try {
+ const histogram = Services.telemetry.getHistogramById(
+ "BROWSER_SET_DEFAULT_USER_CHOICE_RESULT"
+ );
+ histogram.add(telemetryResult);
+ } catch (ex) {}
+ }
+ },
+
+ async setAsDefaultPDFHandlerUserChoice() {
+ if (AppConstants.platform != "win") {
+ throw new Error("Windows-only");
+ }
+
+ // See comment in setAsDefaultUserChoice for an explanation of why we shell
+ // out to WDBA.
+ let telemetryResult = "ErrOther";
+
+ try {
+ const aumi = lazy.XreDirProvider.getInstallHash();
+ const exeProcess = await this._callExternalDefaultBrowserAgent({
+ arguments: [
+ "set-default-extension-handlers-user-choice",
+ aumi,
+ ".pdf",
+ "FirefoxPDF",
+ ],
+ });
+ telemetryResult = "ErrOther";
+ await this._handleWDBAResult(exeProcess);
+ telemetryResult = "Success";
+ } catch (ex) {
+ if (ex instanceof WDBAError) {
+ telemetryResult = ex.telemetryResult;
+ }
+
+ throw ex;
+ } finally {
+ try {
+ const histogram = Services.telemetry.getHistogramById(
+ "BROWSER_SET_DEFAULT_PDF_HANDLER_USER_CHOICE_RESULT"
+ );
+ histogram.add(telemetryResult);
+ } catch (ex) {}
+ }
+ },
+
+ // override nsIShellService.setDefaultBrowser() on the ShellService proxy.
+ setDefaultBrowser(claimAllTypes, forAllUsers) {
+ // On Windows 10, our best chance is to set UserChoice, so try that first.
+ if (
+ AppConstants.isPlatformAndVersionAtLeast("win", "10") &&
+ lazy.NimbusFeatures.shellService.getVariable(
+ "setDefaultBrowserUserChoice"
+ )
+ ) {
+ // nsWindowsShellService::SetDefaultBrowser() kicks off several
+ // operations, but doesn't wait for their result. So we don't need to
+ // await the result of setAsDefaultUserChoice() here, either, we just need
+ // to fall back in case it fails.
+ this.setAsDefaultUserChoice().catch(err => {
+ console.error(err);
+ this.shellService.setDefaultBrowser(claimAllTypes, forAllUsers);
+ });
+ return;
+ }
+
+ this.shellService.setDefaultBrowser(claimAllTypes, forAllUsers);
+ },
+
+ setAsDefault() {
+ let claimAllTypes = true;
+ let setAsDefaultError = false;
+ if (AppConstants.platform == "win") {
+ try {
+ // In Windows 8+, the UI for selecting default protocol is much
+ // nicer than the UI for setting file type associations. So we
+ // only show the protocol association screen on Windows 8+.
+ // Windows 8 is version 6.2.
+ let version = Services.sysinfo.getProperty("version");
+ claimAllTypes = parseFloat(version) < 6.2;
+ } catch (ex) {}
+ }
+ try {
+ ShellService.setDefaultBrowser(claimAllTypes, false);
+ } catch (ex) {
+ setAsDefaultError = true;
+ console.error(ex);
+ }
+ // Here BROWSER_IS_USER_DEFAULT and BROWSER_SET_USER_DEFAULT_ERROR appear
+ // to be inverse of each other, but that is only because this function is
+ // called when the browser is set as the default. During startup we record
+ // the BROWSER_IS_USER_DEFAULT value without recording BROWSER_SET_USER_DEFAULT_ERROR.
+ Services.telemetry
+ .getHistogramById("BROWSER_IS_USER_DEFAULT")
+ .add(!setAsDefaultError);
+ Services.telemetry
+ .getHistogramById("BROWSER_SET_DEFAULT_ERROR")
+ .add(setAsDefaultError);
+ },
+
+ setAsDefaultPDFHandler(onlyIfKnownBrowser = false) {
+ if (onlyIfKnownBrowser && !this.getDefaultPDFHandler().knownBrowser) {
+ return;
+ }
+
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ this.setAsDefaultPDFHandlerUserChoice();
+ }
+ },
+
+ /**
+ * Determine if we're the default handler for the given file extension (like
+ * ".pdf") or protocol (like "https"). Windows-only for now.
+ *
+ * @returns {boolean} true if we are the default handler, false otherwise.
+ */
+ isDefaultHandlerFor(aFileExtensionOrProtocol) {
+ if (AppConstants.platform == "win") {
+ return this.shellService
+ .QueryInterface(Ci.nsIWindowsShellService)
+ .isDefaultHandlerFor(aFileExtensionOrProtocol);
+ }
+ return false;
+ },
+
+ /**
+ * Checks if Firefox app can and isn't pinned to OS "taskbar."
+ *
+ * @throws if not called from main process.
+ */
+ async doesAppNeedPin(privateBrowsing = false) {
+ if (
+ Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT
+ ) {
+ throw new Components.Exception(
+ "Can't determine pinned from child process",
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ }
+
+ // Pretend pinning is not needed/supported if remotely disabled.
+ if (lazy.NimbusFeatures.shellService.getVariable("disablePin")) {
+ return false;
+ }
+
+ // Currently this only works on certain Windows versions.
+ try {
+ // First check if we can even pin the app where an exception means no.
+ await this.shellService
+ .QueryInterface(Ci.nsIWindowsShellService)
+ .checkPinCurrentAppToTaskbarAsync(privateBrowsing);
+ let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(
+ Ci.nsIWinTaskbar
+ );
+
+ // Then check if we're already pinned.
+ return !(await this.shellService.isCurrentAppPinnedToTaskbarAsync(
+ privateBrowsing
+ ? winTaskbar.defaultPrivateGroupId
+ : winTaskbar.defaultGroupId
+ ));
+ } catch (ex) {}
+
+ // Next check mac pinning to dock.
+ try {
+ // Accessing this.macDockSupport will ensure we're actually running
+ // on Mac (it's possible to be on Linux in this block).
+ const isInDock = this.macDockSupport.isAppInDock;
+ // We can't pin Private Browsing mode on Mac, only a shortcut to the vanilla app
+ return privateBrowsing ? false : !isInDock;
+ } catch (ex) {}
+ return false;
+ },
+
+ /**
+ * Pin Firefox app to the OS "taskbar."
+ */
+ async pinToTaskbar(privateBrowsing = false) {
+ if (await this.doesAppNeedPin(privateBrowsing)) {
+ try {
+ if (AppConstants.platform == "win") {
+ await this.shellService.pinCurrentAppToTaskbarAsync(privateBrowsing);
+ } else if (AppConstants.platform == "macosx") {
+ this.macDockSupport.ensureAppIsPinnedToDock();
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ },
+
+ async _handleWDBAResult(exeProcess, exeWaitTimeoutMs = 2000) {
+ // Exit codes, see toolkit/mozapps/defaultagent/SetDefaultBrowser.h
+ const S_OK = 0;
+ const STILL_ACTIVE = 0x103;
+ const MOZ_E_NO_PROGID = 0xa0000001;
+ const MOZ_E_HASH_CHECK = 0xa0000002;
+ const MOZ_E_REJECTED = 0xa0000003;
+ const MOZ_E_BUILD = 0xa0000004;
+
+ const exeWaitPromise = exeProcess.wait();
+ const timeoutPromise = new Promise(function (resolve) {
+ lazy.setTimeout(
+ () => resolve({ exitCode: STILL_ACTIVE }),
+ exeWaitTimeoutMs
+ );
+ });
+
+ const { exitCode } = await Promise.race([exeWaitPromise, timeoutPromise]);
+
+ if (exitCode != S_OK) {
+ const telemetryResult =
+ new Map([
+ [STILL_ACTIVE, "ErrExeTimeout"],
+ [MOZ_E_NO_PROGID, "ErrExeProgID"],
+ [MOZ_E_HASH_CHECK, "ErrExeHash"],
+ [MOZ_E_REJECTED, "ErrExeRejected"],
+ [MOZ_E_BUILD, "ErrBuild"],
+ ]).get(exitCode) ?? "ErrExeOther";
+
+ throw new WDBAError(exitCode, telemetryResult);
+ }
+ },
+};
+
+XPCOMUtils.defineLazyServiceGetters(ShellServiceInternal, {
+ shellService: ["@mozilla.org/browser/shell-service;1", "nsIShellService"],
+ macDockSupport: ["@mozilla.org/widget/macdocksupport;1", "nsIMacDockSupport"],
+});
+
+/**
+ * The external API exported by this module.
+ */
+export var ShellService = new Proxy(ShellServiceInternal, {
+ get(target, name) {
+ if (name in target) {
+ return target[name];
+ }
+ if (target.shellService) {
+ return target.shellService[name];
+ }
+ Services.console.logStringMessage(
+ `${name} not found in ShellService: ${target.shellService}`
+ );
+ return undefined;
+ },
+});
+
+class WDBAError extends Error {
+ constructor(exitCode, telemetryResult) {
+ super(`WDBA nonzero exit code ${exitCode}: ${telemetryResult}`);
+
+ this.exitCode = exitCode;
+ this.telemetryResult = telemetryResult;
+ }
+}