Bug 1703578 - Part 3: Invoke WDBA to set UserChoice. r=bytesized
authorAdam Gashlin <agashlin@mozilla.com>
Thu, 17 Jun 2021 18:06:35 +0000
changeset 583757 91f7b8dc86708e694233f5ddbd2c4213febf90fb
parent 583756 19dce9dc4c02caf462e271286ab70be7aa7c3eab
child 583758 fe2b9abdb53bf6d24ea184ebcb77350f38ea8405
push id145231
push useragashlin@mozilla.com
push dateThu, 17 Jun 2021 20:30:21 +0000
treeherderautoland@fe2b9abdb53b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbytesized
bugs1703578
milestone91.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1703578 - Part 3: Invoke WDBA to set UserChoice. r=bytesized Depends on D113427 Differential Revision: https://phabricator.services.mozilla.com/D113428
browser/app/profile/firefox.js
browser/components/shell/ShellService.jsm
browser/components/shell/moz.build
browser/components/shell/nsIWindowsShellService.idl
browser/components/shell/nsWindowsShellService.cpp
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -258,16 +258,22 @@ pref("browser.compactmode.show", false);
 
 // At startup, check if we're the default browser and prompt user if not.
 pref("browser.shell.checkDefaultBrowser", true);
 pref("browser.shell.shortcutFavicons",true);
 pref("browser.shell.mostRecentDateSetAsDefault", "");
 pref("browser.shell.skipDefaultBrowserCheckOnFirstRun", true);
 pref("browser.shell.didSkipDefaultBrowserCheckOnFirstRun", false);
 pref("browser.shell.defaultBrowserCheckCount", 0);
+#if defined(XP_WIN)
+// Attempt to set the default browser on Windows 10 using the UserChoice registry keys,
+// before falling back to launching the modern Settings dialog.
+pref("browser.shell.setDefaultBrowserUserChoice", true);
+#endif
+
 
 // 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session
 // The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore
 pref("browser.startup.page",                1);
 pref("browser.startup.homepage",            "about:home");
 #ifdef NIGHTLY_BUILD
 pref("browser.startup.homepage.abouthome_cache.enabled", true);
 #else
--- a/browser/components/shell/ShellService.jsm
+++ b/browser/components/shell/ShellService.jsm
@@ -14,16 +14,28 @@ const { XPCOMUtils } = ChromeUtils.impor
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "WindowsRegistry",
   "resource://gre/modules/WindowsRegistry.jsm"
 );
 
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Subprocess: "resource://gre/modules/Subprocess.jsm",
+  setTimeout: "resource://gre/modules/Timer.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "XreDirProvider",
+  "@mozilla.org/xre/directory-provider;1",
+  "nsIXREDirProvider"
+);
+
 /**
  * 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
@@ -107,16 +119,94 @@ let ShellServiceInternal = {
       this._checkedThisSession = true;
     }
     if (this.shellService) {
       return this.shellService.isDefaultBrowser(forAllTypes);
     }
     return false;
   },
 
+  /*
+   * 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");
+    }
+
+    // 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.
+
+    if (!ShellService.checkAllProgIDsExist()) {
+      throw new Error("checkAllProgIDsExist() failed");
+    }
+
+    if (!ShellService.checkBrowserUserChoiceHashes()) {
+      throw new Error("checkBrowserUserChoiceHashes() failed");
+    }
+
+    const wdba = Services.dirsvc.get("XREExeF", Ci.nsIFile);
+    wdba.leafName = "default-browser-agent.exe";
+    const aumi = XreDirProvider.getInstallHash();
+
+    const exeProcess = await Subprocess.call({
+      command: wdba.path,
+      arguments: ["set-default-browser-user-choice", aumi],
+    });
+
+    // exit codes
+    const S_OK = 0;
+    const STILL_ACTIVE = 0x103;
+
+    const exeWaitTimeoutMs = 2000; // 2 seconds
+    const exeWaitPromise = exeProcess.wait();
+    const timeoutPromise = new Promise(function(resolve, reject) {
+      setTimeout(() => resolve({ exitCode: STILL_ACTIVE }), exeWaitTimeoutMs);
+    });
+    const { exitCode } = await Promise.race([exeWaitPromise, timeoutPromise]);
+
+    if (exitCode != S_OK) {
+      throw new Error(`WDBA nonzero exit code ${exitCode}`);
+    }
+  },
+
+  // 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") &&
+      Services.prefs.getBoolPref(
+        "browser.shell.setDefaultBrowserUserChoice",
+        true
+      )
+    ) {
+      // 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 => {
+        Cu.reportError(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+.
--- a/browser/components/shell/moz.build
+++ b/browser/components/shell/moz.build
@@ -46,36 +46,44 @@ elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gt
 
 elif CONFIG["OS_ARCH"] == "WINNT":
     XPIDL_SOURCES += [
         "nsIWindowsShellService.idl",
     ]
     SOURCES += [
         "nsWindowsShellService.cpp",
         "WindowsDefaultBrowser.cpp",
+        "WindowsUserChoice.cpp",
     ]
     LOCAL_INCLUDES += [
         "../../../other-licenses/nsis/Contrib/CityHash/cityhash",
     ]
     OS_LIBS += [
+        "bcrypt",
+        "crypt32",
         "propsys",
     ]
 
 XPIDL_MODULE = "shellservice"
 
 if SOURCES:
     FINAL_LIBRARY = "browsercomps"
 
 EXTRA_JS_MODULES += [
     "HeadlessShell.jsm",
     "ScreenshotChild.jsm",
     "ShellService.jsm",
 ]
 
-for var in ("MOZ_APP_DISPLAYNAME", "MOZ_APP_NAME", "MOZ_APP_VERSION"):
+for var in (
+    "MOZ_APP_DISPLAYNAME",
+    "MOZ_APP_NAME",
+    "MOZ_APP_VERSION",
+    "MOZ_DEFAULT_BROWSER_AGENT",
+):
     DEFINES[var] = '"%s"' % CONFIG[var]
 
 CXXFLAGS += CONFIG["TK_CFLAGS"]
 if CONFIG["MOZ_ENABLE_DBUS"]:
     CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"]
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "Shell Integration")
--- a/browser/components/shell/nsIWindowsShellService.idl
+++ b/browser/components/shell/nsIWindowsShellService.idl
@@ -84,9 +84,28 @@ interface nsIWindowsShellService : nsISu
    * - "Taskbar", Taskbar Pins
    * - "" otherwise
    *
    * NOTE: This tries to avoid I/O, so paths are compared directly as
    * strings, which may not be accurate in all cases. It is intended
    * for noncritical telemetry use.
    */
   AString classifyShortcut(in AString aPath);
+
+  /*
+   * Check if setDefaultBrowserUserChoice() is expected to succeed.
+   *
+   * This checks the ProgIDs for this installation, and the hash of the existing
+   * UserChoice association.
+   *
+   * @return true if the check succeeds, false otherwise.
+   */
+  bool canSetDefaultBrowserUserChoice();
+
+  /*
+   * checkAllProgIDsExist() and checkBrowserUserChoiceHashes() are components
+   * of canSetDefaultBrowserUserChoice(), broken out for telemetry purposes.
+   *
+   * @return true if the check succeeds, false otherwise.
+   */
+  bool checkAllProgIDsExist();
+  bool checkBrowserUserChoiceHashes();
 };
--- a/browser/components/shell/nsWindowsShellService.cpp
+++ b/browser/components/shell/nsWindowsShellService.cpp
@@ -26,16 +26,17 @@
 #include "nsIURLFormatter.h"
 #include "nsXULAppAPI.h"
 #include "mozilla/WindowsVersion.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/gfx/2D.h"
 #include "WindowsDefaultBrowser.h"
+#include "WindowsUserChoice.h"
 
 #include <windows.h>
 #include <shellapi.h>
 #include <propvarutil.h>
 #include <propkey.h>
 
 #ifdef __MINGW32__
 // MinGW-w64 headers are missing PropVariantToString.
@@ -226,16 +227,49 @@ nsresult nsWindowsShellService::LaunchCo
   }
   return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
 }
 
 nsresult nsWindowsShellService::LaunchControlPanelDefaultPrograms() {
   return ::LaunchControlPanelDefaultPrograms() ? NS_OK : NS_ERROR_FAILURE;
 }
 
+NS_IMETHODIMP
+nsWindowsShellService::CheckAllProgIDsExist(bool* aResult) {
+  *aResult = false;
+  nsAutoString aumid;
+  if (!mozilla::widget::WinTaskbar::GetAppUserModelID(aumid)) {
+    return NS_OK;
+  }
+  *aResult =
+      CheckProgIDExists(FormatProgID(L"FirefoxURL", aumid.get()).get()) &&
+      CheckProgIDExists(FormatProgID(L"FirefoxHTML", aumid.get()).get());
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::CheckBrowserUserChoiceHashes(bool* aResult) {
+  *aResult = ::CheckBrowserUserChoiceHashes();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::CanSetDefaultBrowserUserChoice(bool* aResult) {
+  *aResult = false;
+// If the WDBA is not available, this could never succeed.
+#ifdef MOZ_DEFAULT_BROWSER_AGENT
+  bool progIDsExist = false;
+  bool hashOk = false;
+  *aResult = NS_SUCCEEDED(CheckAllProgIDsExist(&progIDsExist)) &&
+             progIDsExist &&
+             NS_SUCCEEDED(CheckBrowserUserChoiceHashes(&hashOk)) && hashOk;
+#endif
+  return NS_OK;
+}
+
 nsresult nsWindowsShellService::LaunchModernSettingsDialogDefaultApps() {
   return ::LaunchModernSettingsDialogDefaultApps() ? NS_OK : NS_ERROR_FAILURE;
 }
 
 nsresult nsWindowsShellService::InvokeHTTPOpenAsVerb() {
   nsCOMPtr<nsIURLFormatter> formatter(
       do_GetService("@mozilla.org/toolkit/URLFormatterService;1"));
   if (!formatter) {