diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/shell/ShellService.sys.mjs | 572 |
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; + } +} |