diff options
Diffstat (limited to 'browser/components/shell/ShellService.sys.mjs')
-rw-r--r-- | browser/components/shell/ShellService.sys.mjs | 499 |
1 files changed, 499 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..c4af0be7de --- /dev/null +++ b/browser/components/shell/ShellService.sys.mjs @@ -0,0 +1,499 @@ +/* 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", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "XreDirProvider", + "@mozilla.org/xre/directory-provider;1", + "nsIXREDirProvider" +); + +ChromeUtils.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; + }, + + /** + * 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; + } + + 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; + }, + + /* + * 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. + "AppXd4nrz8ff68srnhf9t5a8sbjyar1cr723", // Another pre-Blink Edge identifier. See Bug 1858729. + "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"); + + 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 extraFileExtensions = []; + if ( + lazy.NimbusFeatures.shellService.getVariable("setDefaultPDFHandler") + ) { + if (this._shouldSetDefaultPDFHandler()) { + lazy.log.info("Setting Firefox as default PDF handler"); + extraFileExtensions.push(".pdf", "FirefoxPDF"); + } else { + lazy.log.info("Not setting Firefox as default PDF handler"); + } + } + try { + await this.defaultAgent.setDefaultBrowserUserChoiceAsync( + aumi, + extraFileExtensions + ); + } catch (err) { + telemetryResult = "ErrOther"; + this._handleWDBAResult(err.result || Cr.NS_ERROR_FAILURE); + } + 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"); + } + + let telemetryResult = "ErrOther"; + + try { + const aumi = lazy.XreDirProvider.getInstallHash(); + try { + this.defaultAgent.setDefaultExtensionHandlersUserChoice(aumi, [ + ".pdf", + "FirefoxPDF", + ]); + } catch (err) { + telemetryResult = "ErrOther"; + this._handleWDBAResult(err.result || Cr.NS_ERROR_FAILURE); + } + 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. + async setDefaultBrowser(forAllUsers) { + // On Windows, our best chance is to set UserChoice, so try that first. + if ( + AppConstants.platform == "win" && + lazy.NimbusFeatures.shellService.getVariable( + "setDefaultBrowserUserChoice" + ) + ) { + try { + await this.setAsDefaultUserChoice(); + return; + } catch (err) { + lazy.log.warn( + "Error thrown during setAsDefaultUserChoice. Full exception:", + err + ); + + // intentionally fall through to setting via the non-user choice pathway on error + } + } + + this.shellService.setDefaultBrowser(forAllUsers); + }, + + async setAsDefault() { + let setAsDefaultError = false; + try { + await ShellService.setDefaultBrowser(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.platform == "win") { + 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); + } + } + }, + + _handleWDBAResult(exitCode) { + if (exitCode != Cr.NS_OK) { + const telemetryResult = + new Map([ + [Cr.NS_ERROR_WDBA_NO_PROGID, "ErrExeProgID"], + [Cr.NS_ERROR_WDBA_HASH_CHECK, "ErrExeHash"], + [Cr.NS_ERROR_WDBA_REJECTED, "ErrExeRejected"], + [Cr.NS_ERROR_WDBA_BUILD, "ErrBuild"], + ]).get(exitCode) ?? "ErrExeOther"; + + throw new WDBAError(exitCode, telemetryResult); + } + }, +}; + +XPCOMUtils.defineLazyServiceGetters(ShellServiceInternal, { + defaultAgent: ["@mozilla.org/default-agent;1", "nsIDefaultAgent"], + 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; + } +} |