summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/defaultagent
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/defaultagent')
-rw-r--r--toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs456
-rw-r--r--toolkit/mozapps/defaultagent/Cache.cpp594
-rw-r--r--toolkit/mozapps/defaultagent/Cache.h189
-rw-r--r--toolkit/mozapps/defaultagent/DefaultAgent.cpp491
-rw-r--r--toolkit/mozapps/defaultagent/DefaultAgent.h28
-rw-r--r--toolkit/mozapps/defaultagent/DefaultBrowser.cpp240
-rw-r--r--toolkit/mozapps/defaultagent/DefaultBrowser.h39
-rw-r--r--toolkit/mozapps/defaultagent/DefaultPDF.cpp151
-rw-r--r--toolkit/mozapps/defaultagent/DefaultPDF.h34
-rw-r--r--toolkit/mozapps/defaultagent/EventLog.cpp11
-rw-r--r--toolkit/mozapps/defaultagent/EventLog.h24
-rw-r--r--toolkit/mozapps/defaultagent/Notification.cpp709
-rw-r--r--toolkit/mozapps/defaultagent/Notification.h60
-rw-r--r--toolkit/mozapps/defaultagent/Policy.cpp162
-rw-r--r--toolkit/mozapps/defaultagent/Policy.h17
-rw-r--r--toolkit/mozapps/defaultagent/Registry.cpp330
-rw-r--r--toolkit/mozapps/defaultagent/Registry.h100
-rw-r--r--toolkit/mozapps/defaultagent/ScheduledTask.cpp328
-rw-r--r--toolkit/mozapps/defaultagent/ScheduledTask.h23
-rw-r--r--toolkit/mozapps/defaultagent/ScheduledTaskRemove.cpp126
-rw-r--r--toolkit/mozapps/defaultagent/ScheduledTaskRemove.h37
-rw-r--r--toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp347
-rw-r--r--toolkit/mozapps/defaultagent/SetDefaultBrowser.h66
-rw-r--r--toolkit/mozapps/defaultagent/Telemetry.cpp585
-rw-r--r--toolkit/mozapps/defaultagent/Telemetry.h24
-rw-r--r--toolkit/mozapps/defaultagent/UtfConvert.cpp59
-rw-r--r--toolkit/mozapps/defaultagent/UtfConvert.h24
-rw-r--r--toolkit/mozapps/defaultagent/WindowsMutex.cpp103
-rw-r--r--toolkit/mozapps/defaultagent/WindowsMutex.h45
-rw-r--r--toolkit/mozapps/defaultagent/common.cpp85
-rw-r--r--toolkit/mozapps/defaultagent/common.h29
-rw-r--r--toolkit/mozapps/defaultagent/components.conf21
-rw-r--r--toolkit/mozapps/defaultagent/defaultagent.ini9
-rw-r--r--toolkit/mozapps/defaultagent/docs/index.rst49
-rw-r--r--toolkit/mozapps/defaultagent/metrics.yaml208
-rw-r--r--toolkit/mozapps/defaultagent/module.ver1
-rw-r--r--toolkit/mozapps/defaultagent/moz.build113
-rw-r--r--toolkit/mozapps/defaultagent/nsIDefaultAgent.idl167
-rw-r--r--toolkit/mozapps/defaultagent/nsIWindowsMutex.idl62
-rw-r--r--toolkit/mozapps/defaultagent/pings.yaml42
-rw-r--r--toolkit/mozapps/defaultagent/proxy/Makefile.in16
-rw-r--r--toolkit/mozapps/defaultagent/proxy/default-browser-agent.exe.manifest31
-rw-r--r--toolkit/mozapps/defaultagent/proxy/main.cpp118
-rw-r--r--toolkit/mozapps/defaultagent/proxy/moz.build68
-rw-r--r--toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp301
-rw-r--r--toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp55
-rw-r--r--toolkit/mozapps/defaultagent/tests/gtest/moz.build33
-rw-r--r--toolkit/mozapps/defaultagent/tests/xpcshell/test_windows_mutex.js144
-rw-r--r--toolkit/mozapps/defaultagent/tests/xpcshell/xpcshell.toml4
49 files changed, 6958 insertions, 0 deletions
diff --git a/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs
new file mode 100644
index 0000000000..c727a55997
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs
@@ -0,0 +1,456 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* 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 { EXIT_CODE as EXIT_CODE_BASE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
+import { AppConstants as AC } from "resource://gre/modules/AppConstants.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const EXIT_CODE = {
+ ...EXIT_CODE_BASE,
+ DISABLED_BY_POLICY: EXIT_CODE_BASE.LAST_RESERVED + 1,
+ INVALID_ARGUMENT: EXIT_CODE_BASE.LAST_RESERVED + 2,
+ MUTEX_NOT_LOCKABLE: EXIT_CODE_BASE.LAST_RESERVED + 3,
+};
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
+ ShellService: "resource:///modules/ShellService.sys.mjs",
+});
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ AlertsService: ["@mozilla.org/alerts-service;1", "nsIAlertsService"],
+});
+ChromeUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ let consoleOptions = {
+ maxLogLevel: "error",
+ maxLogLevelPref: "app.defaultagent.loglevel",
+ prefix: "DefaultAgent",
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+// Should be slightly longer than NOTIFICATION_WAIT_TIMEOUT_MS in
+// Notification.cpp (divided by 1000 to convert millseconds to seconds) to not
+// cause race between timeouts. Currently 12 hours + 5 additional minutes.
+export const backgroundTaskTimeoutSec = 12 * 60 * 60 + 60 * 5;
+const kNotificationTimeoutMs = 12 * 60 * 60 * 1000;
+
+const kNotificationShown = Object.freeze({
+ notShown: "not-shown",
+ shown: "shown",
+ error: "error",
+});
+
+const kNotificationAction = Object.freeze({
+ dismissedByTimeout: "dismissed-by-timeout",
+ dismissedByButton: "dismissed-by-button",
+ dismissedToActionCenter: "dismissed-to-action-center",
+ makeFirefoxDefaultButton: "make-firefox-default-button",
+ toastClicked: "toast-clicked",
+ noAction: "no-action",
+});
+
+// We expect to be given a command string in argv[1], perhaps followed by other
+// arguments depending on the command. The valid commands are:
+// register-task [unique-token]
+// Create a Windows scheduled task that will launch this binary with the
+// do-task command every 24 hours, starting from 24 hours after register-task
+// is run. unique-token is required and should be some string that uniquely
+// identifies this installation of the product; typically this will be the
+// install path hash that's used for the update directory, the AppUserModelID,
+// and other related purposes.
+// update-task [unique-token]
+// Update an existing task registration, without changing its schedule. This
+// should be called during updates of the application, in case this program
+// has been updated and any of the task parameters have changed. The unique
+// token argument is required and should be the same one that was passed in
+// when the task was registered.
+// unregister-task [unique-token]
+// Removes the previously created task. The unique token argument is required
+// and should be the same one that was passed in when the task was registered.
+// uninstall [unique-token]
+// Removes the previously created task, and also removes all registry entries
+// running the task may have created. The unique token argument is required
+// and should be the same one that was passed in when the task was registered.
+// do-task [app-user-model-id]
+// Actually performs the default agent task, which currently means generating
+// and sending our telemetry ping and possibly showing a notification to the
+// user if their browser has switched from Firefox to Edge with Blink.
+// set-default-browser-user-choice [app-user-model-id] [[.file1 ProgIDRoot1]
+// ...]
+// Set the default browser via the UserChoice registry keys. Additional
+// optional file extensions to register can be specified as additional
+// argument pairs: the first element is the file extension, the second element
+// is the root of a ProgID, which will be suffixed with `-$AUMI`.
+export async function runBackgroundTask(commandLine) {
+ Services.fog.initializeFOG(
+ undefined,
+ "firefox.desktop.background.defaultagent"
+ );
+
+ let defaultAgent = Cc["@mozilla.org/default-agent;1"].getService(
+ Ci.nsIDefaultAgent
+ );
+
+ let command = commandLine.getArgument(0);
+
+ // The uninstall and unregister commands are allowed even if the policy
+ // disabling the task is set, so that uninstalls and updates always work.
+ // Similarly, debug commands are always allowed.
+ switch (command) {
+ case "uninstall": {
+ let token = commandLine.getArgument(1);
+ lazy.log.info(`Uninstalling for token "${token}"`);
+ defaultAgent.uninstall(token);
+ return EXIT_CODE.SUCCESS;
+ }
+ case "unregister-task": {
+ let token = commandLine.getArgument(1);
+ lazy.log.info(`Unregistering task for token "${token}"`);
+ defaultAgent.unregisterTask(token);
+ return EXIT_CODE.SUCCESS;
+ }
+ }
+
+ // We check for disablement by policy because that's assumed to be static.
+ // But we don't check for disablement by remote settings so that
+ // `register-task` and `update-task` can proceed as part of the update
+ // cycle, waiting for remote (re-)enablement.
+ if (defaultAgent.agentDisabled()) {
+ lazy.log.warn("Default Agent disabled, exiting without running.");
+ return EXIT_CODE.DISABLED_BY_POLICY;
+ }
+
+ switch (command) {
+ case "register-task": {
+ let token = commandLine.getArgument(1);
+ lazy.log.info(`Registering task for token "${token}"`);
+ defaultAgent.registerTask(token);
+ return EXIT_CODE.SUCCESS;
+ }
+ case "update-task": {
+ let token = commandLine.getArgument(1);
+ lazy.log.info(`Updating task for token "${token}"`);
+ defaultAgent.updateTask(token);
+ return EXIT_CODE.SUCCESS;
+ }
+ case "do-task": {
+ let aumid = commandLine.getArgument(1);
+ let force = commandLine.findFlag("force", true) != -1;
+
+ lazy.log.info(`Running do-task with AUMID "${aumid}"`);
+
+ let cppFallback = false;
+ try {
+ await lazy.BackgroundTasksUtils.enableNimbus(commandLine);
+ cppFallback =
+ lazy.NimbusFeatures.defaultAgent.getVariable("cppFallback");
+ } catch (e) {
+ lazy.log.error(`Error enabling nimbus: ${e}`);
+ }
+
+ try {
+ if (!cppFallback) {
+ lazy.log.info("Running JS do-task.");
+ await runWithRegistryLocked(async () => {
+ await doTask(defaultAgent, force);
+ });
+ } else {
+ lazy.log.info("Running C++ do-task.");
+ defaultAgent.doTask(aumid, force);
+ }
+ } catch (e) {
+ if (e.message) {
+ lazy.log.error(e.message);
+ }
+
+ if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ return EXIT_CODE.MUTEX_NOT_LOCKABLE;
+ }
+
+ return EXIT_CODE.EXCEPTION;
+ }
+
+ // Bug 1857333: We wait for arbitrary time for Glean to submit telemetry.
+ lazy.log.info("Pinged glean, waiting for submission.");
+ await new Promise(resolve => lazy.setTimeout(resolve, 5000));
+
+ return EXIT_CODE.SUCCESS;
+ }
+ }
+
+ return EXIT_CODE.INVALID_ARGUMENT;
+}
+
+// Throws if unable to lock mutex (therefore function isn't run).
+async function runWithRegistryLocked(aMutexGuardedFunction) {
+ const kVendor = Services.appinfo.vendor || "";
+ const kRegistryMutexName = `${kVendor}${AC.MOZ_APP_BASENAME}DefaultBrowserAgentRegistryMutex`;
+ let mutexFactory = Cc["@mozilla.org/windows-mutex-factory;1"].getService(
+ Ci.nsIWindowsMutexFactory
+ );
+
+ let mutex = mutexFactory.createMutex(kRegistryMutexName);
+ mutex.tryLock(kRegistryMutexName);
+ lazy.log.debug(`Locked named mutex: ${kRegistryMutexName}`);
+ try {
+ await aMutexGuardedFunction();
+ } finally {
+ mutex.unlock();
+ lazy.log.debug(`Unlocked named mutex: ${kRegistryMutexName}`);
+ }
+}
+
+async function doTask(defaultAgent, force) {
+ if (!defaultAgent.appRanRecently() && !force) {
+ lazy.log.warn("Main app has not ran recently, exiting without running.");
+ throw new Error("App hasn't ran recently");
+ }
+
+ let browser = defaultAgent.getDefaultBrowser();
+ lazy.log.debug(`Default browser: ${browser}`);
+ let previousBrowser = defaultAgent.getReplacePreviousDefaultBrowser(browser);
+ lazy.log.debug(`Previous browser: ${previousBrowser}`);
+ let defaultPdfHandler = defaultAgent.getDefaultPdfHandler();
+ lazy.log.debug(`Default PDF Handler: ${defaultPdfHandler}`);
+
+ let notificationTelemetry = {
+ shown: kNotificationShown.notShown,
+ action: kNotificationAction.noAction,
+ };
+ if ((browser == "edge-chrome" && previousBrowser == "firefox") || force) {
+ lazy.log.info("Showing default browser intervention notification.");
+
+ const alertName = "default_agent_intervention";
+ let notification = showNotification(alertName);
+ let timeout = makeTimeout(alertName);
+
+ notificationTelemetry = await Promise.race([notification, timeout]);
+ }
+ lazy.log.debug(`Notification telemetry: ${notificationTelemetry}`);
+
+ if (
+ notificationTelemetry.action ==
+ kNotificationAction.makeFirefoxDefaultButton ||
+ notificationTelemetry.action == kNotificationAction.toastClicked
+ ) {
+ await lazy.ShellService.setDefaultBrowser(false).catch(e => {
+ lazy.log.error(`setDefaultBrowser failed: ${e}`);
+ });
+ }
+
+ defaultAgent.sendPing(
+ browser,
+ previousBrowser,
+ defaultPdfHandler,
+ notificationTelemetry.shown,
+ notificationTelemetry.action
+ );
+}
+
+async function showNotification(name) {
+ let notificationTelemetry = {
+ shown: kNotificationShown.error,
+ action: kNotificationAction.noAction,
+ };
+
+ // Bug 1868714: We disable the notification server to defer on changes
+ // necessary for it to work with Background Tasks.
+ try {
+ lazy.log.debug("Disabling notification server.");
+ Services.prefs.setBoolPref(
+ "alerts.useSystemBackend.windows.notificationserver.enabled",
+ false
+ );
+
+ const l10n = new Localization([
+ "branding/brand.ftl",
+ // Background tasks are only used in a context where browser refs are
+ // present; that it's in toolkit instead of browser is a historical
+ // artifact of the default agent having previously been a
+ // standalone application.
+ // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
+ "browser/backgroundtasks/defaultagent.ftl",
+ ]);
+ let [title, body, yesButtonText, noButtonText] = await l10n.formatValues([
+ { id: "default-browser-notification-header-text" },
+ { id: "default-browser-notification-body-text" },
+ { id: "default-browser-notification-yes-button-text" },
+ { id: "default-browser-notification-no-button-text" },
+ ]);
+
+ let yesAction = "yes-action";
+ let noAction = "no-action";
+
+ let alert = makeAlert({
+ name,
+ title,
+ body,
+ actions: [
+ {
+ action: yesAction,
+ title: yesButtonText,
+ },
+ {
+ action: noAction,
+ title: noButtonText,
+ },
+ ],
+ });
+
+ const { observer, shownPromise } = makeObserver({ yesAction, noAction });
+
+ lazy.AlertsService.showAlert(alert, observer);
+
+ notificationTelemetry = await shownPromise.promise;
+ } catch (e) {
+ if (e.message) {
+ lazy.log.error(e.message);
+ }
+ } finally {
+ // Reset the pref so we can assume the default value in the future.
+ lazy.log.debug("Reenabling notification server.");
+ Services.prefs.clearUserPref(
+ "alerts.useSystemBackend.windows.notificationserver.enabled"
+ );
+ }
+
+ return notificationTelemetry;
+}
+
+function makeAlert(options) {
+ let winalert = Cc["@mozilla.org/windows-alert-notification;1"].createInstance(
+ Ci.nsIWindowsAlertNotification
+ );
+ winalert.handleActions = true;
+ winalert.imagePlacement = winalert.eIcon;
+
+ let alert = winalert.QueryInterface(Ci.nsIAlertNotification);
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ alert.init(
+ options.name,
+ "chrome://branding/content/about-logo@2x.png",
+ options.title,
+ options.body,
+ true /* aTextClickable */,
+ null /* aCookie */,
+ null /* aDir */,
+ null /* aLang */,
+ null /* aData */,
+ systemPrincipal,
+ null /* aInPrivateBrowsing */,
+ true /* aRequireInteraction */
+ );
+
+ alert.actions = options.actions;
+
+ return alert;
+}
+
+function makeObserver(actions) {
+ let shownPromise = Promise.withResolvers();
+
+ // We'll receive multiple callbacks which individually might indicate an
+ // interaction. Only log the first one to disambiguate and reduce noise.
+ let firstInteraction = true;
+ let logFirstInteraction = message => {
+ if (firstInteraction) {
+ lazy.log.debug(message);
+ firstInteraction = false;
+ }
+ };
+
+ let observer = (subject, topic, data) => {
+ switch (topic) {
+ case "alertactioncallback":
+ switch (data) {
+ case actions.yesAction:
+ logFirstInteraction(
+ 'Notification "yes" button clicked, setting default browser.'
+ );
+ shownPromise.resolve({
+ shown: kNotificationShown.shown,
+ action: kNotificationAction.makeFirefoxDefaultButton,
+ });
+ break;
+ case actions.noAction:
+ logFirstInteraction("Notification dismissed by button.");
+ shownPromise.resolve({
+ shown: kNotificationShown.shown,
+ action: kNotificationAction.dismissedByButton,
+ });
+ break;
+ default:
+ lazy.log.error(`Unrecognized notification action ${data}`);
+ throw new Error(`Unexpected notification action received: ${data}`);
+ }
+ break;
+ case "alertclickcallback":
+ logFirstInteraction(
+ "Notification body clicked, setting default browser."
+ );
+ shownPromise.resolve({
+ shown: kNotificationShown.shown,
+ action: kNotificationAction.toastClicked,
+ });
+ break;
+ case "alerterror":
+ lazy.log.error("Error showing notification.");
+ shownPromise.resolve({
+ shown: kNotificationShown.error,
+ action: kNotificationAction.noAction,
+ });
+ break;
+ case "alertfinished":
+ logFirstInteraction("Notification dismissed from action center.");
+ shownPromise.resolve({
+ shown: kNotificationShown.shown,
+ action: kNotificationAction.dismissedToActionCenter,
+ });
+ break;
+ }
+ };
+
+ return { observer, shownPromise };
+}
+
+function makeTimeout(alertName) {
+ return new Promise(resolve => {
+ // If the notification hasn't been activated or dismissed within 12 hours,
+ // stop waiting for it.
+ let timeoutMs = kNotificationTimeoutMs;
+
+ // Allow overriding the notification timeout fron an environment variable.
+ const envTimeoutKey = "MOZ_NOTIFICATION_TIMEOUT_MS";
+ if (Services.env.exists(envTimeoutKey)) {
+ let envTimeoutValue = Services.env.get(envTimeoutKey);
+ if (!isNaN(envTimeoutValue)) {
+ timeoutMs = Number(envTimeoutValue);
+ } else {
+ lazy.log.error(
+ `Environment variable ${envTimeoutKey}=${envTimeoutValue} is not a number.`
+ );
+ }
+ }
+ lazy.log.info(`Registering notification timeout in ${timeoutMs}ms`);
+
+ lazy.setTimeout(() => {
+ lazy.log.warn(`Notification timed out after ${timeoutMs}ms`);
+
+ lazy.AlertsService.closeAlert(alertName);
+
+ resolve({
+ shown: kNotificationShown.shown,
+ action: kNotificationAction.dismissedByTimeout,
+ });
+ }, timeoutMs);
+ });
+}
diff --git a/toolkit/mozapps/defaultagent/Cache.cpp b/toolkit/mozapps/defaultagent/Cache.cpp
new file mode 100644
index 0000000000..1a323e54d9
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Cache.cpp
@@ -0,0 +1,594 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "Cache.h"
+
+#include <algorithm>
+
+#include "common.h"
+#include "EventLog.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla::default_agent {
+
+// Cache entry version documentation:
+// Version 1:
+// The version number is written explicitly when version 1 cache entries are
+// migrated, but in their original location there is no version key.
+// Required Keys:
+// CacheEntryVersion: <DWORD>
+// NotificationType: <string>
+// NotificationShown: <string>
+// NotificationAction: <string>
+// Version 2:
+// Required Keys:
+// CacheEntryVersion: <DWORD>
+// NotificationType: <string>
+// NotificationShown: <string>
+// NotificationAction: <string>
+// PrevNotificationAction: <string>
+
+static std::wstring MakeVersionedRegSubKey(const wchar_t* baseKey) {
+ std::wstring key;
+ if (baseKey) {
+ key = baseKey;
+ } else {
+ key = Cache::kDefaultPingCacheRegKey;
+ }
+ key += L"\\version";
+ key += std::to_wstring(Cache::kVersion);
+ return key;
+}
+
+Cache::Cache(const wchar_t* cacheRegKey /* = nullptr */)
+ : mCacheRegKey(MakeVersionedRegSubKey(cacheRegKey)),
+ mInitializeResult(mozilla::Nothing()),
+ mCapacity(Cache::kDefaultCapacity),
+ mFront(0),
+ mSize(0) {}
+
+Cache::~Cache() {}
+
+VoidResult Cache::Init() {
+ if (mInitializeResult.isSome()) {
+ HRESULT hr = mInitializeResult.value();
+ if (FAILED(hr)) {
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ } else {
+ return mozilla::Ok();
+ }
+ }
+
+ VoidResult result = SetupCache();
+ if (result.isErr()) {
+ HRESULT hr = result.inspectErr().AsHResult();
+ mInitializeResult = mozilla::Some(hr);
+ return result;
+ }
+
+ // At this point, the cache is ready to use, so mark the initialization as
+ // complete. This is important so that when we attempt migration, below,
+ // the migration's attempts to write to the cache don't try to initialize
+ // the cache again.
+ mInitializeResult = mozilla::Some(S_OK);
+
+ // Ignore the result of the migration. If we failed to migrate, there may be
+ // some data loss. But that's better than failing to ever use the new cache
+ // just because there's something wrong with the old one.
+ mozilla::Unused << MaybeMigrateVersion1();
+
+ return mozilla::Ok();
+}
+
+// If the setting does not exist, the default value is written and returned.
+DwordResult Cache::EnsureDwordSetting(const wchar_t* regName,
+ uint32_t defaultValue) {
+ MaybeDwordResult readResult = RegistryGetValueDword(
+ IsPrefixed::Unprefixed, regName, mCacheRegKey.c_str());
+ if (readResult.isErr()) {
+ HRESULT hr = readResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Failed to read setting \"%s\": %#X", regName, hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+ mozilla::Maybe<uint32_t> maybeValue = readResult.unwrap();
+ if (maybeValue.isSome()) {
+ return maybeValue.value();
+ }
+
+ VoidResult writeResult = RegistrySetValueDword(
+ IsPrefixed::Unprefixed, regName, defaultValue, mCacheRegKey.c_str());
+ if (writeResult.isErr()) {
+ HRESULT hr = writeResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Failed to write setting \"%s\": %#X", regName, hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+ return defaultValue;
+}
+
+// This function does two things:
+// 1. It creates and sets the registry values used by the cache, if they don't
+// already exist.
+// 2. If the the values already existed, it reads the settings of the cache
+// into their member variables.
+VoidResult Cache::SetupCache() {
+ DwordResult result =
+ EnsureDwordSetting(Cache::kCapacityRegName, Cache::kDefaultCapacity);
+ if (result.isErr()) {
+ return mozilla::Err(result.unwrapErr());
+ }
+ mCapacity = std::min(result.unwrap(), Cache::kMaxCapacity);
+
+ result = EnsureDwordSetting(Cache::kFrontRegName, 0);
+ if (result.isErr()) {
+ return mozilla::Err(result.unwrapErr());
+ }
+ mFront = std::min(result.unwrap(), Cache::kMaxCapacity - 1);
+
+ result = EnsureDwordSetting(Cache::kSizeRegName, 0);
+ if (result.isErr()) {
+ return mozilla::Err(result.unwrapErr());
+ }
+ mSize = std::min(result.unwrap(), mCapacity);
+
+ return mozilla::Ok();
+}
+
+static MaybeStringResult ReadVersion1CacheKey(const wchar_t* baseRegKeyName,
+ uint32_t index) {
+ std::wstring regName = Cache::kVersion1KeyPrefix;
+ regName += baseRegKeyName;
+ regName += std::to_wstring(index);
+
+ MaybeStringResult result =
+ RegistryGetValueString(IsPrefixed::Unprefixed, regName.c_str());
+ if (result.isErr()) {
+ HRESULT hr = result.inspectErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Failed to read \"%s\": %#X", regName.c_str(), hr);
+ }
+ return result;
+}
+
+static VoidResult DeleteVersion1CacheKey(const wchar_t* baseRegKeyName,
+ uint32_t index) {
+ std::wstring regName = Cache::kVersion1KeyPrefix;
+ regName += baseRegKeyName;
+ regName += std::to_wstring(index);
+
+ VoidResult result =
+ RegistryDeleteValue(IsPrefixed::Unprefixed, regName.c_str());
+ if (result.isErr()) {
+ HRESULT hr = result.inspectErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Failed to delete \"%s\": %#X", regName.c_str(), hr);
+ }
+ return result;
+}
+
+static VoidResult DeleteVersion1CacheEntry(uint32_t index) {
+ VoidResult typeResult =
+ DeleteVersion1CacheKey(Cache::kNotificationTypeKey, index);
+ VoidResult shownResult =
+ DeleteVersion1CacheKey(Cache::kNotificationShownKey, index);
+ VoidResult actionResult =
+ DeleteVersion1CacheKey(Cache::kNotificationActionKey, index);
+
+ if (typeResult.isErr()) {
+ return typeResult;
+ }
+ if (shownResult.isErr()) {
+ return shownResult;
+ }
+ return actionResult;
+}
+
+VoidResult Cache::MaybeMigrateVersion1() {
+ for (uint32_t index = 0; index < Cache::kVersion1MaxSize; ++index) {
+ MaybeStringResult typeResult =
+ ReadVersion1CacheKey(Cache::kNotificationTypeKey, index);
+ if (typeResult.isErr()) {
+ return mozilla::Err(typeResult.unwrapErr());
+ }
+ MaybeString maybeType = typeResult.unwrap();
+
+ MaybeStringResult shownResult =
+ ReadVersion1CacheKey(Cache::kNotificationShownKey, index);
+ if (shownResult.isErr()) {
+ return mozilla::Err(shownResult.unwrapErr());
+ }
+ MaybeString maybeShown = shownResult.unwrap();
+
+ MaybeStringResult actionResult =
+ ReadVersion1CacheKey(Cache::kNotificationActionKey, index);
+ if (actionResult.isErr()) {
+ return mozilla::Err(actionResult.unwrapErr());
+ }
+ MaybeString maybeAction = actionResult.unwrap();
+
+ if (maybeType.isSome() && maybeShown.isSome() && maybeAction.isSome()) {
+ // If something goes wrong, we'd rather lose a little data than migrate
+ // over and over again. So delete the old entry before we add the new one.
+ VoidResult result = DeleteVersion1CacheEntry(index);
+ if (result.isErr()) {
+ return result;
+ }
+
+ VersionedEntry entry = VersionedEntry{
+ .entryVersion = 1,
+ .notificationType = maybeType.value(),
+ .notificationShown = maybeShown.value(),
+ .notificationAction = maybeAction.value(),
+ .prevNotificationAction = mozilla::Nothing(),
+ };
+ result = VersionedEnqueue(entry);
+ if (result.isErr()) {
+ // We already deleted the version 1 cache entry. No real reason to abort
+ // now. May as well keep attempting to migrate.
+ LOG_ERROR_MESSAGE(L"Warning: Version 1 cache entry %u dropped: %#X",
+ index, result.unwrapErr().AsHResult());
+ }
+ } else if (maybeType.isNothing() && maybeShown.isNothing() &&
+ maybeAction.isNothing()) {
+ // Looks like we've reached the end of the version 1 cache.
+ break;
+ } else {
+ // This cache entry seems to be missing a key. Just drop it.
+ LOG_ERROR_MESSAGE(
+ L"Warning: Version 1 cache entry %u dropped due to missing keys",
+ index);
+ mozilla::Unused << DeleteVersion1CacheEntry(index);
+ }
+ }
+ return mozilla::Ok();
+}
+
+std::wstring Cache::MakeEntryRegKeyName(uint32_t index) {
+ std::wstring regName = mCacheRegKey;
+ regName += L'\\';
+ regName += std::to_wstring(index);
+ return regName;
+}
+
+VoidResult Cache::WriteEntryKeys(uint32_t index, const VersionedEntry& entry) {
+ std::wstring subKey = MakeEntryRegKeyName(index);
+
+ VoidResult result =
+ RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kEntryVersionKey,
+ entry.entryVersion, subKey.c_str());
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Unable to write entry version to index %u: %#X", index,
+ result.inspectErr().AsHResult());
+ return result;
+ }
+
+ result = RegistrySetValueString(
+ IsPrefixed::Unprefixed, Cache::kNotificationTypeKey,
+ entry.notificationType.c_str(), subKey.c_str());
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Unable to write notification type to index %u: %#X",
+ index, result.inspectErr().AsHResult());
+ return result;
+ }
+
+ result = RegistrySetValueString(
+ IsPrefixed::Unprefixed, Cache::kNotificationShownKey,
+ entry.notificationShown.c_str(), subKey.c_str());
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Unable to write notification shown to index %u: %#X",
+ index, result.inspectErr().AsHResult());
+ return result;
+ }
+
+ result = RegistrySetValueString(
+ IsPrefixed::Unprefixed, Cache::kNotificationActionKey,
+ entry.notificationAction.c_str(), subKey.c_str());
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Unable to write notification type to index %u: %#X",
+ index, result.inspectErr().AsHResult());
+ return result;
+ }
+
+ if (entry.prevNotificationAction.isSome()) {
+ result = RegistrySetValueString(
+ IsPrefixed::Unprefixed, Cache::kPrevNotificationActionKey,
+ entry.prevNotificationAction.value().c_str(), subKey.c_str());
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(
+ L"Unable to write prev notification type to index %u: %#X", index,
+ result.inspectErr().AsHResult());
+ return result;
+ }
+ }
+
+ return mozilla::Ok();
+}
+
+// Returns success on an attempt to delete a non-existent entry.
+VoidResult Cache::DeleteEntry(uint32_t index) {
+ std::wstring key = AGENT_REGKEY_NAME;
+ key += L'\\';
+ key += MakeEntryRegKeyName(index);
+ // We could probably just delete they key here, rather than use this function,
+ // which deletes keys recursively. But this mechanism allows future entry
+ // versions to contain sub-keys without causing problems for older versions.
+ LSTATUS ls = RegDeleteTreeW(HKEY_CURRENT_USER, key.c_str());
+ if (ls != ERROR_SUCCESS && ls != ERROR_FILE_NOT_FOUND) {
+ return mozilla::Err(mozilla::WindowsError::FromWin32Error(ls));
+ }
+ return mozilla::Ok();
+}
+
+VoidResult Cache::SetFront(uint32_t newFront) {
+ VoidResult result =
+ RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kFrontRegName,
+ newFront, mCacheRegKey.c_str());
+ if (result.isOk()) {
+ mFront = newFront;
+ }
+ return result;
+}
+
+VoidResult Cache::SetSize(uint32_t newSize) {
+ VoidResult result =
+ RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kSizeRegName,
+ newSize, mCacheRegKey.c_str());
+ if (result.isOk()) {
+ mSize = newSize;
+ }
+ return result;
+}
+
+// The entry passed to this function MUST already be valid. This function does
+// not do any validation internally. We must not, for example, pass an entry
+// to it with a version of 2 and a prevNotificationAction of mozilla::Nothing()
+// because a version 2 entry requires that key.
+VoidResult Cache::VersionedEnqueue(const VersionedEntry& entry) {
+ VoidResult result = Init();
+ if (result.isErr()) {
+ return result;
+ }
+
+ if (mSize >= mCapacity) {
+ LOG_ERROR_MESSAGE(L"Attempted to add an entry to the cache, but it's full");
+ return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS));
+ }
+
+ uint32_t index = (mFront + mSize) % mCapacity;
+
+ // We really don't want to write to a location that has stale cache entry data
+ // already lying around.
+ result = DeleteEntry(index);
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Unable to remove stale entry: %#X",
+ result.inspectErr().AsHResult());
+ return result;
+ }
+
+ result = WriteEntryKeys(index, entry);
+ if (result.isErr()) {
+ // We might have written a partial key. Attempt to clean up after ourself.
+ mozilla::Unused << DeleteEntry(index);
+ return result;
+ }
+
+ result = SetSize(mSize + 1);
+ if (result.isErr()) {
+ // If we failed to write the size, the new entry was not added successfully.
+ // Attempt to clean up after ourself.
+ mozilla::Unused << DeleteEntry(index);
+ return result;
+ }
+
+ return mozilla::Ok();
+}
+
+VoidResult Cache::Enqueue(const Cache::Entry& entry) {
+ Cache::VersionedEntry vEntry = Cache::VersionedEntry{
+ .entryVersion = Cache::kEntryVersion,
+ .notificationType = entry.notificationType,
+ .notificationShown = entry.notificationShown,
+ .notificationAction = entry.notificationAction,
+ .prevNotificationAction = mozilla::Some(entry.prevNotificationAction),
+ };
+ return VersionedEnqueue(vEntry);
+}
+
+VoidResult Cache::DiscardFront() {
+ if (mSize < 1) {
+ LOG_ERROR_MESSAGE(L"Attempted to discard entry from an empty cache");
+ return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS));
+ }
+ // It's not a huge deal if we can't delete this. Moving mFront will result in
+ // it being excluded from the cache anyways. We'll try to delete it again
+ // anyways if we try to write to this index again.
+ mozilla::Unused << DeleteEntry(mFront);
+
+ VoidResult result = SetSize(mSize - 1);
+ // We don't really need to bother moving mFront to the next index if the cache
+ // is empty.
+ if (result.isErr() || mSize == 0) {
+ return result;
+ }
+ result = SetFront((mFront + 1) % mCapacity);
+ if (result.isErr()) {
+ // If we failed to set the front after we set the size, the cache is
+ // in an inconsistent state.
+ // But, even if the cache is inconsistent, we'll likely lose some data, but
+ // we should eventually be able to recover. Any expected entries with no
+ // data will be discarded and any unexpected entries with data will be
+ // cleared out before we write data there.
+ LOG_ERROR_MESSAGE(L"Cache inconsistent: Updated Size but not Front: %#X",
+ result.inspectErr().AsHResult());
+ }
+ return result;
+}
+
+/**
+ * This function reads a DWORD cache key's value and returns it. If the expected
+ * argument is true and the key is missing, this will delete the entire entry
+ * and return mozilla::Nothing().
+ */
+MaybeDwordResult Cache::ReadEntryKeyDword(const std::wstring& regKey,
+ const wchar_t* regName,
+ bool expected) {
+ MaybeDwordResult result =
+ RegistryGetValueDword(IsPrefixed::Unprefixed, regName, regKey.c_str());
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Failed to read \"%s\" from \"%s\": %#X", regName,
+ regKey.c_str(), result.inspectErr().AsHResult());
+ return mozilla::Err(result.unwrapErr());
+ }
+ MaybeDword maybeValue = result.unwrap();
+ if (expected && maybeValue.isNothing()) {
+ LOG_ERROR_MESSAGE(L"Missing expected value \"%s\" from \"%s\"", regName,
+ regKey.c_str());
+ VoidResult result = DiscardFront();
+ if (result.isErr()) {
+ return mozilla::Err(result.unwrapErr());
+ }
+ }
+ return maybeValue;
+}
+
+/**
+ * This function reads a string cache key's value and returns it. If the
+ * expected argument is true and the key is missing, this will delete the entire
+ * entry and return mozilla::Nothing().
+ */
+MaybeStringResult Cache::ReadEntryKeyString(const std::wstring& regKey,
+ const wchar_t* regName,
+ bool expected) {
+ MaybeStringResult result =
+ RegistryGetValueString(IsPrefixed::Unprefixed, regName, regKey.c_str());
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Failed to read \"%s\" from \"%s\": %#X", regName,
+ regKey.c_str(), result.inspectErr().AsHResult());
+ return mozilla::Err(result.unwrapErr());
+ }
+ MaybeString maybeValue = result.unwrap();
+ if (expected && maybeValue.isNothing()) {
+ LOG_ERROR_MESSAGE(L"Missing expected value \"%s\" from \"%s\"", regName,
+ regKey.c_str());
+ VoidResult result = DiscardFront();
+ if (result.isErr()) {
+ return mozilla::Err(result.unwrapErr());
+ }
+ }
+ return maybeValue;
+}
+
+Cache::MaybeEntryResult Cache::Dequeue() {
+ VoidResult result = Init();
+ if (result.isErr()) {
+ return mozilla::Err(result.unwrapErr());
+ }
+
+ std::wstring subKey = MakeEntryRegKeyName(mFront);
+
+ // We are going to read within a loop so that if we find incomplete entries,
+ // we can just discard them and try to read the next entry. We'll put a limit
+ // on the maximum number of times this loop can possibly run so that if
+ // something goes horribly wrong, we don't loop forever. If we exit this loop
+ // without returning, it means that not only were we not able to read
+ // anything, but something very unexpected happened.
+ // We are going to potentially loop over this mCapacity + 1 times so that if
+ // we end up discarding every item in the cache, we return mozilla::Nothing()
+ // rather than an error.
+ for (uint32_t i = 0; i <= mCapacity; ++i) {
+ if (mSize == 0) {
+ return MaybeEntry(mozilla::Nothing());
+ }
+
+ Cache::VersionedEntry entry;
+
+ // CacheEntryVersion
+ MaybeDwordResult dResult =
+ ReadEntryKeyDword(subKey, Cache::kEntryVersionKey, true);
+ if (dResult.isErr()) {
+ return mozilla::Err(dResult.unwrapErr());
+ }
+ MaybeDword maybeDValue = dResult.unwrap();
+ if (maybeDValue.isNothing()) {
+ // Note that we only call continue in this function after DiscardFront()
+ // has been called (either directly, or by one of the ReadEntryKey.*
+ // functions). So the continue call results in attempting to read the
+ // next entry in the cache.
+ continue;
+ }
+ entry.entryVersion = maybeDValue.value();
+ if (entry.entryVersion < 1) {
+ LOG_ERROR_MESSAGE(L"Invalid entry version of %u in \"%s\"",
+ entry.entryVersion, subKey.c_str());
+ VoidResult result = DiscardFront();
+ if (result.isErr()) {
+ return mozilla::Err(result.unwrapErr());
+ }
+ continue;
+ }
+
+ // NotificationType
+ MaybeStringResult sResult =
+ ReadEntryKeyString(subKey, Cache::kNotificationTypeKey, true);
+ if (sResult.isErr()) {
+ return mozilla::Err(sResult.unwrapErr());
+ }
+ MaybeString maybeSValue = sResult.unwrap();
+ if (maybeSValue.isNothing()) {
+ continue;
+ }
+ entry.notificationType = maybeSValue.value();
+
+ // NotificationShown
+ sResult = ReadEntryKeyString(subKey, Cache::kNotificationShownKey, true);
+ if (sResult.isErr()) {
+ return mozilla::Err(sResult.unwrapErr());
+ }
+ maybeSValue = sResult.unwrap();
+ if (maybeSValue.isNothing()) {
+ continue;
+ }
+ entry.notificationShown = maybeSValue.value();
+
+ // NotificationAction
+ sResult = ReadEntryKeyString(subKey, Cache::kNotificationActionKey, true);
+ if (sResult.isErr()) {
+ return mozilla::Err(sResult.unwrapErr());
+ }
+ maybeSValue = sResult.unwrap();
+ if (maybeSValue.isNothing()) {
+ continue;
+ }
+ entry.notificationAction = maybeSValue.value();
+
+ // PrevNotificationAction
+ bool expected =
+ entry.entryVersion >= Cache::kInitialVersionPrevNotificationActionKey;
+ sResult =
+ ReadEntryKeyString(subKey, Cache::kPrevNotificationActionKey, expected);
+ if (sResult.isErr()) {
+ return mozilla::Err(sResult.unwrapErr());
+ }
+ maybeSValue = sResult.unwrap();
+ if (expected && maybeSValue.isNothing()) {
+ continue;
+ }
+ entry.prevNotificationAction = maybeSValue;
+
+ // We successfully read the entry. Now we need to remove it from the cache.
+ VoidResult result = DiscardFront();
+ if (result.isErr()) {
+ // If we aren't able to remove the entry from the cache, don't return it.
+ // We don't want to return the same item over and over again if we get
+ // into a bad state.
+ return mozilla::Err(result.unwrapErr());
+ }
+
+ return mozilla::Some(entry);
+ }
+
+ LOG_ERROR_MESSAGE(L"Unexpected: This line shouldn't be reached");
+ return mozilla::Err(mozilla::WindowsError::FromHResult(E_FAIL));
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/Cache.h b/toolkit/mozapps/defaultagent/Cache.h
new file mode 100644
index 0000000000..1deacb17df
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Cache.h
@@ -0,0 +1,189 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_CACHE_H__
+#define __DEFAULT_BROWSER_AGENT_CACHE_H__
+
+#include <cstdint>
+#include <string>
+#include <windows.h>
+
+#include "Registry.h"
+
+namespace mozilla::default_agent {
+
+using DwordResult = mozilla::WindowsErrorResult<uint32_t>;
+
+/**
+ * This cache functions as a FIFO queue which writes its data to the Windows
+ * registry.
+ *
+ * Note that the cache is not thread-safe, so it is recommended that the WDBA's
+ * RegistryMutex be acquired before accessing it.
+ *
+ * Some of the terminology used in this module is a easy to mix up, so let's
+ * just be clear about it:
+ * - registry key/sub-key
+ * A registry key is sort of like the registry's equivalent of a
+ * directory. It can contain values, each of which is made up of a name
+ * and corresponding data. We may also refer to a "sub-key", meaning a
+ * registry key nested in a registry key.
+ * - cache key/entry key
+ * A cache key refers to the string that we use to look up a single
+ * element of cache entry data. Example: "CacheEntryVersion"
+ * - entry
+ * This refers to an entire record stored using Cache::Enqueue or retrieved
+ * using Cache::Dequeue. It consists of numerous cache keys and their
+ * corresponding data.
+ *
+ * The first version of this cache was problematic because of how hard it was to
+ * extend. This version attempts to overcome this. It first migrates all data
+ * out of the version 1 cache. This means that the stored ping data will not
+ * be accessible to out-of-date clients, but presumably they will eventually
+ * be updated or the up-to-date client that performed the migration will send
+ * the pings itself. Because the WDBA telemetry has no client ID, all analysis
+ * is stateless, so even if the other clients send some pings before the stored
+ * ones get sent, that's ok. The ordering isn't really important.
+ *
+ * This version of the cache attempts to correct the problem of how hard it was
+ * to extend the old cache. The biggest problem that the old cache had was that
+ * when it dequeued data it had to shift data, but it wouldn't shift keys that
+ * it didn't know about, causing them to become associated with the wrong cache
+ * entries.
+ *
+ * Version 2 of the cache will make 4 improvements to attempt to avoid problems
+ * like this in the future:
+ * 1. Each cache entry will get its own registry key. This will help to keep
+ * cache entries isolated from each other.
+ * 2. Each cache entry will include version data so that we know what cache
+ * keys to expect when we read it.
+ * 3. Rather than having to shift every entry every time we dequeue, we will
+ * implement a circular queue so that we just have to update what index
+ * currently represents the front
+ * 4. We will store the cache capacity in the cache so that we can expand the
+ * cache later, if we want, without breaking previous versions.
+ */
+class Cache {
+ public:
+ // cacheRegKey is the registry sub-key that the cache will be stored in. If
+ // null is passed (the default), we will use the default cache name. This is
+ // what ought to be used in production. When testing, we will pass a different
+ // key in so that our testing caches don't conflict with each other or with
+ // a possible production cache on the test machine.
+ explicit Cache(const wchar_t* cacheRegKey = nullptr);
+ ~Cache();
+
+ // The version of the cache (not to be confused with the version of the cache
+ // entries). This should only be incremented if we need to make breaking
+ // changes that require migration to a new cache location, like we did between
+ // versions 1 and 2. This value will be used as part of the sub-key that the
+ // cache is stored in (ex: "PingCache\version2").
+ static constexpr const uint32_t kVersion = 2;
+ // This value will be written into each entry. This allows us to know what
+ // cache keys to expect in the event that additional cache keys are added in
+ // later entry versions.
+ static constexpr const uint32_t kEntryVersion = 2;
+ static constexpr const uint32_t kDefaultCapacity = 2;
+ // We want to allow the cache to be expandable, but we don't really want it to
+ // be infinitely expandable. So we'll set an upper bound.
+ static constexpr const uint32_t kMaxCapacity = 100;
+ static constexpr const wchar_t* kDefaultPingCacheRegKey = L"PingCache";
+
+ // Used to read the version 1 cache entries during data migration. Full cache
+ // key names are formatted like: "<keyPrefix><baseKeyName><cacheIndex>"
+ // For example: "PingCacheNotificationType0"
+ static constexpr const wchar_t* kVersion1KeyPrefix = L"PingCache";
+ static constexpr const uint32_t kVersion1MaxSize = 2;
+
+ static constexpr const wchar_t* kCapacityRegName = L"Capacity";
+ static constexpr const wchar_t* kFrontRegName = L"Front";
+ static constexpr const wchar_t* kSizeRegName = L"Size";
+
+ // Cache Entry keys
+ static constexpr const wchar_t* kEntryVersionKey = L"CacheEntryVersion";
+ // Note that the next 3 must also match the base key names from version 1
+ // since we use them to construct those key names.
+ static constexpr const wchar_t* kNotificationTypeKey = L"NotificationType";
+ static constexpr const wchar_t* kNotificationShownKey = L"NotificationShown";
+ static constexpr const wchar_t* kNotificationActionKey =
+ L"NotificationAction";
+ static constexpr const wchar_t* kPrevNotificationActionKey =
+ L"PrevNotificationAction";
+
+ // The version key wasn't added until version 2, but we add it to the version
+ // 1 entries when migrating them to the cache.
+ static constexpr const uint32_t kInitialVersionEntryVersionKey = 1;
+ static constexpr const uint32_t kInitialVersionNotificationTypeKey = 1;
+ static constexpr const uint32_t kInitialVersionNotificationShownKey = 1;
+ static constexpr const uint32_t kInitialVersionNotificationActionKey = 1;
+ static constexpr const uint32_t kInitialVersionPrevNotificationActionKey = 2;
+
+ // We have two cache entry structs: one for the current version, and one
+ // generic one that can handle any version. There are a couple of reasons
+ // for this:
+ // - We only want to support writing the current version, but we want to
+ // support reading any version.
+ // - It makes things a bit nicer for the caller when Enqueue-ing, since
+ // they don't have to set the version or wrap values that were added
+ // later in a mozilla::Maybe.
+ // - It keeps us from having to worry about writing an invalid cache entry,
+ // such as one that claims to be version 2, but doesn't have
+ // prevNotificationAction.
+ // Note that the entry struct for the current version does not contain a
+ // version member value because we already know that its version is equal to
+ // Cache::kEntryVersion.
+ struct Entry {
+ std::string notificationType;
+ std::string notificationShown;
+ std::string notificationAction;
+ std::string prevNotificationAction;
+ };
+ struct VersionedEntry {
+ uint32_t entryVersion;
+ std::string notificationType;
+ std::string notificationShown;
+ std::string notificationAction;
+ mozilla::Maybe<std::string> prevNotificationAction;
+ };
+
+ using MaybeEntry = mozilla::Maybe<VersionedEntry>;
+ using MaybeEntryResult = mozilla::WindowsErrorResult<MaybeEntry>;
+
+ VoidResult Init();
+ VoidResult Enqueue(const Entry& entry);
+ MaybeEntryResult Dequeue();
+
+ private:
+ const std::wstring mCacheRegKey;
+
+ // We can't easily copy a VoidResult, so just store the raw HRESULT here.
+ mozilla::Maybe<HRESULT> mInitializeResult;
+ // How large the cache will grow before it starts rejecting new entries.
+ uint32_t mCapacity;
+ // The index of the first present cache entry.
+ uint32_t mFront;
+ // How many entries are present in the cache.
+ uint32_t mSize;
+
+ DwordResult EnsureDwordSetting(const wchar_t* regName, uint32_t defaultValue);
+ VoidResult SetupCache();
+ VoidResult MaybeMigrateVersion1();
+ std::wstring MakeEntryRegKeyName(uint32_t index);
+ VoidResult WriteEntryKeys(uint32_t index, const VersionedEntry& entry);
+ VoidResult DeleteEntry(uint32_t index);
+ VoidResult SetFront(uint32_t newFront);
+ VoidResult SetSize(uint32_t newSize);
+ VoidResult VersionedEnqueue(const VersionedEntry& entry);
+ VoidResult DiscardFront();
+ MaybeDwordResult ReadEntryKeyDword(const std::wstring& regKey,
+ const wchar_t* regName, bool expected);
+ MaybeStringResult ReadEntryKeyString(const std::wstring& regKey,
+ const wchar_t* regName, bool expected);
+};
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_AGENT_CACHE_H__
diff --git a/toolkit/mozapps/defaultagent/DefaultAgent.cpp b/toolkit/mozapps/defaultagent/DefaultAgent.cpp
new file mode 100644
index 0000000000..2ebb5e466e
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/DefaultAgent.cpp
@@ -0,0 +1,491 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include <windows.h>
+#include <shlwapi.h>
+#include <objbase.h>
+#include <string.h>
+#include <vector>
+
+#include "nsAutoRef.h"
+#include "nsDebug.h"
+#include "nsProxyRelease.h"
+#include "nsWindowsHelpers.h"
+#include "nsString.h"
+
+#include "common.h"
+#include "DefaultBrowser.h"
+#include "DefaultPDF.h"
+#include "EventLog.h"
+#include "Notification.h"
+#include "Policy.h"
+#include "Registry.h"
+#include "ScheduledTask.h"
+#include "ScheduledTaskRemove.h"
+#include "SetDefaultBrowser.h"
+#include "Telemetry.h"
+#include "xpcpublic.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/ErrorResult.h"
+
+#include "DefaultAgent.h"
+
+// The AGENT_REGKEY_NAME is dependent on MOZ_APP_VENDOR and MOZ_APP_BASENAME,
+// so using those values in the mutex name prevents waiting on processes that
+// are using completely different data.
+#define REGISTRY_MUTEX_NAME \
+ L"" MOZ_APP_VENDOR MOZ_APP_BASENAME L"DefaultBrowserAgentRegistryMutex"
+// How long to wait on the registry mutex before giving up on it. This should
+// be short. Although the WDBA runs in the background, uninstallation happens
+// synchronously in the foreground.
+#define REGISTRY_MUTEX_TIMEOUT_MS (3 * 1000)
+
+namespace mozilla::default_agent {
+
+// This class is designed to prevent concurrency problems when accessing the
+// registry. It should be acquired before any usage of unprefixed registry
+// entries.
+class RegistryMutex {
+ private:
+ nsAutoHandle mMutex;
+ bool mLocked;
+
+ public:
+ RegistryMutex() : mMutex(nullptr), mLocked(false) {}
+ ~RegistryMutex() {
+ Release();
+ // nsAutoHandle will take care of closing the mutex's handle.
+ }
+
+ // Returns true on success, false on failure.
+ bool Acquire() {
+ if (mLocked) {
+ return true;
+ }
+
+ if (mMutex.get() == nullptr) {
+ // It seems like we would want to set the second parameter (bInitialOwner)
+ // to TRUE, but the documentation for CreateMutexW suggests that, because
+ // we aren't sure that the mutex doesn't already exist, we can't be sure
+ // whether we got ownership via this mechanism.
+ mMutex.own(CreateMutexW(nullptr, FALSE, REGISTRY_MUTEX_NAME));
+ if (mMutex.get() == nullptr) {
+ LOG_ERROR_MESSAGE(L"Couldn't open registry mutex: %#X", GetLastError());
+ return false;
+ }
+ }
+
+ DWORD mutexStatus =
+ WaitForSingleObject(mMutex.get(), REGISTRY_MUTEX_TIMEOUT_MS);
+ if (mutexStatus == WAIT_OBJECT_0) {
+ mLocked = true;
+ } else if (mutexStatus == WAIT_TIMEOUT) {
+ LOG_ERROR_MESSAGE(L"Timed out waiting for registry mutex");
+ } else if (mutexStatus == WAIT_ABANDONED) {
+ // This isn't really an error for us. No one else is using the registry.
+ // This status code means that we are supposed to check our data for
+ // consistency, but there isn't really anything we can fix here.
+ // This is an indication that an agent crashed though, which is clearly an
+ // error, so log an error message.
+ LOG_ERROR_MESSAGE(L"Found abandoned registry mutex. Continuing...");
+ mLocked = true;
+ } else {
+ // The only other documented status code is WAIT_FAILED. In the case that
+ // we somehow get some other code, that is also an error.
+ LOG_ERROR_MESSAGE(L"Failed to wait on registry mutex: %#X",
+ GetLastError());
+ }
+ return mLocked;
+ }
+
+ bool IsLocked() { return mLocked; }
+
+ void Release() {
+ if (mLocked) {
+ if (mMutex.get() == nullptr) {
+ LOG_ERROR_MESSAGE(L"Unexpectedly missing registry mutex");
+ return;
+ }
+ BOOL success = ReleaseMutex(mMutex.get());
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Failed to release registry mutex");
+ }
+ mLocked = false;
+ }
+ }
+};
+
+// Returns true if the registry value name given is one of the
+// install-directory-prefixed values used by the Windows Default Browser Agent.
+// ex: "C:\Program Files\Mozilla Firefox|PreviousDefault"
+// Returns true
+// ex: "InitialNotificationShown"
+// Returns false
+static bool IsPrefixedValueName(const wchar_t* valueName) {
+ // Prefixed value names use '|' as a delimiter. None of the
+ // non-install-directory-prefixed value names contain one.
+ return wcschr(valueName, L'|') != nullptr;
+}
+
+static void RemoveAllRegistryEntries() {
+ mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
+ if (!PathRemoveFileSpecW(installPath.get())) {
+ return;
+ }
+
+ HKEY rawRegKey = nullptr;
+ if (ERROR_SUCCESS !=
+ RegOpenKeyExW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME, 0,
+ KEY_WRITE | KEY_QUERY_VALUE | KEY_WOW64_64KEY,
+ &rawRegKey)) {
+ return;
+ }
+ nsAutoRegKey regKey(rawRegKey);
+
+ DWORD maxValueNameLen = 0;
+ if (ERROR_SUCCESS != RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr,
+ &maxValueNameLen, nullptr, nullptr,
+ nullptr)) {
+ return;
+ }
+ // The length that RegQueryInfoKeyW returns is without a terminator.
+ maxValueNameLen += 1;
+
+ mozilla::UniquePtr<wchar_t[]> valueName =
+ mozilla::MakeUnique<wchar_t[]>(maxValueNameLen);
+
+ DWORD valueIndex = 0;
+ // Set this to true if we encounter values in this key that are prefixed with
+ // different install directories, indicating that this key is still in use
+ // by other installs.
+ bool keyStillInUse = false;
+
+ while (true) {
+ DWORD valueNameLen = maxValueNameLen;
+ LSTATUS ls =
+ RegEnumValueW(regKey.get(), valueIndex, valueName.get(), &valueNameLen,
+ nullptr, nullptr, nullptr, nullptr);
+ if (ls != ERROR_SUCCESS) {
+ break;
+ }
+
+ if (!wcsnicmp(valueName.get(), installPath.get(),
+ wcslen(installPath.get()))) {
+ RegDeleteValueW(regKey.get(), valueName.get());
+ // Only increment the index if we did not delete this value, because if
+ // we did then the indexes of all the values after that one just got
+ // decremented, meaning the index we already have now refers to a value
+ // that we haven't looked at yet.
+ } else {
+ valueIndex++;
+ if (IsPrefixedValueName(valueName.get())) {
+ // If this is not one of the unprefixed value names, it must be one of
+ // the install-directory prefixed values.
+ keyStillInUse = true;
+ }
+ }
+ }
+
+ regKey.reset();
+
+ // If no other installs are using this key, remove it now.
+ if (!keyStillInUse) {
+ // Use RegDeleteTreeW to remove the cache as well, which is in subkey.
+ RegDeleteTreeW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME);
+ }
+}
+
+// This function adds a registry value with this format:
+// <install-dir>|Installed=1
+// RemoveAllRegistryEntries() determines whether the registry key is in use
+// by other installations by checking for install-directory-prefixed value
+// names. Although Firefox mirrors some preferences into install-directory-
+// prefixed values, the WDBA no longer uses any prefixed values. Adding this one
+// makes uninstallation work as expected slightly more reliably.
+static void WriteInstallationRegistryEntry() {
+ mozilla::WindowsErrorResult<mozilla::Ok> result =
+ RegistrySetValueBool(IsPrefixed::Prefixed, L"Installed", true);
+ if (result.isErr()) {
+ LOG_ERROR_MESSAGE(L"Failed to write installation registry entry: %#X",
+ result.unwrapErr().AsHResult());
+ }
+}
+
+// Returns false (without setting aResult) if reading last run time failed.
+static bool CheckIfAppRanRecently(bool* aResult) {
+ const ULONGLONG kTaskExpirationDays = 90;
+ const ULONGLONG kTaskExpirationSeconds = kTaskExpirationDays * 24 * 60 * 60;
+
+ MaybeQwordResult lastRunTimeResult =
+ RegistryGetValueQword(IsPrefixed::Prefixed, L"AppLastRunTime");
+ if (lastRunTimeResult.isErr()) {
+ return false;
+ }
+ mozilla::Maybe<ULONGLONG> lastRunTimeMaybe = lastRunTimeResult.unwrap();
+ if (!lastRunTimeMaybe.isSome()) {
+ return false;
+ }
+
+ ULONGLONG secondsSinceLastRunTime =
+ SecondsPassedSince(lastRunTimeMaybe.value());
+
+ *aResult = secondsSinceLastRunTime < kTaskExpirationSeconds;
+ return true;
+}
+
+// Use the macro to inject all of the definitions for nsISupports.
+NS_IMPL_ISUPPORTS(DefaultAgent, nsIDefaultAgent)
+
+NS_IMETHODIMP
+DefaultAgent::RegisterTask(const nsAString& aUniqueToken) {
+ // We aren't actually going to check whether we got the mutex here.
+ // Ideally we would acquire it since registration might migrate registry
+ // entries. But it is preferable to ignore a mutex wait timeout here
+ // because:
+ // 1. Otherwise the task doesn't get registered at all
+ // 2. If another installation's agent is holding the mutex, it either
+ // is far enough out of date that it doesn't yet use the migrated
+ // values, or it already did the migration for us.
+ RegistryMutex regMutex;
+ regMutex.Acquire();
+
+ WriteInstallationRegistryEntry();
+
+ HRESULT hr =
+ default_agent::RegisterTask(PromiseFlatString(aUniqueToken).get());
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+DefaultAgent::UpdateTask(const nsAString& aUniqueToken) {
+ // Not checking if we got the mutex for the same reason we didn't in
+ // register-task
+ RegistryMutex regMutex;
+ regMutex.Acquire();
+
+ WriteInstallationRegistryEntry();
+
+ HRESULT hr = default_agent::UpdateTask(PromiseFlatString(aUniqueToken).get());
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+DefaultAgent::UnregisterTask(const nsAString& aUniqueToken) {
+ HRESULT hr = RemoveTasks(PromiseFlatString(aUniqueToken).get(),
+ WhichTasks::WdbaTaskOnly);
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+DefaultAgent::Uninstall(const nsAString& aUniqueToken) {
+ // We aren't actually going to check whether we got the mutex here.
+ // Ideally we would acquire it since we are about to access the registry,
+ // so we would like to block simultaneous users of our registry key.
+ // But there are two reasons that it is preferable to ignore a mutex
+ // wait timeout here:
+ // 1. If we fail to uninstall our prefixed registry entries, the
+ // registry key containing them will never be removed, even when the
+ // last installation is uninstalled.
+ // 2. If we timed out waiting on the mutex, it implies that there are
+ // other installations. If there are other installations, there will
+ // be other prefixed registry entries. If there are other prefixed
+ // registry entries, we won't remove the whole key or touch the
+ // unprefixed entries during uninstallation. Therefore, we should
+ // be able to safely uninstall without stepping on anyone's toes.
+ RegistryMutex regMutex;
+ regMutex.Acquire();
+
+ RemoveAllRegistryEntries();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultAgent::DoTask(const nsAString& aUniqueToken, const bool aForce) {
+ // Acquire() has a short timeout. Since this runs in the background, we
+ // could use a longer timeout in this situation. However, if another
+ // installation's agent is already running, it will update CurrentDefault,
+ // possibly send a ping, and possibly show a notification.
+ // Once all that has happened, there is no real reason to do it again. We
+ // only send one ping per day, so we aren't going to do that again. And
+ // the only time we ever show a second notification is 7 days after the
+ // first one, so we aren't going to do that again either.
+ // If the other process didn't take those actions, there is no reason that
+ // this process would take them.
+ // If the other process fails, this one will most likely fail for the same
+ // reason.
+ // So we'll just bail if we can't get the mutex quickly.
+ RegistryMutex regMutex;
+ if (!regMutex.Acquire()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Check that Firefox ran recently, if not then stop here.
+ // Also stop if no timestamp was found, which most likely indicates
+ // that Firefox was not yet run.
+ bool ranRecently = false;
+ if (!aForce && (!CheckIfAppRanRecently(&ranRecently) || !ranRecently)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ DefaultBrowserResult defaultBrowserResult = GetDefaultBrowserInfo();
+ DefaultBrowserInfo browserInfo{};
+ if (defaultBrowserResult.isOk()) {
+ browserInfo = defaultBrowserResult.unwrap();
+ } else {
+ browserInfo.currentDefaultBrowser = Browser::Error;
+ browserInfo.previousDefaultBrowser = Browser::Error;
+ }
+
+ DefaultPdfResult defaultPdfResult = GetDefaultPdfInfo();
+ DefaultPdfInfo pdfInfo{};
+ if (defaultPdfResult.isOk()) {
+ pdfInfo = defaultPdfResult.unwrap();
+ } else {
+ pdfInfo.currentDefaultPdf = PDFHandler::Error;
+ }
+
+ NotificationActivities activitiesPerformed;
+ // We block while waiting for the notification which prevents STA thread
+ // callbacks from running as the event loop won't run. Moving notification
+ // handling to an MTA thread prevents this conflict.
+ activitiesPerformed = MaybeShowNotification(
+ browserInfo, PromiseFlatString(aUniqueToken).get(), aForce);
+
+ HRESULT hr = SendDefaultAgentPing(browserInfo, pdfInfo, activitiesPerformed);
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+DefaultAgent::AppRanRecently(bool* aRanRecently) {
+ bool ranRecently = false;
+ *aRanRecently = CheckIfAppRanRecently(&ranRecently) && ranRecently;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultAgent::GetDefaultBrowser(nsAString& aDefaultBrowser) {
+ Browser browser = default_agent::GetDefaultBrowser();
+ aDefaultBrowser = NS_ConvertUTF8toUTF16(GetStringForBrowser(browser));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultAgent::GetReplacePreviousDefaultBrowser(
+ const nsAString& aDefaultBrowser, nsAString& aPreviousDefaultBrowser) {
+ Browser browser =
+ GetBrowserFromString(std::string(NS_ConvertUTF16toUTF8(aDefaultBrowser)));
+ Browser previousBrowser =
+ default_agent::GetReplacePreviousDefaultBrowser(browser);
+ aPreviousDefaultBrowser =
+ NS_ConvertUTF8toUTF16(GetStringForBrowser(previousBrowser));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultAgent::GetDefaultPdfHandler(nsAString& aDefaultPdfHandler) {
+ PDFHandler pdf = default_agent::GetDefaultPdfInfo()
+ .unwrapOr({PDFHandler::Error})
+ .currentDefaultPdf;
+ aDefaultPdfHandler = NS_ConvertUTF8toUTF16(GetStringForPDFHandler(pdf));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DefaultAgent::SendPing(const nsAString& aDefaultBrowser,
+ const nsAString& aPreviousDefaultBrowser,
+ const nsAString& aDefaultPdfHandler,
+ const nsAString& aNotificationShown,
+ const nsAString& aNotificationAction) {
+ DefaultBrowserInfo browserInfo = {
+ GetBrowserFromString(std::string(NS_ConvertUTF16toUTF8(aDefaultBrowser))),
+ GetBrowserFromString(
+ std::string(NS_ConvertUTF16toUTF8(aPreviousDefaultBrowser)))};
+
+ DefaultPdfInfo pdfInfo = {GetPDFHandlerFromString(
+ std::string(NS_ConvertUTF16toUTF8(aDefaultPdfHandler)))};
+
+ // The JS implementation has never supported the "two notification flow",
+ // i.e., displaying a followup notification.
+ NotificationShown shown = GetNotificationShownFromString(aNotificationShown);
+ NotificationAction action =
+ GetNotificationActionFromString(aNotificationAction);
+ NotificationActivities activitiesPerformed = {NotificationType::Initial,
+ shown, action};
+
+ HRESULT hr = SendDefaultAgentPing(browserInfo, pdfInfo, activitiesPerformed);
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+DefaultAgent::SetDefaultBrowserUserChoice(
+ const nsAString& aAumid, const nsTArray<nsString>& aExtraFileExtensions) {
+ return default_agent::SetDefaultBrowserUserChoice(
+ PromiseFlatString(aAumid).get(), aExtraFileExtensions);
+}
+
+NS_IMETHODIMP
+DefaultAgent::SetDefaultBrowserUserChoiceAsync(
+ const nsAString& aAumid, const nsTArray<nsString>& aExtraFileExtensions,
+ JSContext* aCx, dom::Promise** aPromise) {
+ if (!NS_IsMainThread()) {
+ return NS_ERROR_NOT_SAME_THREAD;
+ }
+
+ ErrorResult rv;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ // A holder to pass the promise through the background task and back to
+ // the main thread when finished.
+ auto promiseHolder = MakeRefPtr<nsMainThreadPtrHolder<dom::Promise>>(
+ "SetDefaultBrowserUserChoiceAsync promise", promise);
+
+ nsresult result = NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "SetDefaultBrowserUserChoiceAsync",
+ // Make a local copy of the aAudmid parameter which is a reference
+ // which will go out of scope
+ [aumid = nsString(aAumid), promiseHolder = std::move(promiseHolder),
+ aExtraFileExtensions =
+ CopyableTArray<nsString>(aExtraFileExtensions)] {
+ nsresult rv = default_agent::SetDefaultBrowserUserChoice(
+ PromiseFlatString(aumid).get(), aExtraFileExtensions);
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "SetDefaultBrowserUserChoiceAsync callback",
+ [rv, promiseHolder = std::move(promiseHolder)] {
+ dom::Promise* promise = promiseHolder.get()->get();
+ if (NS_SUCCEEDED(rv)) {
+ promise->MaybeResolveWithUndefined();
+ } else {
+ promise->MaybeReject(rv);
+ }
+ }));
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ promise.forget(aPromise);
+ return result;
+}
+
+NS_IMETHODIMP
+DefaultAgent::SetDefaultExtensionHandlersUserChoice(
+ const nsAString& aAumid, const nsTArray<nsString>& aFileExtensions) {
+ return default_agent::SetDefaultExtensionHandlersUserChoice(
+ PromiseFlatString(aAumid).get(), aFileExtensions);
+}
+
+NS_IMETHODIMP
+DefaultAgent::AgentDisabled(bool* aDisabled) {
+ *aDisabled = IsAgentDisabled();
+ return NS_OK;
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/DefaultAgent.h b/toolkit/mozapps/defaultagent/DefaultAgent.h
new file mode 100644
index 0000000000..518ac44afe
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/DefaultAgent.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_H__
+#define __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_H__
+
+#include "nsIDefaultAgent.h"
+
+namespace mozilla::default_agent {
+
+class DefaultAgent final : public nsIDefaultAgent {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDEFAULTAGENT
+
+ DefaultAgent() = default;
+
+ private:
+ // A private destructor must be declared.
+ ~DefaultAgent() = default;
+};
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_H__
diff --git a/toolkit/mozapps/defaultagent/DefaultBrowser.cpp b/toolkit/mozapps/defaultagent/DefaultBrowser.cpp
new file mode 100644
index 0000000000..87d3f62632
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/DefaultBrowser.cpp
@@ -0,0 +1,240 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "DefaultBrowser.h"
+
+#include <string>
+
+#include <shlobj.h>
+
+#include "EventLog.h"
+#include "Registry.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Try.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+namespace mozilla::default_agent {
+
+using BrowserResult = mozilla::WindowsErrorResult<Browser>;
+
+constexpr std::string_view kUnknownBrowserString = "";
+
+constexpr std::pair<std::string_view, Browser> kStringBrowserMap[]{
+ {"error", Browser::Error},
+ {kUnknownBrowserString, Browser::Unknown},
+ {"firefox", Browser::Firefox},
+ {"chrome", Browser::Chrome},
+ {"edge", Browser::EdgeWithEdgeHTML},
+ {"edge-chrome", Browser::EdgeWithBlink},
+ {"ie", Browser::InternetExplorer},
+ {"opera", Browser::Opera},
+ {"brave", Browser::Brave},
+ {"yandex", Browser::Yandex},
+ {"qq-browser", Browser::QQBrowser},
+ {"360-browser", Browser::_360Browser},
+ {"sogou", Browser::Sogou},
+ {"duckduckgo", Browser::DuckDuckGo},
+};
+
+static_assert(mozilla::ArrayLength(kStringBrowserMap) == kBrowserCount);
+
+std::string GetStringForBrowser(Browser browser) {
+ for (const auto& [mapString, mapBrowser] : kStringBrowserMap) {
+ if (browser == mapBrowser) {
+ return std::string{mapString};
+ }
+ }
+
+ return std::string(kUnknownBrowserString);
+}
+
+Browser GetBrowserFromString(const std::string& browserString) {
+ for (const auto& [mapString, mapBrowser] : kStringBrowserMap) {
+ if (browserString == mapString) {
+ return mapBrowser;
+ }
+ }
+
+ return Browser::Unknown;
+}
+
+BrowserResult TryGetDefaultBrowser() {
+ RefPtr<IApplicationAssociationRegistration> pAAR;
+ HRESULT hr = CoCreateInstance(
+ CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return BrowserResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ // Whatever is handling the HTTP protocol is effectively the default browser.
+ mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> registeredApp;
+ {
+ wchar_t* rawRegisteredApp;
+ hr = pAAR->QueryCurrentDefault(L"http", AT_URLPROTOCOL, AL_EFFECTIVE,
+ &rawRegisteredApp);
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return BrowserResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ registeredApp = mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter>(
+ rawRegisteredApp);
+ }
+
+ // Get the application Friendly Name associated to the found ProgID. This is
+ // sized to be larger than any observed or expected friendly names. Long
+ // friendly names tend to be in the form `[Company] [Browser] [Variant]`
+ std::array<wchar_t, 256> friendlyName{};
+ DWORD friendlyNameLen = friendlyName.size();
+ hr = AssocQueryStringW(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME,
+ registeredApp.get(), nullptr, friendlyName.data(),
+ &friendlyNameLen);
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return BrowserResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ // This maps a browser's Friendly Name prefix to an enum variant that we'll
+ // use to identify that browser in our telemetry ping (which is this
+ // function's return value).
+ constexpr std::pair<std::wstring_view, Browser> kFriendlyNamePrefixes[] = {
+ {L"Firefox", Browser::Firefox},
+ {L"Google Chrome", Browser::Chrome},
+ {L"Microsoft Edge", Browser::EdgeWithBlink},
+ {L"Internet Explorer", Browser::InternetExplorer},
+ {L"Opera", Browser::Opera},
+ {L"Brave", Browser::Brave},
+ {L"Yandex", Browser::Yandex},
+ {L"QQBrowser", Browser::QQBrowser},
+ // 360安全浏览器 UTF-16 encoding
+ {L"\u0033\u0036\u0030\u5b89\u5168\u6d4f\u89c8\u5668",
+ Browser::_360Browser},
+ // 搜狗高速浏览器 UTF-16 encoding
+ {L"\u641c\u72d7\u9ad8\u901f\u6d4f\u89c8\u5668", Browser::Sogou},
+ {L"DuckDuckGo", Browser::DuckDuckGo},
+ };
+
+ // We should have one prefix for every browser we track, minus exceptions
+ // listed below.
+ // Error - not a real browser.
+ // Unknown - not a real browser.
+ // EdgeWithEdgeHTML - duplicate friendly name with EdgeWithBlink with special
+ // handling below.
+ static_assert(mozilla::ArrayLength(kFriendlyNamePrefixes) ==
+ kBrowserCount - 3);
+
+ for (const auto& [prefix, browser] : kFriendlyNamePrefixes) {
+ // Find matching Friendly Name prefix.
+ if (!wcsnicmp(friendlyName.data(), prefix.data(), prefix.length())) {
+ if (browser == Browser::EdgeWithBlink) {
+ // Disambiguate EdgeWithEdgeHTML and EdgeWithBlink.
+ // The ProgID below is documented as having not changed while Edge was
+ // actively developed. It's assumed but unverified this is true in all
+ // cases (e.g. across locales).
+ //
+ // Note: at time of commit EdgeWithBlink from the Windows Store was a
+ // wrapper for Edge Installer instead of a package containing Edge,
+ // therefore the Default Browser associating ProgID was not in the form
+ // "AppX[hash]" as expected. It is unclear if the EdgeWithEdgeHTML and
+ // EdgeWithBlink ProgIDs would differ if the latter is changed into a
+ // package containing Edge.
+ constexpr std::wstring_view progIdEdgeHtml1{
+ L"AppXq0fevzme2pys62n3e0fbqa7peapykr8v"};
+ // Apparently there is at least one other ProgID used by EdgeHTML Edge.
+ constexpr std::wstring_view progIdEdgeHtml2{
+ L"AppXd4nrz8ff68srnhf9t5a8sbjyar1cr723"};
+
+ if (!wcsnicmp(registeredApp.get(), progIdEdgeHtml1.data(),
+ progIdEdgeHtml1.length()) ||
+ !wcsnicmp(registeredApp.get(), progIdEdgeHtml2.data(),
+ progIdEdgeHtml2.length())) {
+ return Browser::EdgeWithEdgeHTML;
+ }
+ }
+
+ return browser;
+ }
+ }
+
+ // The default browser is one that we don't know about.
+ return Browser::Unknown;
+}
+
+BrowserResult TryGetReplacePreviousDefaultBrowser(Browser currentDefault) {
+ // This function uses a registry value which stores the current default
+ // browser. It returns the data stored in that registry value and replaces the
+ // stored string with the current default browser string that was passed in.
+
+ std::string currentDefaultStr = GetStringForBrowser(currentDefault);
+ std::string previousDefault =
+ RegistryGetValueString(IsPrefixed::Unprefixed, L"CurrentDefault")
+ .unwrapOr(mozilla::Some(currentDefaultStr))
+ .valueOr(currentDefaultStr);
+
+ mozilla::Unused << RegistrySetValueString(
+ IsPrefixed::Unprefixed, L"CurrentDefault", currentDefaultStr.c_str());
+
+ return GetBrowserFromString(previousDefault);
+}
+
+DefaultBrowserResult GetDefaultBrowserInfo() {
+ DefaultBrowserInfo browserInfo;
+
+ MOZ_TRY_VAR(browserInfo.currentDefaultBrowser, TryGetDefaultBrowser());
+ MOZ_TRY_VAR(
+ browserInfo.previousDefaultBrowser,
+ TryGetReplacePreviousDefaultBrowser(browserInfo.currentDefaultBrowser));
+
+ return browserInfo;
+}
+
+// We used to prefix this key with the installation directory, but that causes
+// problems with our new "only one ping per day across installs" restriction.
+// To make sure all installations use consistent data, the value's name is
+// being migrated to a shared, non-prefixed name.
+// This function doesn't really do any error handling, because there isn't
+// really anything to be done if it fails.
+void MaybeMigrateCurrentDefault() {
+ const wchar_t* valueName = L"CurrentDefault";
+
+ MaybeStringResult valueResult =
+ RegistryGetValueString(IsPrefixed::Prefixed, valueName);
+ if (valueResult.isErr()) {
+ return;
+ }
+ mozilla::Maybe<std::string> maybeValue = valueResult.unwrap();
+ if (maybeValue.isNothing()) {
+ // No value to migrate
+ return;
+ }
+ std::string value = maybeValue.value();
+
+ mozilla::Unused << RegistryDeleteValue(IsPrefixed::Prefixed, valueName);
+
+ // Only migrate the value if no value is in the new location yet.
+ valueResult = RegistryGetValueString(IsPrefixed::Unprefixed, valueName);
+ if (valueResult.isErr()) {
+ return;
+ }
+ if (valueResult.unwrap().isNothing()) {
+ mozilla::Unused << RegistrySetValueString(IsPrefixed::Unprefixed, valueName,
+ value.c_str());
+ }
+}
+
+Browser GetDefaultBrowser() {
+ return TryGetDefaultBrowser().unwrapOr(Browser::Error);
+}
+Browser GetReplacePreviousDefaultBrowser(Browser currentBrowser) {
+ return TryGetReplacePreviousDefaultBrowser(currentBrowser)
+ .unwrapOr(Browser::Error);
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/DefaultBrowser.h b/toolkit/mozapps/defaultagent/DefaultBrowser.h
new file mode 100644
index 0000000000..f1b940959f
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/DefaultBrowser.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_DEFAULT_BROWSER_H__
+#define __DEFAULT_BROWSER_DEFAULT_BROWSER_H__
+
+#include <string>
+
+#include "mozilla/DefineEnum.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+namespace mozilla::default_agent {
+
+MOZ_DEFINE_ENUM_CLASS(Browser,
+ (Error, Unknown, Firefox, Chrome, EdgeWithEdgeHTML,
+ EdgeWithBlink, InternetExplorer, Opera, Brave, Yandex,
+ QQBrowser, _360Browser, Sogou, DuckDuckGo));
+
+struct DefaultBrowserInfo {
+ Browser currentDefaultBrowser;
+ Browser previousDefaultBrowser;
+};
+
+using DefaultBrowserResult = mozilla::WindowsErrorResult<DefaultBrowserInfo>;
+
+DefaultBrowserResult GetDefaultBrowserInfo();
+Browser GetDefaultBrowser();
+Browser GetReplacePreviousDefaultBrowser(Browser currentBrowser);
+
+std::string GetStringForBrowser(Browser browser);
+Browser GetBrowserFromString(const std::string& browserString);
+void MaybeMigrateCurrentDefault();
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_DEFAULT_BROWSER_H__
diff --git a/toolkit/mozapps/defaultagent/DefaultPDF.cpp b/toolkit/mozapps/defaultagent/DefaultPDF.cpp
new file mode 100644
index 0000000000..e0a9f2e85a
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/DefaultPDF.cpp
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "DefaultPDF.h"
+
+#include <string>
+
+#include <shlobj.h>
+#include <winerror.h>
+
+#include "EventLog.h"
+
+#include "mozilla/Buffer.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "mozilla/Try.h"
+
+namespace mozilla::default_agent {
+
+constexpr std::string_view kUnknownPdfString = "";
+
+constexpr std::pair<std::string_view, PDFHandler> kStringPdfHandlerMap[]{
+ {"error", PDFHandler::Error},
+ {kUnknownPdfString, PDFHandler::Unknown},
+ {"Firefox", PDFHandler::Firefox},
+ {"Microsoft Edge", PDFHandler::MicrosoftEdge},
+ {"Google Chrome", PDFHandler::GoogleChrome},
+ {"Adobe Acrobat", PDFHandler::AdobeAcrobat},
+ {"WPS", PDFHandler::WPS},
+ {"Nitro", PDFHandler::Nitro},
+ {"Foxit", PDFHandler::Foxit},
+ {"PDF-XChange", PDFHandler::PDFXChange},
+ {"Avast", PDFHandler::AvastSecureBrowser},
+ {"Sumatra", PDFHandler::SumatraPDF},
+};
+
+static_assert(mozilla::ArrayLength(kStringPdfHandlerMap) == kPDFHandlerCount);
+
+std::string GetStringForPDFHandler(PDFHandler handler) {
+ for (const auto& [mapString, mapPdf] : kStringPdfHandlerMap) {
+ if (handler == mapPdf) {
+ return std::string{mapString};
+ }
+ }
+
+ return std::string(kUnknownPdfString);
+}
+
+PDFHandler GetPDFHandlerFromString(const std::string& pdfHandlerString) {
+ for (const auto& [mapString, mapPdfHandler] : kStringPdfHandlerMap) {
+ if (pdfHandlerString == mapString) {
+ return mapPdfHandler;
+ }
+ }
+
+ return PDFHandler::Unknown;
+}
+
+using PdfResult = mozilla::WindowsErrorResult<PDFHandler>;
+
+static PdfResult GetDefaultPdf() {
+ RefPtr<IApplicationAssociationRegistration> pAAR;
+ HRESULT hr = CoCreateInstance(
+ CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return PdfResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> registeredApp;
+ {
+ wchar_t* rawRegisteredApp;
+ hr = pAAR->QueryCurrentDefault(L".pdf", AT_FILEEXTENSION, AL_EFFECTIVE,
+ &rawRegisteredApp);
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return PdfResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ registeredApp = mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter>{
+ rawRegisteredApp};
+ }
+
+ // Get the application Friendly Name associated to the found ProgID. This is
+ // sized to be larger than any observed or expected friendly names. Long
+ // friendly names tend to be in the form `[Company] [Viewer] [Variant]`
+ DWORD friendlyNameLen = 0;
+ hr = AssocQueryStringW(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME,
+ registeredApp.get(), nullptr, nullptr,
+ &friendlyNameLen);
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return PdfResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ mozilla::Buffer<wchar_t> friendlyNameBuffer(friendlyNameLen);
+ hr = AssocQueryStringW(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME,
+ registeredApp.get(), nullptr,
+ friendlyNameBuffer.Elements(), &friendlyNameLen);
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return PdfResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ constexpr std::pair<std::wstring_view, PDFHandler> kFriendlyNamePrefixes[] = {
+ {L"Firefox", PDFHandler::Firefox},
+ {L"Microsoft Edge", PDFHandler::MicrosoftEdge},
+ {L"Google Chrome", PDFHandler::GoogleChrome},
+ {L"Adobe", PDFHandler::AdobeAcrobat},
+ {L"Acrobat", PDFHandler::AdobeAcrobat},
+ {L"WPS", PDFHandler::WPS},
+ {L"Nitro", PDFHandler::Nitro},
+ {L"Foxit", PDFHandler::Foxit},
+ {L"PDF-XChange", PDFHandler::PDFXChange},
+ {L"Avast", PDFHandler::AvastSecureBrowser},
+ {L"Sumatra", PDFHandler::SumatraPDF},
+ };
+
+ // We should have one prefix for every PDF handler we track, with exceptions
+ // listed below.
+ // Error - removed; not a real pdf handler.
+ // Unknown - removed; not a real pdf handler.
+ // AdobeAcrobat - duplicate; `Adobe` and `Acrobat` prefixes are both seen in
+ // telemetry.
+ static_assert(mozilla::ArrayLength(kFriendlyNamePrefixes) ==
+ kPDFHandlerCount - 2 + 1);
+
+ PDFHandler resolvedHandler = PDFHandler::Unknown;
+ for (const auto& [knownHandlerSubstring, handlerEnum] :
+ kFriendlyNamePrefixes) {
+ if (!wcsnicmp(friendlyNameBuffer.Elements(), knownHandlerSubstring.data(),
+ knownHandlerSubstring.length())) {
+ resolvedHandler = handlerEnum;
+ break;
+ }
+ }
+
+ return resolvedHandler;
+}
+
+DefaultPdfResult GetDefaultPdfInfo() {
+ DefaultPdfInfo pdfInfo;
+ MOZ_TRY_VAR(pdfInfo.currentDefaultPdf, GetDefaultPdf());
+
+ return pdfInfo;
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/DefaultPDF.h b/toolkit/mozapps/defaultagent/DefaultPDF.h
new file mode 100644
index 0000000000..73afaa7025
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/DefaultPDF.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef DEFAULT_BROWSER_DEFAULT_PDF_H__
+#define DEFAULT_BROWSER_DEFAULT_PDF_H__
+
+#include <string>
+
+#include "mozilla/DefineEnum.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+namespace mozilla::default_agent {
+
+MOZ_DEFINE_ENUM_CLASS(PDFHandler,
+ (Error, Unknown, Firefox, MicrosoftEdge, GoogleChrome,
+ AdobeAcrobat, WPS, Nitro, Foxit, PDFXChange,
+ AvastSecureBrowser, SumatraPDF));
+
+struct DefaultPdfInfo {
+ PDFHandler currentDefaultPdf;
+};
+
+using DefaultPdfResult = mozilla::WindowsErrorResult<DefaultPdfInfo>;
+
+DefaultPdfResult GetDefaultPdfInfo();
+std::string GetStringForPDFHandler(PDFHandler handler);
+PDFHandler GetPDFHandlerFromString(const std::string& pdfHandlerString);
+
+} // namespace mozilla::default_agent
+
+#endif // DEFAULT_BROWSER_DEFAULT_PDF_H__
diff --git a/toolkit/mozapps/defaultagent/EventLog.cpp b/toolkit/mozapps/defaultagent/EventLog.cpp
new file mode 100644
index 0000000000..eaac1161bb
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/EventLog.cpp
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "EventLog.h"
+
+// This is an easy way to expose `MOZ_APP_DISPLAYNAME` to Rust code.
+const wchar_t* gWinEventLogSourceName =
+ L"" MOZ_APP_DISPLAYNAME " Default Browser Agent";
diff --git a/toolkit/mozapps/defaultagent/EventLog.h b/toolkit/mozapps/defaultagent/EventLog.h
new file mode 100644
index 0000000000..84b35010f8
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/EventLog.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__
+#define __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__
+
+#include "mozilla/Types.h"
+
+MOZ_BEGIN_EXTERN_C
+
+extern MOZ_EXPORT const wchar_t* gWinEventLogSourceName;
+
+MOZ_END_EXTERN_C
+
+#include "mozilla/WindowsEventLog.h"
+
+#define LOG_ERROR(hr) MOZ_WIN_EVENT_LOG_ERROR(gWinEventLogSourceName, hr)
+#define LOG_ERROR_MESSAGE(format, ...) \
+ MOZ_WIN_EVENT_LOG_ERROR_MESSAGE(gWinEventLogSourceName, format, __VA_ARGS__)
+
+#endif // __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__
diff --git a/toolkit/mozapps/defaultagent/Notification.cpp b/toolkit/mozapps/defaultagent/Notification.cpp
new file mode 100644
index 0000000000..961e57c9b3
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Notification.cpp
@@ -0,0 +1,709 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "Notification.h"
+
+#include <shlwapi.h>
+#include <wchar.h>
+#include <windows.h>
+#include <winnt.h>
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/mscom/EnsureMTA.h"
+#include "mozilla/intl/FileSource.h"
+#include "mozilla/intl/Localization.h"
+#include "mozilla/ShellHeaderOnlyUtils.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWindowsHelpers.h"
+#include "readstrings.h"
+#include "updatererrors.h"
+#include "WindowsDefaultBrowser.h"
+
+#include "common.h"
+#include "DefaultBrowser.h"
+#include "EventLog.h"
+#include "Registry.h"
+#include "SetDefaultBrowser.h"
+
+#include "wintoastlib.h"
+
+using mozilla::intl::Localization;
+
+#define SEVEN_DAYS_IN_SECONDS (7 * 24 * 60 * 60)
+
+// If the notification hasn't been activated or dismissed within 12 hours,
+// stop waiting for it.
+#define NOTIFICATION_WAIT_TIMEOUT_MS (12 * 60 * 60 * 1000)
+// If the mutex hasn't been released within a few minutes, something is wrong
+// and we should give up on it
+#define MUTEX_TIMEOUT_MS (10 * 60 * 1000)
+
+namespace mozilla::default_agent {
+
+bool FirefoxInstallIsEnglish();
+
+static bool SetInitialNotificationShown(bool wasShown) {
+ return !RegistrySetValueBool(IsPrefixed::Unprefixed,
+ L"InitialNotificationShown", wasShown)
+ .isErr();
+}
+
+static bool GetInitialNotificationShown() {
+ return RegistryGetValueBool(IsPrefixed::Unprefixed,
+ L"InitialNotificationShown")
+ .unwrapOr(mozilla::Some(false))
+ .valueOr(false);
+}
+
+static bool ResetInitialNotificationShown() {
+ return RegistryDeleteValue(IsPrefixed::Unprefixed,
+ L"InitialNotificationShown")
+ .isOk();
+}
+
+static bool SetFollowupNotificationShown(bool wasShown) {
+ return !RegistrySetValueBool(IsPrefixed::Unprefixed,
+ L"FollowupNotificationShown", wasShown)
+ .isErr();
+}
+
+static bool GetFollowupNotificationShown() {
+ return RegistryGetValueBool(IsPrefixed::Unprefixed,
+ L"FollowupNotificationShown")
+ .unwrapOr(mozilla::Some(false))
+ .valueOr(false);
+}
+
+static bool SetFollowupNotificationSuppressed(bool value) {
+ return !RegistrySetValueBool(IsPrefixed::Unprefixed,
+ L"FollowupNotificationSuppressed", value)
+ .isErr();
+}
+
+static bool GetFollowupNotificationSuppressed() {
+ return RegistryGetValueBool(IsPrefixed::Unprefixed,
+ L"FollowupNotificationSuppressed")
+ .unwrapOr(mozilla::Some(false))
+ .valueOr(false);
+}
+
+// Returns 0 if no value is set.
+static ULONGLONG GetFollowupNotificationRequestTime() {
+ return RegistryGetValueQword(IsPrefixed::Unprefixed, L"FollowupRequestTime")
+ .unwrapOr(mozilla::Some(0))
+ .valueOr(0);
+}
+
+// Returns false if no value is set.
+static bool GetPrefSetDefaultBrowserUserChoice() {
+ return RegistryGetValueBool(IsPrefixed::Prefixed,
+ L"SetDefaultBrowserUserChoice")
+ .unwrapOr(mozilla::Some(false))
+ .valueOr(false);
+}
+
+struct ToastStrings {
+ mozilla::UniquePtr<wchar_t[]> text1;
+ mozilla::UniquePtr<wchar_t[]> text2;
+ mozilla::UniquePtr<wchar_t[]> action1;
+ mozilla::UniquePtr<wchar_t[]> action2;
+ mozilla::UniquePtr<wchar_t[]> relImagePath;
+};
+
+struct Strings {
+ // Toast notification button text is hard to localize because it tends to
+ // overflow. Thus, we have 3 different toast notifications:
+ // - The initial notification, which includes a button with text like
+ // "Ask me later". Since we cannot easily localize this, we will display
+ // it only in English.
+ // - The followup notification, to be shown if the user clicked "Ask me
+ // later". Since we only have that button in English, we only need this
+ // notification in English.
+ // - The localized notification, which has much shorter button text to
+ // (hopefully) prevent overflow: just "Yes" and "No". Since we no longer
+ // have an "Ask me later" button, a followup localized notification is not
+ // needed.
+ ToastStrings initialToast;
+ ToastStrings followupToast;
+ ToastStrings localizedToast;
+
+ // Returned pointer points within this struct and should not be freed.
+ const ToastStrings* GetToastStrings(NotificationType whichToast,
+ bool englishStrings) const {
+ if (!englishStrings) {
+ return &localizedToast;
+ }
+ if (whichToast == NotificationType::Initial) {
+ return &initialToast;
+ }
+ return &followupToast;
+ }
+};
+
+// Gets all strings out of the relevant INI files.
+// Returns true on success, false on failure
+static bool GetStrings(Strings& strings) {
+ mozilla::UniquePtr<wchar_t[]> installPath;
+ bool success = GetInstallDirectory(installPath);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Failed to get install directory when getting strings");
+ return false;
+ }
+ nsTArray<nsCString> resIds = {"branding/brand.ftl"_ns,
+ "browser/backgroundtasks/defaultagent.ftl"_ns};
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+ nsAutoCString daHeaderText, daBodyText, daYesButton, daNoButton;
+ mozilla::ErrorResult daRv;
+ l10n->FormatValueSync("default-browser-notification-header-text"_ns, {},
+ daHeaderText, daRv);
+ ENSURE_SUCCESS(daRv, false);
+ l10n->FormatValueSync("default-browser-notification-body-text"_ns, {},
+ daBodyText, daRv);
+ ENSURE_SUCCESS(daRv, false);
+ l10n->FormatValueSync("default-browser-notification-yes-button-text"_ns, {},
+ daYesButton, daRv);
+ ENSURE_SUCCESS(daRv, false);
+ l10n->FormatValueSync("default-browser-notification-no-button-text"_ns, {},
+ daNoButton, daRv);
+ ENSURE_SUCCESS(daRv, false);
+
+ NS_ConvertUTF8toUTF16 daHeaderTextW(daHeaderText), daBodyTextW(daBodyText),
+ daYesButtonW(daYesButton), daNoButtonW(daNoButton);
+ strings.localizedToast.text1 =
+ mozilla::MakeUnique<wchar_t[]>(daHeaderTextW.Length() + 1);
+ wcsncpy(strings.localizedToast.text1.get(), daHeaderTextW.get(),
+ daHeaderTextW.Length() + 1);
+ strings.localizedToast.text2 =
+ mozilla::MakeUnique<wchar_t[]>(daBodyTextW.Length() + 1);
+ wcsncpy(strings.localizedToast.text2.get(), daBodyTextW.get(),
+ daBodyTextW.Length() + 1);
+ strings.localizedToast.action1 =
+ mozilla::MakeUnique<wchar_t[]>(daYesButtonW.Length() + 1);
+ wcsncpy(strings.localizedToast.action1.get(), daYesButtonW.get(),
+ daYesButtonW.Length() + 1);
+ strings.localizedToast.action2 =
+ mozilla::MakeUnique<wchar_t[]>(daNoButtonW.Length() + 1);
+ wcsncpy(strings.localizedToast.action2.get(), daNoButtonW.get(),
+ daNoButtonW.Length() + 1);
+ const wchar_t* iniFormat = L"%s\\defaultagent.ini";
+ int bufferSize = _scwprintf(iniFormat, installPath.get());
+ ++bufferSize; // Extra character for terminating null
+ mozilla::UniquePtr<wchar_t[]> iniPath =
+ mozilla::MakeUnique<wchar_t[]>(bufferSize);
+ _snwprintf_s(iniPath.get(), bufferSize, _TRUNCATE, iniFormat,
+ installPath.get());
+
+ IniReader nonlocalizedReader(iniPath.get(), "Nonlocalized");
+ nonlocalizedReader.AddKey("InitialToastRelativeImagePath",
+ &strings.initialToast.relImagePath);
+ nonlocalizedReader.AddKey("FollowupToastRelativeImagePath",
+ &strings.followupToast.relImagePath);
+ nonlocalizedReader.AddKey("LocalizedToastRelativeImagePath",
+ &strings.localizedToast.relImagePath);
+ int result = nonlocalizedReader.Read();
+ if (result != OK) {
+ LOG_ERROR_MESSAGE(L"Unable to read non-localized strings: %d", result);
+ return false;
+ }
+
+ return true;
+}
+
+static mozilla::WindowsError LaunchFirefoxToHandleDefaultBrowserAgent() {
+ // Could also be `MOZ_APP_NAME.exe`, but there's no generality to be gained:
+ // the WDBA is Firefox-only.
+ FilePathResult firefoxPathResult = GetRelativeBinaryPath(L"firefox.exe");
+ if (firefoxPathResult.isErr()) {
+ return firefoxPathResult.unwrapErr();
+ }
+ std::wstring firefoxPath = firefoxPathResult.unwrap();
+
+ _bstr_t cmd = firefoxPath.c_str();
+ // Omit argv[0] because ShellExecute doesn't need it.
+ _variant_t args(L"-to-handle-default-browser-agent");
+ _variant_t operation(L"open");
+ _variant_t directory;
+ _variant_t showCmd(SW_SHOWNORMAL);
+
+ // To prevent inheriting environment variables from the background task, we
+ // run Firefox via Explorer instead of our own process. This mimics the
+ // implementation of the Windows Launcher Process.
+ auto result =
+ ShellExecuteByExplorer(cmd, args, operation, directory, showCmd);
+ NS_ENSURE_TRUE(result.isOk(), result.unwrapErr());
+
+ return mozilla::WindowsError::CreateSuccess();
+}
+
+/*
+ * Set the default browser.
+ *
+ * First check if we can directly write UserChoice, if so attempt that.
+ * If we can't write UserChoice, or if the attempt fails, fall back to
+ * showing the Default Apps page of Settings.
+ *
+ * @param aAumi The AUMI of the installation to set as default.
+ */
+static void SetDefaultBrowserFromNotification(const wchar_t* aumi) {
+ nsresult rv = NS_ERROR_FAILURE;
+ if (GetPrefSetDefaultBrowserUserChoice()) {
+ rv = SetDefaultBrowserUserChoice(aumi);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ mozilla::Unused << LaunchFirefoxToHandleDefaultBrowserAgent();
+ } else {
+ LOG_ERROR_MESSAGE(L"Failed to SetDefaultBrowserUserChoice: %#X",
+ GetLastError());
+ LaunchModernSettingsDialogDefaultApps();
+ }
+}
+
+// This encapsulates the data that needs to be protected by a mutex because it
+// will be shared by the main thread and the handler thread.
+// To ensure the data is only written once, handlerDataHasBeenSet should be
+// initialized to false, then set to true when the handler writes data into the
+// structure.
+struct HandlerData {
+ NotificationActivities activitiesPerformed;
+ bool handlerDataHasBeenSet;
+};
+
+// The value that ToastHandler writes into should be a global. We can't control
+// when ToastHandler is called, and if this value isn't a global, ToastHandler
+// may be called and attempt to access this after it has been deconstructed.
+// Since this value is accessed by the handler thread and the main thread, it
+// is protected by a mutex (gHandlerMutex).
+// Since ShowNotification deconstructs the mutex, it might seem like once
+// ShowNotification exits, we can just rely on the inability to wait on an
+// invalid mutex to protect the deconstructed data, but it's possible that
+// we could deconstruct the mutex while the handler is holding it and is
+// already accessing the protected data.
+static HandlerData gHandlerReturnData;
+static HANDLE gHandlerMutex = INVALID_HANDLE_VALUE;
+
+class ToastHandler : public WinToastLib::IWinToastHandler {
+ private:
+ NotificationType mWhichNotification;
+ HANDLE mEvent;
+ const std::wstring mAumiStr;
+
+ public:
+ ToastHandler(NotificationType whichNotification, HANDLE event,
+ const wchar_t* aumi)
+ : mWhichNotification(whichNotification), mEvent(event), mAumiStr(aumi) {}
+
+ void FinishHandler(NotificationActivities& returnData) const {
+ SetReturnData(returnData);
+
+ BOOL success = SetEvent(mEvent);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Event could not be set: %#X", GetLastError());
+ }
+ }
+
+ void SetReturnData(NotificationActivities& toSet) const {
+ DWORD result = WaitForSingleObject(gHandlerMutex, MUTEX_TIMEOUT_MS);
+ if (result == WAIT_TIMEOUT) {
+ LOG_ERROR_MESSAGE(L"Unable to obtain mutex ownership");
+ return;
+ } else if (result == WAIT_FAILED) {
+ LOG_ERROR_MESSAGE(L"Failed to wait on mutex: %#X", GetLastError());
+ return;
+ } else if (result == WAIT_ABANDONED) {
+ LOG_ERROR_MESSAGE(L"Found abandoned mutex");
+ ReleaseMutex(gHandlerMutex);
+ return;
+ }
+
+ // Only set this data once
+ if (!gHandlerReturnData.handlerDataHasBeenSet) {
+ gHandlerReturnData.activitiesPerformed = toSet;
+ gHandlerReturnData.handlerDataHasBeenSet = true;
+ }
+
+ BOOL success = ReleaseMutex(gHandlerMutex);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X",
+ GetLastError());
+ }
+ }
+
+ void toastActivated() const override {
+ NotificationActivities activitiesPerformed;
+ activitiesPerformed.type = mWhichNotification;
+ activitiesPerformed.shown = NotificationShown::Shown;
+ activitiesPerformed.action = NotificationAction::ToastClicked;
+
+ // Notification strings are written to indicate the default browser is
+ // restored to Firefox when the notification body is clicked to prevent
+ // ambiguity when buttons aren't pressed.
+ SetDefaultBrowserFromNotification(mAumiStr.c_str());
+
+ FinishHandler(activitiesPerformed);
+ }
+
+ void toastActivated(int actionIndex) const override {
+ NotificationActivities activitiesPerformed;
+ activitiesPerformed.type = mWhichNotification;
+ activitiesPerformed.shown = NotificationShown::Shown;
+ // Override this below
+ activitiesPerformed.action = NotificationAction::NoAction;
+
+ if (actionIndex == 0) {
+ // "Make Firefox the default" button, on both the initial and followup
+ // notifications. "Yes" button on the localized notification.
+ activitiesPerformed.action = NotificationAction::MakeFirefoxDefaultButton;
+
+ SetDefaultBrowserFromNotification(mAumiStr.c_str());
+ } else if (actionIndex == 1) {
+ // Do nothing. As long as we don't call
+ // SetFollowupNotificationRequestTime, there will be no followup
+ // notification.
+ activitiesPerformed.action = NotificationAction::DismissedByButton;
+ }
+
+ FinishHandler(activitiesPerformed);
+ }
+
+ void toastDismissed(WinToastDismissalReason state) const override {
+ NotificationActivities activitiesPerformed;
+ activitiesPerformed.type = mWhichNotification;
+ activitiesPerformed.shown = NotificationShown::Shown;
+ // Override this below
+ activitiesPerformed.action = NotificationAction::NoAction;
+
+ if (state == WinToastDismissalReason::TimedOut) {
+ activitiesPerformed.action = NotificationAction::DismissedByTimeout;
+ } else if (state == WinToastDismissalReason::ApplicationHidden) {
+ activitiesPerformed.action =
+ NotificationAction::DismissedByApplicationHidden;
+ } else if (state == WinToastDismissalReason::UserCanceled) {
+ activitiesPerformed.action = NotificationAction::DismissedToActionCenter;
+ }
+
+ FinishHandler(activitiesPerformed);
+ }
+
+ void toastFailed() const override {
+ NotificationActivities activitiesPerformed;
+ activitiesPerformed.type = mWhichNotification;
+ activitiesPerformed.shown = NotificationShown::Error;
+ activitiesPerformed.action = NotificationAction::NoAction;
+
+ LOG_ERROR_MESSAGE(L"Toast notification failed to display");
+ FinishHandler(activitiesPerformed);
+ }
+};
+
+// This function blocks until the shown notification is activated or dismissed.
+static NotificationActivities ShowNotification(
+ NotificationType whichNotification, const wchar_t* aumi) {
+ // Initially set the value that will be returned to error. If the notification
+ // is shown successfully, we'll update it.
+ NotificationActivities activitiesPerformed = {whichNotification,
+ NotificationShown::Error,
+ NotificationAction::NoAction};
+
+ bool isEnglishInstall = FirefoxInstallIsEnglish();
+
+ Strings strings;
+ if (!GetStrings(strings)) {
+ return activitiesPerformed;
+ }
+ const ToastStrings* toastStrings =
+ strings.GetToastStrings(whichNotification, isEnglishInstall);
+
+ mozilla::mscom::EnsureMTA([&] {
+ using namespace WinToastLib;
+
+ if (!WinToast::isCompatible()) {
+ LOG_ERROR_MESSAGE(L"System is not compatible with WinToast");
+ return;
+ }
+ WinToast::instance()->setAppName(L"" MOZ_APP_DISPLAYNAME);
+ std::wstring aumiStr = aumi;
+ WinToast::instance()->setAppUserModelId(aumiStr);
+ WinToast::instance()->setShortcutPolicy(
+ WinToastLib::WinToast::SHORTCUT_POLICY_REQUIRE_NO_CREATE);
+ WinToast::WinToastError error;
+ if (!WinToast::instance()->initialize(&error)) {
+ LOG_ERROR_MESSAGE(WinToast::strerror(error).c_str());
+ return;
+ }
+
+ // This event object will let the handler notify us when it has handled the
+ // notification.
+ nsAutoHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
+ if (event.get() == nullptr) {
+ LOG_ERROR_MESSAGE(L"Unable to create event object: %#X", GetLastError());
+ return;
+ }
+
+ bool success = false;
+ if (whichNotification == NotificationType::Initial) {
+ success = SetInitialNotificationShown(true);
+ } else {
+ success = SetFollowupNotificationShown(true);
+ }
+ if (!success) {
+ // Return early in this case to prevent the notification from being shown
+ // on every run.
+ LOG_ERROR_MESSAGE(L"Unable to set notification as displayed");
+ return;
+ }
+
+ // We need the absolute image path, not the relative path.
+ mozilla::UniquePtr<wchar_t[]> installPath;
+ success = GetInstallDirectory(installPath);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Failed to get install directory for the image path");
+ return;
+ }
+ const wchar_t* absPathFormat = L"%s\\%s";
+ int bufferSize = _scwprintf(absPathFormat, installPath.get(),
+ toastStrings->relImagePath.get());
+ ++bufferSize; // Extra character for terminating null
+ mozilla::UniquePtr<wchar_t[]> absImagePath =
+ mozilla::MakeUnique<wchar_t[]>(bufferSize);
+ _snwprintf_s(absImagePath.get(), bufferSize, _TRUNCATE, absPathFormat,
+ installPath.get(), toastStrings->relImagePath.get());
+
+ // This is used to protect gHandlerReturnData.
+ gHandlerMutex = CreateMutexW(nullptr, TRUE, nullptr);
+ if (gHandlerMutex == nullptr) {
+ LOG_ERROR_MESSAGE(L"Unable to create mutex: %#X", GetLastError());
+ return;
+ }
+ // Automatically close this mutex when this function exits.
+ nsAutoHandle autoMutex(gHandlerMutex);
+ // No need to initialize gHandlerReturnData.activitiesPerformed, since it
+ // will be set by the handler. But we do need to initialize
+ // gHandlerReturnData.handlerDataHasBeenSet so the handler knows that no
+ // data has been set yet.
+ gHandlerReturnData.handlerDataHasBeenSet = false;
+ success = ReleaseMutex(gHandlerMutex);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X",
+ GetLastError());
+ }
+
+ // Finally ready to assemble the notification and dispatch it.
+ WinToastTemplate toastTemplate =
+ WinToastTemplate(WinToastTemplate::ImageAndText02);
+ toastTemplate.setTextField(toastStrings->text1.get(),
+ WinToastTemplate::FirstLine);
+ toastTemplate.setTextField(toastStrings->text2.get(),
+ WinToastTemplate::SecondLine);
+ toastTemplate.addAction(toastStrings->action1.get());
+ toastTemplate.addAction(toastStrings->action2.get());
+ toastTemplate.setImagePath(absImagePath.get());
+ toastTemplate.setScenario(WinToastTemplate::Scenario::Reminder);
+ ToastHandler* handler =
+ new ToastHandler(whichNotification, event.get(), aumi);
+ INT64 id = WinToast::instance()->showToast(toastTemplate, handler, &error);
+ if (id < 0) {
+ LOG_ERROR_MESSAGE(WinToast::strerror(error).c_str());
+ return;
+ }
+
+ DWORD result =
+ WaitForSingleObject(event.get(), NOTIFICATION_WAIT_TIMEOUT_MS);
+ // Don't return after these errors. Attempt to hide the notification.
+ if (result == WAIT_FAILED) {
+ LOG_ERROR_MESSAGE(L"Unable to wait on event object: %#X", GetLastError());
+ } else if (result == WAIT_TIMEOUT) {
+ LOG_ERROR_MESSAGE(L"Timed out waiting for event object");
+ } else {
+ result = WaitForSingleObject(gHandlerMutex, MUTEX_TIMEOUT_MS);
+ if (result == WAIT_TIMEOUT) {
+ LOG_ERROR_MESSAGE(L"Unable to obtain mutex ownership");
+ // activitiesPerformed is already set to error. No change needed.
+ } else if (result == WAIT_FAILED) {
+ LOG_ERROR_MESSAGE(L"Failed to wait on mutex: %#X", GetLastError());
+ // activitiesPerformed is already set to error. No change needed.
+ } else if (result == WAIT_ABANDONED) {
+ LOG_ERROR_MESSAGE(L"Found abandoned mutex");
+ ReleaseMutex(gHandlerMutex);
+ // activitiesPerformed is already set to error. No change needed.
+ } else {
+ // Mutex is being held. It is safe to access gHandlerReturnData.
+ // If gHandlerReturnData.handlerDataHasBeenSet is false, the handler
+ // never ran. Use the error value activitiesPerformed already contains.
+ if (gHandlerReturnData.handlerDataHasBeenSet) {
+ activitiesPerformed = gHandlerReturnData.activitiesPerformed;
+ }
+
+ success = ReleaseMutex(gHandlerMutex);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X",
+ GetLastError());
+ }
+ }
+ }
+
+ if (!WinToast::instance()->hideToast(id)) {
+ LOG_ERROR_MESSAGE(L"Failed to hide notification");
+ }
+ });
+ return activitiesPerformed;
+}
+
+// Previously this function checked that the Firefox build was using English.
+// This was checked because of the peculiar way we were localizing toast
+// notifications where we used a completely different set of strings in English.
+//
+// We've since unified the notification flows but need to clean up unused code
+// and config files - Bug 1826375.
+bool FirefoxInstallIsEnglish() { return false; }
+
+// If a notification is shown, this function will block until the notification
+// is activated or dismissed.
+// aumi is the App User Model ID.
+NotificationActivities MaybeShowNotification(
+ const DefaultBrowserInfo& browserInfo, const wchar_t* aumi, bool force) {
+ // Default to not showing a notification. Any other value will be returned
+ // directly from ShowNotification.
+ NotificationActivities activitiesPerformed = {NotificationType::Initial,
+ NotificationShown::NotShown,
+ NotificationAction::NoAction};
+
+ // Reset notification state machine, user setting default browser to Firefox
+ // is a strong signal that they intend to have it as the default browser.
+ if (browserInfo.currentDefaultBrowser == Browser::Firefox) {
+ ResetInitialNotificationShown();
+ }
+
+ bool initialNotificationShown = GetInitialNotificationShown();
+ if (!initialNotificationShown || force) {
+ if ((browserInfo.currentDefaultBrowser == Browser::EdgeWithBlink &&
+ browserInfo.previousDefaultBrowser == Browser::Firefox) ||
+ force) {
+ return ShowNotification(NotificationType::Initial, aumi);
+ }
+ return activitiesPerformed;
+ }
+ activitiesPerformed.type = NotificationType::Followup;
+
+ ULONGLONG followupNotificationRequestTime =
+ GetFollowupNotificationRequestTime();
+ bool followupNotificationRequested = followupNotificationRequestTime != 0;
+ bool followupNotificationShown = GetFollowupNotificationShown();
+ if (followupNotificationRequested && !followupNotificationShown &&
+ !GetFollowupNotificationSuppressed()) {
+ ULONGLONG secondsSinceRequestTime =
+ SecondsPassedSince(followupNotificationRequestTime);
+
+ if (secondsSinceRequestTime >= SEVEN_DAYS_IN_SECONDS) {
+ // If we go to show the followup notification and the user has already
+ // changed the default browser, permanently suppress the followup since
+ // it's no longer relevant.
+ if (browserInfo.currentDefaultBrowser == Browser::EdgeWithBlink) {
+ return ShowNotification(NotificationType::Followup, aumi);
+ } else {
+ SetFollowupNotificationSuppressed(true);
+ }
+ }
+ }
+ return activitiesPerformed;
+}
+
+std::string GetStringForNotificationType(NotificationType type) {
+ switch (type) {
+ case NotificationType::Initial:
+ return std::string("initial");
+ case NotificationType::Followup:
+ return std::string("followup");
+ }
+}
+
+std::string GetStringForNotificationShown(NotificationShown shown) {
+ switch (shown) {
+ case NotificationShown::NotShown:
+ return std::string("not-shown");
+ case NotificationShown::Shown:
+ return std::string("shown");
+ case NotificationShown::Error:
+ return std::string("error");
+ }
+}
+
+NotificationShown GetNotificationShownFromString(const nsAString& shown) {
+ if (shown == u"not-shown"_ns) {
+ return NotificationShown::NotShown;
+ } else if (shown == u"shown"_ns) {
+ return NotificationShown::Shown;
+ } else if (shown == u"error"_ns) {
+ return NotificationShown::Error;
+ } else {
+ // Catch all.
+ return NotificationShown::Error;
+ }
+}
+
+std::string GetStringForNotificationAction(NotificationAction action) {
+ switch (action) {
+ case NotificationAction::DismissedByTimeout:
+ return std::string("dismissed-by-timeout");
+ case NotificationAction::DismissedToActionCenter:
+ return std::string("dismissed-to-action-center");
+ case NotificationAction::DismissedByButton:
+ return std::string("dismissed-by-button");
+ case NotificationAction::DismissedByApplicationHidden:
+ return std::string("dismissed-by-application-hidden");
+ case NotificationAction::RemindMeLater:
+ return std::string("remind-me-later");
+ case NotificationAction::MakeFirefoxDefaultButton:
+ return std::string("make-firefox-default-button");
+ case NotificationAction::ToastClicked:
+ return std::string("toast-clicked");
+ case NotificationAction::NoAction:
+ return std::string("no-action");
+ }
+}
+
+NotificationAction GetNotificationActionFromString(const nsAString& action) {
+ if (action == u"dismissed-by-timeout"_ns) {
+ return NotificationAction::DismissedByTimeout;
+ } else if (action == u"dismissed-to-action-center"_ns) {
+ return NotificationAction::DismissedToActionCenter;
+ } else if (action == u"dismissed-by-button"_ns) {
+ return NotificationAction::DismissedByButton;
+ } else if (action == u"dismissed-by-application-hidden"_ns) {
+ return NotificationAction::DismissedByApplicationHidden;
+ } else if (action == u"remind-me-later"_ns) {
+ return NotificationAction::RemindMeLater;
+ } else if (action == u"make-firefox-default-button"_ns) {
+ return NotificationAction::MakeFirefoxDefaultButton;
+ } else if (action == u"toast-clicked"_ns) {
+ return NotificationAction::ToastClicked;
+ } else if (action == u"no-action"_ns) {
+ return NotificationAction::NoAction;
+ } else {
+ // Catch all.
+ return NotificationAction::NoAction;
+ }
+}
+
+void EnsureValidNotificationAction(std::string& actionString) {
+ if (actionString != "dismissed-by-timeout" &&
+ actionString != "dismissed-to-action-center" &&
+ actionString != "dismissed-by-button" &&
+ actionString != "dismissed-by-application-hidden" &&
+ actionString != "remind-me-later" &&
+ actionString != "make-firefox-default-button" &&
+ actionString != "toast-clicked" && actionString != "no-action") {
+ actionString = "no-action";
+ }
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/Notification.h b/toolkit/mozapps/defaultagent/Notification.h
new file mode 100644
index 0000000000..210c55f559
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Notification.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_NOTIFICATION_H__
+#define __DEFAULT_BROWSER_NOTIFICATION_H__
+
+#include "DefaultBrowser.h"
+
+namespace mozilla::default_agent {
+
+enum class NotificationType {
+ Initial,
+ Followup,
+};
+
+enum class NotificationShown {
+ NotShown,
+ Shown,
+ Error,
+};
+
+enum class NotificationAction {
+ DismissedByTimeout,
+ DismissedToActionCenter,
+ DismissedByButton,
+ DismissedByApplicationHidden,
+ RemindMeLater,
+ MakeFirefoxDefaultButton,
+ ToastClicked,
+ NoAction, // Should not be used with NotificationShown::Shown
+};
+
+struct NotificationActivities {
+ NotificationType type;
+ NotificationShown shown;
+ NotificationAction action;
+};
+
+NotificationActivities MaybeShowNotification(
+ const DefaultBrowserInfo& browserInfo, const wchar_t* aumi, bool force);
+
+// These take enum values and get strings suitable for telemetry
+std::string GetStringForNotificationType(NotificationType type);
+std::string GetStringForNotificationShown(NotificationShown shown);
+NotificationShown GetNotificationShownFromString(const nsAString& shown);
+std::string GetStringForNotificationAction(NotificationAction action);
+NotificationAction GetNotificationActionFromString(const nsAString& action);
+
+// If actionString is a valid action string (i.e. corresponds to one of the
+// NotificationAction values), this function has no effect. If actionString is
+// not a valid action string, its value will be replaced with the string for
+// NotificationAction::NoAction.
+void EnsureValidNotificationAction(std::string& actionString);
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_NOTIFICATION_H__
diff --git a/toolkit/mozapps/defaultagent/Policy.cpp b/toolkit/mozapps/defaultagent/Policy.cpp
new file mode 100644
index 0000000000..f8efdf24a2
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Policy.cpp
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "Policy.h"
+
+#include <windows.h>
+#include <shlwapi.h>
+#include <fstream>
+
+#include "common.h"
+#include "Registry.h"
+#include "UtfConvert.h"
+
+#include "json/json.h"
+#include "mozilla/HelperMacros.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+// There is little logging or error handling in this file, because the file and
+// registry values we are reading here are normally absent, so never finding
+// anything that we look for at all would not be an error worth generating an
+// event log for.
+
+#define AGENT_POLICY_NAME "DisableDefaultBrowserAgent"
+#define TELEMETRY_POLICY_NAME "DisableTelemetry"
+
+// The Firefox policy engine hardcodes the string "Mozilla" in its registry
+// key accesses rather than using the configured vendor name, so we should do
+// the same here to be sure we're compatible with it.
+#define POLICY_REGKEY_NAME L"SOFTWARE\\Policies\\Mozilla\\" MOZ_APP_BASENAME
+
+namespace mozilla::default_agent {
+
+// This enum is the return type for the functions that check policy values.
+enum class PolicyState {
+ Enabled, // There is a policy explicitly set to enabled
+ Disabled, // There is a policy explicitly set to disabled
+ NoPolicy, // This policy isn't configured
+};
+
+static PolicyState FindPolicyInRegistry(HKEY rootKey,
+ const wchar_t* policyName) {
+ HKEY rawRegKey = nullptr;
+ RegOpenKeyExW(rootKey, POLICY_REGKEY_NAME, 0, KEY_READ, &rawRegKey);
+
+ nsAutoRegKey regKey(rawRegKey);
+
+ if (!regKey) {
+ return PolicyState::NoPolicy;
+ }
+
+ // If this key is empty and doesn't have any actual policies in it,
+ // treat that the same as the key not existing and return no result.
+ DWORD numSubKeys = 0, numValues = 0;
+ LSTATUS ls = RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr,
+ &numSubKeys, nullptr, nullptr, &numValues,
+ nullptr, nullptr, nullptr, nullptr);
+ if (ls != ERROR_SUCCESS) {
+ return PolicyState::NoPolicy;
+ }
+
+ DWORD policyValue = UINT32_MAX;
+ DWORD policyValueSize = sizeof(policyValue);
+ ls = RegGetValueW(regKey.get(), nullptr, policyName, RRF_RT_REG_DWORD,
+ nullptr, &policyValue, &policyValueSize);
+
+ if (ls != ERROR_SUCCESS) {
+ return PolicyState::NoPolicy;
+ }
+ return policyValue == 0 ? PolicyState::Disabled : PolicyState::Enabled;
+}
+
+static PolicyState FindPolicyInFile(const char* policyName) {
+ mozilla::UniquePtr<wchar_t[]> thisBinaryPath = mozilla::GetFullBinaryPath();
+ if (!PathRemoveFileSpecW(thisBinaryPath.get())) {
+ return PolicyState::NoPolicy;
+ }
+
+ wchar_t policiesFilePath[MAX_PATH] = L"";
+ if (!PathCombineW(policiesFilePath, thisBinaryPath.get(), L"distribution")) {
+ return PolicyState::NoPolicy;
+ }
+
+ if (!PathAppendW(policiesFilePath, L"policies.json")) {
+ return PolicyState::NoPolicy;
+ }
+
+ // We need a narrow string-based std::ifstream because that's all jsoncpp can
+ // use; that means we need to supply it the file path as a narrow string.
+ Utf16ToUtf8Result policiesFilePathToUtf8 = Utf16ToUtf8(policiesFilePath);
+ if (policiesFilePathToUtf8.isErr()) {
+ return PolicyState::NoPolicy;
+ }
+ std::string policiesFilePathA = policiesFilePathToUtf8.unwrap();
+
+ Json::Value jsonRoot;
+ std::ifstream stream(policiesFilePathA);
+ Json::Reader().parse(stream, jsonRoot);
+
+ if (jsonRoot.isObject() && jsonRoot.isMember("Policies") &&
+ jsonRoot["Policies"].isObject()) {
+ if (jsonRoot["Policies"].isMember(policyName) &&
+ jsonRoot["Policies"][policyName].isBool()) {
+ return jsonRoot["Policies"][policyName].asBool() ? PolicyState::Enabled
+ : PolicyState::Disabled;
+ } else {
+ return PolicyState::NoPolicy;
+ }
+ }
+
+ return PolicyState::NoPolicy;
+}
+
+static PolicyState IsDisabledByPref(const wchar_t* prefRegValue) {
+ auto prefValueResult =
+ RegistryGetValueBool(IsPrefixed::Prefixed, prefRegValue);
+
+ if (prefValueResult.isErr()) {
+ return PolicyState::NoPolicy;
+ }
+ auto prefValue = prefValueResult.unwrap();
+ if (prefValue.isNothing()) {
+ return PolicyState::NoPolicy;
+ }
+ return prefValue.value() ? PolicyState::Enabled : PolicyState::Disabled;
+}
+
+// Everything we call from this function wants wide strings, except for jsoncpp,
+// which cannot work with them at all, so at some point we need both formats.
+// It's awkward to take both formats as individual arguments, but it would be
+// more awkward to take one and runtime convert it to the other, or to turn
+// this function into a macro so that the preprocessor can trigger the
+// conversion for us, so this is what we've got.
+static bool IsThingDisabled(const char* thing, const wchar_t* wideThing) {
+ // The logic here is intended to be the same as that used by Firefox's policy
+ // engine implementation; they should be kept in sync. We have added the pref
+ // check at the end though, since that's our own custom mechanism.
+ PolicyState state = FindPolicyInRegistry(HKEY_LOCAL_MACHINE, wideThing);
+ if (state == PolicyState::NoPolicy) {
+ state = FindPolicyInRegistry(HKEY_CURRENT_USER, wideThing);
+ }
+ if (state == PolicyState::NoPolicy) {
+ state = FindPolicyInFile(thing);
+ }
+ if (state == PolicyState::NoPolicy) {
+ state = IsDisabledByPref(wideThing);
+ }
+ return state == PolicyState::Enabled ? true : false;
+}
+
+bool IsAgentDisabled() {
+ return IsThingDisabled(AGENT_POLICY_NAME, L"" AGENT_POLICY_NAME);
+}
+
+bool IsTelemetryDisabled() {
+ return IsThingDisabled(TELEMETRY_POLICY_NAME, L"" TELEMETRY_POLICY_NAME);
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/Policy.h b/toolkit/mozapps/defaultagent/Policy.h
new file mode 100644
index 0000000000..2a07a94543
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Policy.h
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_POLICY_H__
+#define __DEFAULT_BROWSER_AGENT_POLICY_H__
+
+namespace mozilla::default_agent {
+
+bool IsAgentDisabled();
+bool IsTelemetryDisabled();
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_AGENT_POLICY_H__
diff --git a/toolkit/mozapps/defaultagent/Registry.cpp b/toolkit/mozapps/defaultagent/Registry.cpp
new file mode 100644
index 0000000000..a2153c50ac
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Registry.cpp
@@ -0,0 +1,330 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "Registry.h"
+
+#include <windows.h>
+#include <shlwapi.h>
+
+#include "common.h"
+#include "EventLog.h"
+#include "UtfConvert.h"
+
+#include "mozilla/Buffer.h"
+#include "mozilla/Try.h"
+
+namespace mozilla::default_agent {
+
+using WStringResult = mozilla::WindowsErrorResult<std::wstring>;
+
+static WStringResult MaybePrefixRegistryValueName(
+ IsPrefixed isPrefixed, const wchar_t* registryValueNameSuffix) {
+ if (isPrefixed == IsPrefixed::Unprefixed) {
+ std::wstring registryValueName = registryValueNameSuffix;
+ return registryValueName;
+ }
+
+ mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
+ if (!PathRemoveFileSpecW(installPath.get())) {
+ HRESULT hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+ std::wstring registryValueName(installPath.get());
+ registryValueName.append(L"|");
+ registryValueName.append(registryValueNameSuffix);
+
+ return registryValueName;
+}
+
+// Creates a sub key of AGENT_REGKEY_NAME by appending the passed subKey. If
+// subKey is null, nothing is appended.
+static std::wstring MakeKeyName(const wchar_t* subKey) {
+ std::wstring keyName = AGENT_REGKEY_NAME;
+ if (subKey) {
+ keyName += L"\\";
+ keyName += subKey;
+ }
+ return keyName;
+}
+
+MaybeStringResult RegistryGetValueString(
+ IsPrefixed isPrefixed, const wchar_t* registryValueName,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ // Get the string size
+ DWORD wideDataSize = 0;
+ LSTATUS ls =
+ RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(),
+ RRF_RT_REG_SZ, nullptr, nullptr, &wideDataSize);
+ if (ls == ERROR_FILE_NOT_FOUND) {
+ return mozilla::Maybe<std::string>(mozilla::Nothing());
+ } else if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ // Convert bytes to characters. The extra character should be unnecessary, but
+ // addresses the possible rounding problem inherent with integer division.
+ DWORD charCount = (wideDataSize / sizeof(wchar_t)) + 1;
+
+ // Read the data from the registry into a wide string
+ mozilla::Buffer<wchar_t> wideData(charCount);
+ ls = RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(),
+ RRF_RT_REG_SZ, nullptr, wideData.Elements(), &wideDataSize);
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ // Convert to narrow string and return.
+ std::string narrowData;
+ MOZ_TRY_VAR(narrowData, Utf16ToUtf8(wideData.Elements()));
+
+ return mozilla::Some(narrowData);
+}
+
+VoidResult RegistrySetValueString(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const char* newValue,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ // Convert the value from a narrow string to a wide string
+ std::wstring wideValue;
+ MOZ_TRY_VAR(wideValue, Utf8ToUtf16(newValue));
+
+ // Store the value
+ LSTATUS ls = RegSetKeyValueW(HKEY_CURRENT_USER, keyName.c_str(),
+ valueName.c_str(), REG_SZ, wideValue.c_str(),
+ (wideValue.size() + 1) * sizeof(wchar_t));
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return mozilla::Ok();
+}
+
+MaybeBoolResult RegistryGetValueBool(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ // Read the integer value from the registry
+ DWORD value;
+ DWORD valueSize = sizeof(DWORD);
+ LSTATUS ls =
+ RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(),
+ RRF_RT_REG_DWORD, nullptr, &value, &valueSize);
+ if (ls == ERROR_FILE_NOT_FOUND) {
+ return mozilla::Maybe<bool>(mozilla::Nothing());
+ }
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return mozilla::Some(value != 0);
+}
+
+VoidResult RegistrySetValueBool(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName, bool newValue,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ // Write the value to the registry
+ DWORD value = newValue ? 1 : 0;
+ LSTATUS ls =
+ RegSetKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(),
+ REG_DWORD, &value, sizeof(DWORD));
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return mozilla::Ok();
+}
+
+MaybeQwordResult RegistryGetValueQword(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ // Read the integer value from the registry
+ ULONGLONG value;
+ DWORD valueSize = sizeof(ULONGLONG);
+ LSTATUS ls =
+ RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(),
+ RRF_RT_REG_QWORD, nullptr, &value, &valueSize);
+ if (ls == ERROR_FILE_NOT_FOUND) {
+ return mozilla::Maybe<ULONGLONG>(mozilla::Nothing());
+ }
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return mozilla::Some(value);
+}
+
+VoidResult RegistrySetValueQword(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ ULONGLONG newValue,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ // Write the value to the registry
+ LSTATUS ls =
+ RegSetKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(),
+ REG_QWORD, &newValue, sizeof(ULONGLONG));
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return mozilla::Ok();
+}
+
+MaybeDwordResult RegistryGetValueDword(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ // Read the integer value from the registry
+ uint32_t value;
+ DWORD valueSize = sizeof(uint32_t);
+ LSTATUS ls =
+ RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(),
+ RRF_RT_DWORD, nullptr, &value, &valueSize);
+ if (ls == ERROR_FILE_NOT_FOUND) {
+ return mozilla::Maybe<uint32_t>(mozilla::Nothing());
+ }
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return mozilla::Some(value);
+}
+
+VoidResult RegistrySetValueDword(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ uint32_t newValue,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ // Write the value to the registry
+ LSTATUS ls =
+ RegSetKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(),
+ REG_DWORD, &newValue, sizeof(uint32_t));
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return mozilla::Ok();
+}
+
+VoidResult RegistryDeleteValue(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey /* = nullptr */) {
+ // Get the full registry value name
+ WStringResult registryValueNameResult =
+ MaybePrefixRegistryValueName(isPrefixed, registryValueName);
+ if (registryValueNameResult.isErr()) {
+ return mozilla::Err(registryValueNameResult.unwrapErr());
+ }
+ std::wstring valueName = registryValueNameResult.unwrap();
+
+ std::wstring keyName = MakeKeyName(subKey);
+
+ LSTATUS ls =
+ RegDeleteKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str());
+ if (ls != ERROR_SUCCESS) {
+ HRESULT hr = HRESULT_FROM_WIN32(ls);
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return mozilla::Ok();
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/Registry.h b/toolkit/mozapps/defaultagent/Registry.h
new file mode 100644
index 0000000000..26bea1ae72
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Registry.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_REGISTRY_H__
+#define __DEFAULT_BROWSER_AGENT_REGISTRY_H__
+
+#include <cstdint>
+#include <windows.h>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+namespace mozilla::default_agent {
+
+// Indicates whether or not a registry value name is prefixed with the install
+// directory path (Prefixed), or not (Unprefixed). Prefixing a registry value
+// name with the install directory makes that value specific to this
+// installation's default browser agent.
+enum class IsPrefixed {
+ Prefixed,
+ Unprefixed,
+};
+
+// The result of an operation only, containing no other data on success.
+using VoidResult = mozilla::WindowsErrorResult<mozilla::Ok>;
+
+using MaybeString = mozilla::Maybe<std::string>;
+using MaybeStringResult = mozilla::WindowsErrorResult<MaybeString>;
+// Get a string from the registry. If necessary, value name prefixing will be
+// performed automatically.
+// Strings are stored as wide strings, but are converted to narrow UTF-8 before
+// being returned.
+MaybeStringResult RegistryGetValueString(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey = nullptr);
+
+// Set a string in the registry. If necessary, value name prefixing will be
+// performed automatically.
+// Strings are converted to wide strings for registry storage.
+VoidResult RegistrySetValueString(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const char* newValue,
+ const wchar_t* subKey = nullptr);
+
+using MaybeBoolResult = mozilla::WindowsErrorResult<mozilla::Maybe<bool>>;
+// Get a bool from the registry.
+// Bools are stored as a single DWORD, with 0 meaning false and any other value
+// meaning true.
+MaybeBoolResult RegistryGetValueBool(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey = nullptr);
+
+// Set a bool in the registry. If necessary, value name prefixing will be
+// performed automatically.
+// Bools are stored as a single DWORD, with 0 meaning false and any other value
+// meaning true.
+VoidResult RegistrySetValueBool(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName, bool newValue,
+ const wchar_t* subKey = nullptr);
+
+using MaybeQwordResult = mozilla::WindowsErrorResult<mozilla::Maybe<ULONGLONG>>;
+// Get a QWORD (ULONGLONG) from the registry. If necessary, value name prefixing
+// will be performed automatically.
+MaybeQwordResult RegistryGetValueQword(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey = nullptr);
+
+// Get a QWORD (ULONGLONG) in the registry. If necessary, value name prefixing
+// will be performed automatically.
+VoidResult RegistrySetValueQword(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ ULONGLONG newValue,
+ const wchar_t* subKey = nullptr);
+
+using MaybeDword = mozilla::Maybe<uint32_t>;
+using MaybeDwordResult = mozilla::WindowsErrorResult<MaybeDword>;
+// Get a DWORD (uint32_t) from the registry. If necessary, value name prefixing
+// will be performed automatically.
+MaybeDwordResult RegistryGetValueDword(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey = nullptr);
+
+// Get a DWORD (uint32_t) in the registry. If necessary, value name prefixing
+// will be performed automatically.
+VoidResult RegistrySetValueDword(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ uint32_t newValue,
+ const wchar_t* subKey = nullptr);
+
+VoidResult RegistryDeleteValue(IsPrefixed isPrefixed,
+ const wchar_t* registryValueName,
+ const wchar_t* subKey = nullptr);
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_AGENT_REGISTRY_H__
diff --git a/toolkit/mozapps/defaultagent/ScheduledTask.cpp b/toolkit/mozapps/defaultagent/ScheduledTask.cpp
new file mode 100644
index 0000000000..a9cd647c03
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/ScheduledTask.cpp
@@ -0,0 +1,328 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "ScheduledTask.h"
+#include "ScheduledTaskRemove.h"
+
+#include <string>
+#include <time.h>
+
+#include <comutil.h>
+#include <taskschd.h>
+
+#include "readstrings.h"
+#include "updatererrors.h"
+#include "EventLog.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "WindowsDefaultBrowser.h"
+
+#include "DefaultBrowser.h"
+
+#include "mozilla/ErrorResult.h"
+#include "mozilla/intl/Localization.h"
+#include "nsString.h"
+#include "nsTArray.h"
+using mozilla::intl::Localization;
+
+namespace mozilla::default_agent {
+
+// The task scheduler requires its time values to come in the form of a string
+// in the format YYYY-MM-DDTHH:MM:SSZ. This format string is used to get that
+// out of the C library wcsftime function.
+const wchar_t* kTimeFormat = L"%Y-%m-%dT%H:%M:%SZ";
+// The expanded time string should always be this length, for example:
+// 2020-02-12T16:59:32Z
+const size_t kTimeStrMaxLen = 20;
+
+#define ENSURE(x) \
+ if (FAILED(hr = (x))) { \
+ LOG_ERROR(hr); \
+ return hr; \
+ }
+
+bool GetTaskDescription(mozilla::UniquePtr<wchar_t[]>& description) {
+ mozilla::UniquePtr<wchar_t[]> installPath;
+ bool success = GetInstallDirectory(installPath);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Failed to get install directory");
+ return false;
+ }
+ nsTArray<nsCString> resIds = {"branding/brand.ftl"_ns,
+ "browser/backgroundtasks/defaultagent.ftl"_ns};
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+ nsAutoCString daTaskDesc;
+ mozilla::ErrorResult rv;
+ l10n->FormatValueSync("default-browser-agent-task-description"_ns, {},
+ daTaskDesc, rv);
+ if (rv.Failed()) {
+ LOG_ERROR_MESSAGE(L"Failed to read task description");
+ return false;
+ }
+ NS_ConvertUTF8toUTF16 daTaskDescW(daTaskDesc);
+ description = mozilla::MakeUnique<wchar_t[]>(daTaskDescW.Length() + 1);
+ wcsncpy(description.get(), daTaskDescW.get(), daTaskDescW.Length() + 1);
+ return true;
+}
+
+HRESULT RegisterTask(const wchar_t* uniqueToken,
+ BSTR startTime /* = nullptr */) {
+ // Do data migration during the task installation. This might seem like it
+ // belongs in UpdateTask, but we want to be able to call
+ // RemoveTasks();
+ // RegisterTask();
+ // and still have data migration happen. Also, UpdateTask calls this function,
+ // so migration will still get run in that case.
+ MaybeMigrateCurrentDefault();
+
+ // Make sure we don't try to register a task that already exists.
+ RemoveTasks(uniqueToken, WhichTasks::WdbaTaskOnly);
+
+ // If we create a folder and then fail to create the task, we need to
+ // remember to delete the folder so that whatever set of permissions it ends
+ // up with doesn't interfere with trying to create the task again later, and
+ // so that we don't just leave an empty folder behind.
+ bool createdFolder = false;
+
+ HRESULT hr = S_OK;
+ RefPtr<ITaskService> scheduler;
+ ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ITaskService, getter_AddRefs(scheduler)));
+
+ ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
+
+ RefPtr<ITaskFolder> rootFolder;
+ BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
+ ENSURE(
+ scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
+
+ RefPtr<ITaskFolder> taskFolder;
+ BStrPtr vendorBStr = BStrPtr(SysAllocString(kTaskVendor));
+ if (FAILED(rootFolder->GetFolder(vendorBStr.get(),
+ getter_AddRefs(taskFolder)))) {
+ hr = rootFolder->CreateFolder(vendorBStr.get(), VARIANT{},
+ getter_AddRefs(taskFolder));
+
+ if (SUCCEEDED(hr)) {
+ createdFolder = true;
+ } else if (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) {
+ // `CreateFolder` doesn't assign to the out pointer on
+ // `ERROR_ALREADY_EXISTS`, so try to get the folder again. This behavior
+ // is undocumented but was verified in a debugger.
+ HRESULT priorHr = hr;
+ hr = rootFolder->GetFolder(vendorBStr.get(), getter_AddRefs(taskFolder));
+
+ if (FAILED(hr)) {
+ LOG_ERROR(priorHr);
+ LOG_ERROR(hr);
+ return hr;
+ }
+ } else {
+ LOG_ERROR(hr);
+ return hr;
+ }
+ }
+
+ auto cleanupFolder =
+ mozilla::MakeScopeExit([&hr, createdFolder, &rootFolder, &vendorBStr] {
+ if (createdFolder && FAILED(hr)) {
+ // If this fails, we can't really handle that intelligently, so
+ // don't even bother to check the return code.
+ rootFolder->DeleteFolder(vendorBStr.get(), 0);
+ }
+ });
+
+ RefPtr<ITaskDefinition> newTask;
+ ENSURE(scheduler->NewTask(0, getter_AddRefs(newTask)));
+
+ mozilla::UniquePtr<wchar_t[]> description;
+ if (!GetTaskDescription(description)) {
+ return E_FAIL;
+ }
+ BStrPtr descriptionBstr = BStrPtr(SysAllocString(description.get()));
+
+ RefPtr<IRegistrationInfo> taskRegistration;
+ ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(taskRegistration)));
+ ENSURE(taskRegistration->put_Description(descriptionBstr.get()));
+
+ RefPtr<ITaskSettings> taskSettings;
+ ENSURE(newTask->get_Settings(getter_AddRefs(taskSettings)));
+ ENSURE(taskSettings->put_DisallowStartIfOnBatteries(VARIANT_FALSE));
+ ENSURE(taskSettings->put_MultipleInstances(TASK_INSTANCES_IGNORE_NEW));
+ ENSURE(taskSettings->put_StartWhenAvailable(VARIANT_TRUE));
+ ENSURE(taskSettings->put_StopIfGoingOnBatteries(VARIANT_FALSE));
+ // This cryptic string means "12 hours 5 minutes". So, if the task runs for
+ // longer than that, the process will be killed, because that should never
+ // happen. See
+ // https://docs.microsoft.com/en-us/windows/win32/taskschd/tasksettings-executiontimelimit
+ // for a detailed explanation of these strings.
+ BStrPtr execTimeLimitBStr = BStrPtr(SysAllocString(L"PT12H5M"));
+ ENSURE(taskSettings->put_ExecutionTimeLimit(execTimeLimitBStr.get()));
+
+ RefPtr<IRegistrationInfo> regInfo;
+ ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(regInfo)));
+
+ ENSURE(regInfo->put_Author(vendorBStr.get()));
+
+ RefPtr<ITriggerCollection> triggers;
+ ENSURE(newTask->get_Triggers(getter_AddRefs(triggers)));
+
+ RefPtr<ITrigger> newTrigger;
+ ENSURE(triggers->Create(TASK_TRIGGER_DAILY, getter_AddRefs(newTrigger)));
+
+ RefPtr<IDailyTrigger> dailyTrigger;
+ ENSURE(newTrigger->QueryInterface(IID_IDailyTrigger,
+ getter_AddRefs(dailyTrigger)));
+
+ if (startTime) {
+ ENSURE(dailyTrigger->put_StartBoundary(startTime));
+ } else {
+ // The time that the task is scheduled to run at every day is taken from the
+ // time in the trigger's StartBoundary property. We'll set this to the
+ // current time, on the theory that the time at which we're being installed
+ // is a time that the computer is likely to be on other days. If our
+ // theory is wrong and the computer is offline at the scheduled time, then
+ // because we've set StartWhenAvailable above, the task will run whenever
+ // it wakes up. Since our task is entirely in the background and doesn't use
+ // a lot of resources, we're not concerned about it bothering the user if it
+ // runs while they're actively using this computer.
+ time_t now_t = time(nullptr);
+ // Subtract a minute from the current time, to avoid "winning" a potential
+ // race with the scheduler that might have it start the task immediately
+ // after we register it, if we finish doing that and then it evaluates the
+ // trigger during the same second. We haven't seen this happen in practice,
+ // but there's no documented guarantee that it won't, so let's be sure.
+ now_t -= 60;
+
+ tm now_tm;
+ errno_t errno_rv = gmtime_s(&now_tm, &now_t);
+ if (errno_rv != 0) {
+ // The C runtime has a (private) function to convert Win32 error codes to
+ // errno values, but there's nothing that goes the other way, and it
+ // isn't worth including one here for something that's this unlikely to
+ // fail anyway. So just return a generic error.
+ hr = HRESULT_FROM_WIN32(ERROR_INVALID_TIME);
+ LOG_ERROR(hr);
+ return hr;
+ }
+
+ mozilla::UniquePtr<wchar_t[]> timeStr =
+ mozilla::MakeUnique<wchar_t[]>(kTimeStrMaxLen + 1);
+
+ if (wcsftime(timeStr.get(), kTimeStrMaxLen + 1, kTimeFormat, &now_tm) ==
+ 0) {
+ hr = E_NOT_SUFFICIENT_BUFFER;
+ LOG_ERROR(hr);
+ return hr;
+ }
+
+ BStrPtr startTimeBStr = BStrPtr(SysAllocString(timeStr.get()));
+ ENSURE(dailyTrigger->put_StartBoundary(startTimeBStr.get()));
+ }
+
+ ENSURE(dailyTrigger->put_DaysInterval(1));
+
+ RefPtr<IActionCollection> actions;
+ ENSURE(newTask->get_Actions(getter_AddRefs(actions)));
+
+ RefPtr<IAction> action;
+ ENSURE(actions->Create(TASK_ACTION_EXEC, getter_AddRefs(action)));
+
+ RefPtr<IExecAction> execAction;
+ ENSURE(action->QueryInterface(IID_IExecAction, getter_AddRefs(execAction)));
+
+ // Register proxy instead of Firefox background task.
+ mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
+ if (!PathRemoveFileSpecW(installPath.get())) {
+ return E_FAIL;
+ }
+ std::wstring proxyPath(installPath.get());
+ proxyPath += L"\\default-browser-agent.exe";
+
+ BStrPtr binaryPathBStr = BStrPtr(SysAllocString(proxyPath.c_str()));
+ ENSURE(execAction->put_Path(binaryPathBStr.get()));
+
+ std::wstring taskArgs = L"do-task \"";
+ taskArgs += uniqueToken;
+ taskArgs += L"\"";
+ BStrPtr argsBStr = BStrPtr(SysAllocString(taskArgs.c_str()));
+ ENSURE(execAction->put_Arguments(argsBStr.get()));
+
+ std::wstring taskName(kTaskName);
+ taskName += uniqueToken;
+ BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
+
+ RefPtr<IRegisteredTask> registeredTask;
+ ENSURE(taskFolder->RegisterTaskDefinition(
+ taskNameBStr.get(), newTask, TASK_CREATE_OR_UPDATE, VARIANT{}, VARIANT{},
+ TASK_LOGON_INTERACTIVE_TOKEN, VARIANT{}, getter_AddRefs(registeredTask)));
+
+ return hr;
+}
+
+HRESULT UpdateTask(const wchar_t* uniqueToken) {
+ RefPtr<ITaskService> scheduler;
+ HRESULT hr = S_OK;
+ ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ITaskService, getter_AddRefs(scheduler)));
+
+ ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
+
+ RefPtr<ITaskFolder> taskFolder;
+ BStrPtr folderBStr = BStrPtr(SysAllocString(kTaskVendor));
+
+ if (FAILED(
+ scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)))) {
+ // If our folder doesn't exist, create it and the task.
+ return RegisterTask(uniqueToken);
+ }
+
+ std::wstring taskName(kTaskName);
+ taskName += uniqueToken;
+ BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str()));
+
+ RefPtr<IRegisteredTask> task;
+ if (FAILED(taskFolder->GetTask(taskNameBStr.get(), getter_AddRefs(task)))) {
+ // If our task doesn't exist at all, just create one.
+ return RegisterTask(uniqueToken);
+ }
+
+ // If we have a task registered already, we need to recreate it because
+ // something might have changed that we need to update. But we don't
+ // want to restart the schedule from now, because that might mean the
+ // task never runs at all for e.g. Nightly. So create a new task, but
+ // first get and preserve the existing trigger.
+ RefPtr<ITaskDefinition> definition;
+ if (FAILED(task->get_Definition(getter_AddRefs(definition)))) {
+ // This task is broken, make a new one.
+ return RegisterTask(uniqueToken);
+ }
+
+ RefPtr<ITriggerCollection> triggerList;
+ if (FAILED(definition->get_Triggers(getter_AddRefs(triggerList)))) {
+ // This task is broken, make a new one.
+ return RegisterTask(uniqueToken);
+ }
+
+ RefPtr<ITrigger> trigger;
+ if (FAILED(triggerList->get_Item(1, getter_AddRefs(trigger)))) {
+ // This task is broken, make a new one.
+ return RegisterTask(uniqueToken);
+ }
+
+ BSTR startTimeBstr;
+ if (FAILED(trigger->get_StartBoundary(&startTimeBstr))) {
+ // This task is broken, make a new one.
+ return RegisterTask(uniqueToken);
+ }
+ BStrPtr startTime(startTimeBstr);
+
+ return RegisterTask(uniqueToken, startTime.get());
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/ScheduledTask.h b/toolkit/mozapps/defaultagent/ScheduledTask.h
new file mode 100644
index 0000000000..a3709823ad
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/ScheduledTask.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__
+#define __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__
+
+#include <windows.h>
+#include <wtypes.h>
+
+namespace mozilla::default_agent {
+
+// uniqueToken should be a string unique to the installation, so that a
+// separate task can be created for each installation. Typically this will be
+// the install hash string.
+HRESULT RegisterTask(const wchar_t* uniqueToken, BSTR startTime = nullptr);
+HRESULT UpdateTask(const wchar_t* uniqueToken);
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__
diff --git a/toolkit/mozapps/defaultagent/ScheduledTaskRemove.cpp b/toolkit/mozapps/defaultagent/ScheduledTaskRemove.cpp
new file mode 100644
index 0000000000..e672a813e3
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/ScheduledTaskRemove.cpp
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "ScheduledTaskRemove.h"
+
+#include <string>
+
+#include <comutil.h>
+#include <taskschd.h>
+
+#include "EventLog.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla::default_agent {
+
+#define ENSURE(x) \
+ if (FAILED(hr = (x))) { \
+ LOG_ERROR(hr); \
+ return hr; \
+ }
+
+bool EndsWith(const wchar_t* string, const wchar_t* suffix) {
+ size_t string_len = wcslen(string);
+ size_t suffix_len = wcslen(suffix);
+ if (suffix_len > string_len) {
+ return false;
+ }
+ const wchar_t* substring = string + string_len - suffix_len;
+ return wcscmp(substring, suffix) == 0;
+}
+
+HRESULT RemoveTasks(const wchar_t* uniqueToken, WhichTasks tasksToRemove) {
+ if (!uniqueToken || wcslen(uniqueToken) == 0) {
+ return E_INVALIDARG;
+ }
+
+ RefPtr<ITaskService> scheduler;
+ HRESULT hr = S_OK;
+ ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ITaskService, getter_AddRefs(scheduler)));
+
+ ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{}));
+
+ RefPtr<ITaskFolder> taskFolder;
+ BStrPtr folderBStr(SysAllocString(kTaskVendor));
+
+ hr = scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder));
+ if (FAILED(hr)) {
+ if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
+ // Don't return an error code if our folder doesn't exist,
+ // because that just means it's been removed already.
+ return S_OK;
+ } else {
+ return hr;
+ }
+ }
+
+ RefPtr<IRegisteredTaskCollection> tasksInFolder;
+ ENSURE(taskFolder->GetTasks(TASK_ENUM_HIDDEN, getter_AddRefs(tasksInFolder)));
+
+ LONG numTasks = 0;
+ ENSURE(tasksInFolder->get_Count(&numTasks));
+
+ std::wstring WdbaTaskName(kTaskName);
+ WdbaTaskName += uniqueToken;
+
+ // This will be set to the last error that we encounter while deleting tasks.
+ // This allows us to keep attempting to remove the remaining tasks, even if
+ // we encounter an error, while still preserving what error we encountered so
+ // we can return it from this function.
+ HRESULT deleteResult = S_OK;
+ // Set to true if we intentionally skip any tasks.
+ bool tasksSkipped = false;
+
+ for (LONG i = 0; i < numTasks; ++i) {
+ RefPtr<IRegisteredTask> task;
+ // IRegisteredTaskCollection's are 1-indexed.
+ hr = tasksInFolder->get_Item(_variant_t(i + 1), getter_AddRefs(task));
+ if (FAILED(hr)) {
+ deleteResult = hr;
+ continue;
+ }
+
+ BSTR taskName;
+ hr = task->get_Name(&taskName);
+ if (FAILED(hr)) {
+ deleteResult = hr;
+ continue;
+ }
+ // Automatically free taskName when we are done with it.
+ BStrPtr uniqueTaskName(taskName);
+
+ if (tasksToRemove == WhichTasks::WdbaTaskOnly) {
+ if (WdbaTaskName.compare(taskName) != 0) {
+ tasksSkipped = true;
+ continue;
+ }
+ } else { // tasksToRemove == WhichTasks::AllTasksForInstallation
+ if (!EndsWith(taskName, uniqueToken)) {
+ tasksSkipped = true;
+ continue;
+ }
+ }
+
+ hr = taskFolder->DeleteTask(taskName, 0 /* flags */);
+ if (FAILED(hr)) {
+ deleteResult = hr;
+ }
+ }
+
+ // If we successfully removed all the tasks, delete the folder too.
+ if (!tasksSkipped && SUCCEEDED(deleteResult)) {
+ RefPtr<ITaskFolder> rootFolder;
+ BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\"));
+ ENSURE(
+ scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder)));
+ ENSURE(rootFolder->DeleteFolder(folderBStr.get(), 0));
+ }
+
+ return deleteResult;
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/ScheduledTaskRemove.h b/toolkit/mozapps/defaultagent/ScheduledTaskRemove.h
new file mode 100644
index 0000000000..17fd75d5e1
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/ScheduledTaskRemove.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_REMOVE_H__
+#define __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_REMOVE_H__
+
+#include <windows.h>
+#include <wtypes.h>
+
+#include <oleauto.h>
+
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla::default_agent {
+
+struct SysFreeStringDeleter {
+ void operator()(BSTR aPtr) { ::SysFreeString(aPtr); }
+};
+using BStrPtr = mozilla::UniquePtr<OLECHAR, SysFreeStringDeleter>;
+
+static const wchar_t* kTaskVendor = L"" MOZ_APP_VENDOR;
+// kTaskName should have the unique token appended before being used.
+static const wchar_t* kTaskName =
+ L"" MOZ_APP_DISPLAYNAME " Default Browser Agent ";
+
+enum class WhichTasks {
+ WdbaTaskOnly,
+ AllTasksForInstallation,
+};
+HRESULT RemoveTasks(const wchar_t* uniqueToken, WhichTasks tasksToRemove);
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_REMOVE_H__
diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
new file mode 100644
index 0000000000..8bc0889e67
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
@@ -0,0 +1,347 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include <windows.h>
+#include <appmodel.h>
+#include <shlobj.h> // for SHChangeNotify and IApplicationAssociationRegistration
+#include <functional>
+#include <timeapi.h>
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "WindowsUserChoice.h"
+#include "nsThreadUtils.h"
+
+#include "EventLog.h"
+#include "SetDefaultBrowser.h"
+
+namespace mozilla::default_agent {
+
+/*
+ * The implementation for setting extension handlers by writing UserChoice.
+ *
+ * This is used by both SetDefaultBrowserUserChoice and
+ * SetDefaultExtensionHandlersUserChoice.
+ *
+ * @param aAumi The AUMI of the installation to set as default.
+ *
+ * @param aSid Current user's string SID
+ *
+ * @param aExtraFileExtensions Optional array of extra file association pairs to
+ * set as default, like `[ ".pdf", "FirefoxPDF" ]`.
+ *
+ * @returns NS_OK All associations set and checked
+ * successfully.
+ * NS_ERROR_WDBA_REJECTED UserChoice was set, but checking the default
+ * did not return our ProgID.
+ * NS_ERROR_FAILURE Failed to set at least one association.
+ */
+static nsresult SetDefaultExtensionHandlersUserChoiceImpl(
+ const wchar_t* aAumi, const wchar_t* const aSid,
+ const nsTArray<nsString>& aFileExtensions);
+
+static bool AddMillisecondsToSystemTime(SYSTEMTIME& aSystemTime,
+ ULONGLONG aIncrementMS) {
+ FILETIME fileTime;
+ ULARGE_INTEGER fileTimeInt;
+ if (!::SystemTimeToFileTime(&aSystemTime, &fileTime)) {
+ return false;
+ }
+ fileTimeInt.LowPart = fileTime.dwLowDateTime;
+ fileTimeInt.HighPart = fileTime.dwHighDateTime;
+
+ // FILETIME is in units of 100ns.
+ fileTimeInt.QuadPart += aIncrementMS * 1000 * 10;
+
+ fileTime.dwLowDateTime = fileTimeInt.LowPart;
+ fileTime.dwHighDateTime = fileTimeInt.HighPart;
+ SYSTEMTIME tmpSystemTime;
+ if (!::FileTimeToSystemTime(&fileTime, &tmpSystemTime)) {
+ return false;
+ }
+
+ aSystemTime = tmpSystemTime;
+ return true;
+}
+
+// Compare two SYSTEMTIMEs as FILETIME after clearing everything
+// below minutes.
+static bool CheckEqualMinutes(SYSTEMTIME aSystemTime1,
+ SYSTEMTIME aSystemTime2) {
+ aSystemTime1.wSecond = 0;
+ aSystemTime1.wMilliseconds = 0;
+
+ aSystemTime2.wSecond = 0;
+ aSystemTime2.wMilliseconds = 0;
+
+ FILETIME fileTime1;
+ FILETIME fileTime2;
+ if (!::SystemTimeToFileTime(&aSystemTime1, &fileTime1) ||
+ !::SystemTimeToFileTime(&aSystemTime2, &fileTime2)) {
+ return false;
+ }
+
+ return (fileTime1.dwLowDateTime == fileTime2.dwLowDateTime) &&
+ (fileTime1.dwHighDateTime == fileTime2.dwHighDateTime);
+}
+
+static bool SetUserChoiceRegistry(const wchar_t* aExt, const wchar_t* aProgID,
+ mozilla::UniquePtr<wchar_t[]> aHash) {
+ auto assocKeyPath = GetAssociationKeyPath(aExt);
+ if (!assocKeyPath) {
+ return false;
+ }
+
+ LSTATUS ls;
+ HKEY rawAssocKey;
+ ls = ::RegOpenKeyExW(HKEY_CURRENT_USER, assocKeyPath.get(), 0,
+ KEY_READ | KEY_WRITE, &rawAssocKey);
+ if (ls != ERROR_SUCCESS) {
+ LOG_ERROR(HRESULT_FROM_WIN32(ls));
+ return false;
+ }
+ nsAutoRegKey assocKey(rawAssocKey);
+
+ // When Windows creates this key, it is read-only (Deny Set Value), so we need
+ // to delete it first.
+ // We don't set any similar special permissions.
+ ls = ::RegDeleteKeyW(assocKey.get(), L"UserChoice");
+ if (ls != ERROR_SUCCESS) {
+ LOG_ERROR(HRESULT_FROM_WIN32(ls));
+ return false;
+ }
+
+ HKEY rawUserChoiceKey;
+ ls = ::RegCreateKeyExW(assocKey.get(), L"UserChoice", 0, nullptr,
+ 0 /* options */, KEY_READ | KEY_WRITE,
+ 0 /* security attributes */, &rawUserChoiceKey,
+ nullptr);
+ if (ls != ERROR_SUCCESS) {
+ LOG_ERROR(HRESULT_FROM_WIN32(ls));
+ return false;
+ }
+ nsAutoRegKey userChoiceKey(rawUserChoiceKey);
+
+ DWORD progIdByteCount = (::lstrlenW(aProgID) + 1) * sizeof(wchar_t);
+ ls = ::RegSetValueExW(userChoiceKey.get(), L"ProgID", 0, REG_SZ,
+ reinterpret_cast<const unsigned char*>(aProgID),
+ progIdByteCount);
+ if (ls != ERROR_SUCCESS) {
+ LOG_ERROR(HRESULT_FROM_WIN32(ls));
+ return false;
+ }
+
+ DWORD hashByteCount = (::lstrlenW(aHash.get()) + 1) * sizeof(wchar_t);
+ ls = ::RegSetValueExW(userChoiceKey.get(), L"Hash", 0, REG_SZ,
+ reinterpret_cast<const unsigned char*>(aHash.get()),
+ hashByteCount);
+ if (ls != ERROR_SUCCESS) {
+ LOG_ERROR(HRESULT_FROM_WIN32(ls));
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * Set an association with a UserChoice key
+ *
+ * Removes the old key, creates a new one with ProgID and Hash set to
+ * enable a new asociation.
+ *
+ * @param aExt File type or protocol to associate
+ * @param aSid Current user's string SID
+ * @param aProgID ProgID to use for the asociation
+ * @param inMsix Are we running from in an msix package?
+ *
+ * @return true if successful, false on error.
+ */
+static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid,
+ const wchar_t* aProgID, bool inMsix) {
+ if (inMsix) {
+ LOG_ERROR_MESSAGE(L"SetUserChoice does not work on MSIX builds.");
+ return false;
+ }
+
+ SYSTEMTIME hashTimestamp;
+ ::GetSystemTime(&hashTimestamp);
+ auto hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp);
+ if (!hash) {
+ return false;
+ }
+
+ // The hash changes at the end of each minute, so check that the hash should
+ // be the same by the time we're done writing.
+ const ULONGLONG kWriteTimingThresholdMilliseconds = 1000;
+ // Generating the hash could have taken some time, so start from now.
+ SYSTEMTIME writeEndTimestamp;
+ ::GetSystemTime(&writeEndTimestamp);
+ if (!AddMillisecondsToSystemTime(writeEndTimestamp,
+ kWriteTimingThresholdMilliseconds)) {
+ return false;
+ }
+ if (!CheckEqualMinutes(hashTimestamp, writeEndTimestamp)) {
+ LOG_ERROR_MESSAGE(
+ L"Hash is too close to expiration, sleeping until next hash.");
+ ::Sleep(kWriteTimingThresholdMilliseconds * 2);
+
+ // For consistency, use the current time.
+ ::GetSystemTime(&hashTimestamp);
+ hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp);
+ if (!hash) {
+ return false;
+ }
+ }
+
+ // We're outside of an MSIX package and can use the Win32 Registry API.
+ return SetUserChoiceRegistry(aExt, aProgID, std::move(hash));
+}
+
+static bool VerifyUserDefault(const wchar_t* aExt, const wchar_t* aProgID) {
+ RefPtr<IApplicationAssociationRegistration> pAAR;
+ HRESULT hr = ::CoCreateInstance(
+ CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return false;
+ }
+
+ wchar_t* rawRegisteredApp;
+ bool isProtocol = aExt[0] != L'.';
+ // Note: Checks AL_USER instead of AL_EFFECTIVE.
+ hr = pAAR->QueryCurrentDefault(aExt,
+ isProtocol ? AT_URLPROTOCOL : AT_FILEEXTENSION,
+ AL_USER, &rawRegisteredApp);
+ if (FAILED(hr)) {
+ if (hr == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) {
+ LOG_ERROR_MESSAGE(L"UserChoice ProgID %s for %s was rejected", aProgID,
+ aExt);
+ } else {
+ LOG_ERROR(hr);
+ }
+ return false;
+ }
+ mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> registeredApp(
+ rawRegisteredApp);
+
+ if (::CompareStringOrdinal(registeredApp.get(), -1, aProgID, -1, FALSE) !=
+ CSTR_EQUAL) {
+ LOG_ERROR_MESSAGE(
+ L"Default was %s after writing ProgID %s to UserChoice for %s",
+ registeredApp.get(), aProgID, aExt);
+ return false;
+ }
+
+ return true;
+}
+
+nsresult SetDefaultBrowserUserChoice(
+ const wchar_t* aAumi, const nsTArray<nsString>& aExtraFileExtensions) {
+ // Verify that the implementation of UserChoice hashing has not changed by
+ // computing the current default hash and comparing with the existing value.
+ if (!CheckBrowserUserChoiceHashes()) {
+ LOG_ERROR_MESSAGE(L"UserChoice Hash mismatch");
+ return NS_ERROR_WDBA_HASH_CHECK;
+ }
+
+ if (!mozilla::IsWin10CreatorsUpdateOrLater()) {
+ LOG_ERROR_MESSAGE(L"UserChoice hash matched, but Windows build is too old");
+ return NS_ERROR_WDBA_BUILD;
+ }
+
+ auto sid = GetCurrentUserStringSid();
+ if (!sid) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTArray<nsString> browserDefaults = {
+ u"https"_ns, u"FirefoxURL"_ns, u"http"_ns, u"FirefoxURL"_ns,
+ u".html"_ns, u"FirefoxHTML"_ns, u".htm"_ns, u"FirefoxHTML"_ns};
+
+ browserDefaults.AppendElements(aExtraFileExtensions);
+
+ nsresult rv = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(),
+ browserDefaults);
+ if (!NS_SUCCEEDED(rv)) {
+ LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi);
+ }
+
+ // Notify shell to refresh icons
+ ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
+
+ return rv;
+}
+
+nsresult SetDefaultExtensionHandlersUserChoice(
+ const wchar_t* aAumi, const nsTArray<nsString>& aFileExtensions) {
+ auto sid = GetCurrentUserStringSid();
+ if (!sid) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(),
+ aFileExtensions);
+ if (!NS_SUCCEEDED(rv)) {
+ LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi);
+ }
+
+ // Notify shell to refresh icons
+ ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
+
+ return rv;
+}
+
+nsresult SetDefaultExtensionHandlersUserChoiceImpl(
+ const wchar_t* aAumi, const wchar_t* const aSid,
+ const nsTArray<nsString>& aFileExtensions) {
+ UINT32 pfnLen = 0;
+ bool inMsix =
+ GetCurrentPackageFullName(&pfnLen, nullptr) != APPMODEL_ERROR_NO_PACKAGE;
+
+ if (inMsix) {
+ // MSIX packages can not meaningfully modify the registry keys related to
+ // default handlers
+ return NS_ERROR_FAILURE;
+ }
+
+ for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) {
+ const wchar_t* extraFileExtension = aFileExtensions[i].get();
+ const wchar_t* extraProgIDRoot = aFileExtensions[i + 1].get();
+ // Formatting the ProgID here prevents using this helper to target arbitrary
+ // ProgIDs.
+ mozilla::UniquePtr<wchar_t[]> extraProgID;
+ if (inMsix) {
+ nsresult rv = GetMsixProgId(extraFileExtension, extraProgID);
+ if (NS_FAILED(rv)) {
+ LOG_ERROR_MESSAGE(L"Failed to retrieve MSIX progID for %s",
+ extraFileExtension);
+ return rv;
+ }
+ } else {
+ extraProgID = FormatProgID(extraProgIDRoot, aAumi);
+ if (!CheckProgIDExists(extraProgID.get())) {
+ LOG_ERROR_MESSAGE(L"ProgID %s not found", extraProgID.get());
+ return NS_ERROR_WDBA_NO_PROGID;
+ }
+ }
+
+ if (!SetUserChoice(extraFileExtension, aSid, extraProgID.get(), inMsix)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!VerifyUserDefault(extraFileExtension, extraProgID.get())) {
+ return NS_ERROR_WDBA_REJECTED;
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.h b/toolkit/mozapps/defaultagent/SetDefaultBrowser.h
new file mode 100644
index 0000000000..bb33365058
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__
+#define DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__
+
+#include "nsStringFwd.h"
+#include "nsArray.h"
+#include <functional>
+
+namespace mozilla::default_agent {
+
+/*
+ * Set the default browser by writing the UserChoice registry keys.
+ *
+ * This sets the associations for https, http, .html, and .htm, and
+ * optionally for additional extra file extensions.
+ *
+ * When the agent is run with set-default-browser-user-choice,
+ * the exit code is the result of this function.
+ *
+ * @param aAumi The AUMI of the installation to set as default.
+ *
+ * @param aExtraFileExtensions Optional array of extra file association pairs to
+ * set as default, like `[ ".pdf", "FirefoxPDF" ]`.
+ *
+ * @return NS_OK All associations set and checked
+ * successfully.
+ * NS_ERROR_WDBA_NO_PROGID The ProgID classes had not been registered.
+ * NS_ERROR_WDBA_HASH_CHECK The existing UserChoice Hash could not be
+ * verified.
+ * NS_ERROR_WDBA_REJECTED UserChoice was set, but checking the default
+ * did not return our ProgID.
+ * NS_ERROR_WDBA_BUILD The existing UserChoice Hash was verified,
+ * but we're on an older, unsupported Windows
+ * build, so do not attempt to update the
+ * UserChoice hash.
+ * NS_ERROR_FAILURE other failure
+ */
+nsresult SetDefaultBrowserUserChoice(
+ const wchar_t* aAumi,
+ const nsTArray<nsString>& aExtraFileExtensions = nsTArray<nsString>());
+
+/*
+ * Set the default extension handlers for the given file extensions by writing
+ * the UserChoice registry keys.
+ *
+ * @param aAumi The AUMI of the installation to set as default.
+ *
+ * @param aExtraFileExtensions Optional array of extra file association pairs to
+ * set as default, like `[ ".pdf", "FirefoxPDF" ]`.
+ *
+ * @returns NS_OK All associations set and checked
+ * successfully.
+ * NS_ERROR_WDBA_REJECTED UserChoice was set, but checking the default
+ * did not return our ProgID.
+ * NS_ERROR_FAILURE Failed to set at least one association.
+ */
+nsresult SetDefaultExtensionHandlersUserChoice(
+ const wchar_t* aAumi, const nsTArray<nsString>& aFileExtensions);
+
+} // namespace mozilla::default_agent
+
+#endif // DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__
diff --git a/toolkit/mozapps/defaultagent/Telemetry.cpp b/toolkit/mozapps/defaultagent/Telemetry.cpp
new file mode 100644
index 0000000000..0b71fb7949
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Telemetry.cpp
@@ -0,0 +1,585 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "Telemetry.h"
+
+#include <fstream>
+#include <string>
+
+#include <windows.h>
+
+#include <knownfolders.h>
+#include <shlobj_core.h>
+
+#include "common.h"
+#include "Cache.h"
+#include "EventLog.h"
+#include "Notification.h"
+#include "Policy.h"
+#include "UtfConvert.h"
+#include "Registry.h"
+
+#include "json/json.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/glean/GleanPings.h"
+#include "mozilla/HelperMacros.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "nsStringFwd.h"
+
+#define TELEMETRY_BASE_URL "https://incoming.telemetry.mozilla.org/submit"
+#define TELEMETRY_NAMESPACE "default-browser-agent"
+#define TELEMETRY_PING_VERSION "1"
+#define TELEMETRY_PING_DOCTYPE "default-browser"
+
+// This is almost the complete URL, just needs a UUID appended.
+#define TELEMETRY_PING_URL \
+ TELEMETRY_BASE_URL "/" TELEMETRY_NAMESPACE "/" TELEMETRY_PING_DOCTYPE \
+ "/" TELEMETRY_PING_VERSION "/"
+
+// We only want to send one ping per day. However, this is slightly less than 24
+// hours so that we have a little bit of wiggle room on our task, which is also
+// supposed to run every 24 hours.
+#define MINIMUM_PING_PERIOD_SEC ((23 * 60 * 60) + (45 * 60))
+
+#define PREV_NOTIFICATION_ACTION_REG_NAME L"PrevNotificationAction"
+
+#if !defined(RRF_SUBKEY_WOW6464KEY)
+# define RRF_SUBKEY_WOW6464KEY 0x00010000
+#endif // !defined(RRF_SUBKEY_WOW6464KEY)
+
+namespace mozilla::default_agent {
+
+using TelemetryFieldResult = mozilla::WindowsErrorResult<std::string>;
+using BoolResult = mozilla::WindowsErrorResult<bool>;
+
+// This function was copied from the implementation of
+// nsITelemetry::isOfficialTelemetry, currently found in the file
+// toolkit/components/telemetry/core/Telemetry.cpp.
+static bool IsOfficialTelemetry() {
+#if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && \
+ !defined(DEBUG)
+ return true;
+#else
+ return false;
+#endif
+}
+
+static TelemetryFieldResult GetOSVersion() {
+ OSVERSIONINFOEXW osv = {sizeof(osv)};
+ if (::GetVersionExW(reinterpret_cast<OSVERSIONINFOW*>(&osv))) {
+ std::ostringstream oss;
+ oss << osv.dwMajorVersion << "." << osv.dwMinorVersion << "."
+ << osv.dwBuildNumber;
+
+ if (osv.dwMajorVersion == 10 && osv.dwMinorVersion == 0) {
+ // Get the "Update Build Revision" (UBR) value
+ DWORD ubrValue;
+ DWORD ubrValueLen = sizeof(ubrValue);
+ LSTATUS ubrOk =
+ ::RegGetValueW(HKEY_LOCAL_MACHINE,
+ L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
+ L"UBR", RRF_RT_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr,
+ &ubrValue, &ubrValueLen);
+ if (ubrOk == ERROR_SUCCESS) {
+ oss << "." << ubrValue;
+ }
+ }
+
+ return oss.str();
+ }
+
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+}
+
+static TelemetryFieldResult GetOSLocale() {
+ wchar_t localeName[LOCALE_NAME_MAX_LENGTH] = L"";
+ if (!GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH)) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ // We'll need the locale string in UTF-8 to be able to submit it.
+ Utf16ToUtf8Result narrowLocaleName = Utf16ToUtf8(localeName);
+
+ return narrowLocaleName.unwrapOr("");
+}
+
+static FilePathResult GetPingFilePath(std::wstring& uuid) {
+ wchar_t* rawAppDataPath;
+ HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr,
+ &rawAppDataPath);
+ if (FAILED(hr)) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> appDataPath(
+ rawAppDataPath);
+
+ // The Path* functions don't set LastError, but this is the only thing that
+ // can really cause them to fail, so if they ever do we assume this is why.
+ hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
+
+ wchar_t pingFilePath[MAX_PATH] = L"";
+ if (!PathCombineW(pingFilePath, appDataPath.get(), L"" MOZ_APP_VENDOR)) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ if (!PathAppendW(pingFilePath, L"" MOZ_APP_BASENAME)) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ if (!PathAppendW(pingFilePath, L"Pending Pings")) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ if (!PathAppendW(pingFilePath, uuid.c_str())) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return std::wstring(pingFilePath);
+}
+
+// Sends Firefox Desktop telemetry ping. Note: this is sent in parallel to Glean
+// telemetry.
+static mozilla::WindowsError SendDesktopTelemetryPing(
+ const std::string defaultBrowser, const std::string previousDefaultBrowser,
+ const std::string defaultPdf, const std::string osVersion,
+ const std::string prevOSVersion, const std::string osLocale,
+ const std::string notificationType, const std::string notificationShown,
+ const std::string notificationAction,
+ const std::string prevNotificationAction) {
+ // Fill in the ping JSON object.
+ Json::Value ping;
+ ping["build_channel"] = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL);
+ ping["build_version"] = MOZILLA_VERSION;
+ ping["default_browser"] = defaultBrowser;
+ ping["previous_default_browser"] = previousDefaultBrowser;
+ ping["default_pdf_viewer_raw"] = defaultPdf;
+ ping["os_version"] = osVersion;
+ ping["previous_os_version"] = prevOSVersion;
+ ping["os_locale"] = osLocale;
+ ping["notification_type"] = notificationType;
+ ping["notification_shown"] = notificationShown;
+ ping["notification_action"] = notificationAction;
+ ping["previous_notification_action"] = prevNotificationAction;
+
+ // Stringify the JSON.
+ Json::StreamWriterBuilder jsonStream;
+ jsonStream["indentation"] = "";
+ std::string pingStr = Json::writeString(jsonStream, ping);
+
+ // Generate a UUID for the ping.
+ FilePathResult uuidResult = GenerateUUIDStr();
+ if (uuidResult.isErr()) {
+ return uuidResult.unwrapErr();
+ }
+ std::wstring uuid = uuidResult.unwrap();
+
+ // Write the JSON string to a file. Use the UUID in the file name so that if
+ // multiple instances of this task are running they'll have their own files.
+ FilePathResult pingFilePathResult = GetPingFilePath(uuid);
+ if (pingFilePathResult.isErr()) {
+ return pingFilePathResult.unwrapErr();
+ }
+ std::wstring pingFilePath = pingFilePathResult.unwrap();
+
+ {
+ std::ofstream outFile(pingFilePath);
+ outFile << pingStr;
+ if (outFile.fail()) {
+ // We have no way to get a specific error code out of a file stream
+ // other than to catch an exception, so substitute a generic error code.
+ HRESULT hr = HRESULT_FROM_WIN32(ERROR_IO_DEVICE);
+ LOG_ERROR(hr);
+ return mozilla::WindowsError::FromHResult(hr);
+ }
+ }
+
+ // Hand the file off to pingsender to submit.
+ FilePathResult pingsenderPathResult =
+ GetRelativeBinaryPath(L"pingsender.exe");
+ if (pingsenderPathResult.isErr()) {
+ return pingsenderPathResult.unwrapErr();
+ }
+ std::wstring pingsenderPath = pingsenderPathResult.unwrap();
+
+ std::wstring url(L"" TELEMETRY_PING_URL);
+ url.append(uuid);
+
+ const wchar_t* pingsenderArgs[] = {pingsenderPath.c_str(), url.c_str(),
+ pingFilePath.c_str()};
+ mozilla::UniquePtr<wchar_t[]> pingsenderCmdLine(
+ mozilla::MakeCommandLine(mozilla::ArrayLength(pingsenderArgs),
+ const_cast<wchar_t**>(pingsenderArgs)));
+
+ PROCESS_INFORMATION pi;
+ STARTUPINFOW si = {sizeof(si)};
+ si.dwFlags = STARTF_USESHOWWINDOW;
+ si.wShowWindow = SW_HIDE;
+ if (!::CreateProcessW(pingsenderPath.c_str(), pingsenderCmdLine.get(),
+ nullptr, nullptr, FALSE,
+ DETACHED_PROCESS | NORMAL_PRIORITY_CLASS, nullptr,
+ nullptr, &si, &pi)) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return mozilla::WindowsError::FromHResult(hr);
+ }
+
+ CloseHandle(pi.hThread);
+ CloseHandle(pi.hProcess);
+
+ return mozilla::WindowsError::CreateSuccess();
+}
+
+// This function checks if a ping has already been sent today. If one has not,
+// it assumes that we are about to send one and sets a registry entry that will
+// cause this function to return true for the next day.
+// This function uses unprefixed registry entries, so a RegistryMutex should be
+// held before calling.
+static BoolResult GetPingAlreadySentToday() {
+ const wchar_t* valueName = L"LastPingSentAt";
+ MaybeQwordResult readResult =
+ RegistryGetValueQword(IsPrefixed::Unprefixed, valueName);
+ if (readResult.isErr()) {
+ HRESULT hr = readResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr);
+ return BoolResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ mozilla::Maybe<ULONGLONG> maybeValue = readResult.unwrap();
+ ULONGLONG now = GetCurrentTimestamp();
+ if (maybeValue.isSome()) {
+ ULONGLONG lastPingTime = maybeValue.value();
+ if (SecondsPassedSince(lastPingTime, now) < MINIMUM_PING_PERIOD_SEC) {
+ return true;
+ }
+ }
+
+ mozilla::WindowsErrorResult<mozilla::Ok> writeResult =
+ RegistrySetValueQword(IsPrefixed::Unprefixed, valueName, now);
+ if (writeResult.isErr()) {
+ HRESULT hr = readResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr);
+ return BoolResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ return false;
+}
+
+// This both retrieves a value from the registry and writes new data
+// (currentDefault) to the same value. If there is no value stored, the value
+// passed for prevDefault will be converted to a string and returned instead.
+//
+// Although we already store and retrieve a cached previous default browser
+// value elsewhere, it may be updated when we don't send a ping. The value we
+// retrieve here will only be updated when we are sending a ping to ensure
+// that pings don't miss a default browser transition.
+static TelemetryFieldResult GetAndUpdatePreviousDefaultBrowser(
+ const std::string& currentDefault, Browser prevDefault) {
+ const wchar_t* registryValueName = L"PingCurrentDefault";
+
+ MaybeStringResult readResult =
+ RegistryGetValueString(IsPrefixed::Unprefixed, registryValueName);
+ if (readResult.isErr()) {
+ HRESULT hr = readResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ mozilla::Maybe<std::string> maybeValue = readResult.unwrap();
+ std::string oldCurrentDefault;
+ if (maybeValue.isSome()) {
+ oldCurrentDefault = maybeValue.value();
+ } else {
+ oldCurrentDefault = GetStringForBrowser(prevDefault);
+ }
+
+ mozilla::WindowsErrorResult<mozilla::Ok> writeResult = RegistrySetValueString(
+ IsPrefixed::Unprefixed, registryValueName, currentDefault.c_str());
+ if (writeResult.isErr()) {
+ HRESULT hr = writeResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ return oldCurrentDefault;
+}
+
+// This both retrieves a value from the registry and writes new data
+// (`currentOSVersion`) to the same value. If there is no value stored,
+// `currentOSVersion` is returned instead.
+//
+// The value we retrieve here will only be updated when we are sending a ping to
+// ensure that pings don't miss a Windows OS version transition.
+static TelemetryFieldResult GetAndUpdatePreviousOSVersion(
+ const std::string& currentOSVersion) {
+ const wchar_t* registryValueName = L"PingCurrentOSVersion";
+
+ MaybeStringResult readResult =
+ RegistryGetValueString(IsPrefixed::Unprefixed, registryValueName);
+ if (readResult.isErr()) {
+ HRESULT hr = readResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ mozilla::Maybe<std::string> maybeValue = readResult.unwrap();
+ std::string oldOSVersion = maybeValue.valueOr(currentOSVersion);
+
+ mozilla::WindowsErrorResult<mozilla::Ok> writeResult = RegistrySetValueString(
+ IsPrefixed::Unprefixed, registryValueName, currentOSVersion.c_str());
+ if (writeResult.isErr()) {
+ HRESULT hr = writeResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr);
+ return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr));
+ }
+ return oldOSVersion;
+}
+
+// If notifications actions occurred, we want to make sure a ping gets sent for
+// them. If we aren't sending a ping right now, we want to cache the ping values
+// for the next time the ping is sent.
+// The values passed will only be cached if actions were actually taken
+// (i.e. not when notificationShown == "not-shown")
+HRESULT MaybeCache(Cache& cache, const std::string& notificationType,
+ const std::string& notificationShown,
+ const std::string& notificationAction,
+ const std::string& prevNotificationAction) {
+ std::string notShown =
+ GetStringForNotificationShown(NotificationShown::NotShown);
+ if (notificationShown == notShown) {
+ return S_OK;
+ }
+
+ Cache::Entry entry{
+ .notificationType = notificationType,
+ .notificationShown = notificationShown,
+ .notificationAction = notificationAction,
+ .prevNotificationAction = prevNotificationAction,
+ };
+ VoidResult result = cache.Enqueue(entry);
+ if (result.isErr()) {
+ return result.unwrapErr().AsHResult();
+ }
+ return S_OK;
+}
+
+// This function retrieves values cached by MaybeCache. If any values were
+// loaded from the cache, the values passed in to this function are passed to
+// MaybeCache so that they are not lost. If there are no values in the cache,
+// the values passed will not be changed.
+// Values retrieved from the cache will also be removed from it.
+HRESULT MaybeSwapForCached(Cache& cache, std::string& notificationType,
+ std::string& notificationShown,
+ std::string& notificationAction,
+ std::string& prevNotificationAction) {
+ Cache::MaybeEntryResult result = cache.Dequeue();
+ if (result.isErr()) {
+ HRESULT hr = result.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Failed to read cache: %#X", hr);
+ return hr;
+ }
+ Cache::MaybeEntry maybeEntry = result.unwrap();
+ if (maybeEntry.isNothing()) {
+ return S_OK;
+ }
+
+ MaybeCache(cache, notificationType, notificationShown, notificationAction,
+ prevNotificationAction);
+ notificationType = maybeEntry.value().notificationType;
+ notificationShown = maybeEntry.value().notificationShown;
+ notificationAction = maybeEntry.value().notificationAction;
+ if (maybeEntry.value().prevNotificationAction.isSome()) {
+ prevNotificationAction = maybeEntry.value().prevNotificationAction.value();
+ } else {
+ prevNotificationAction =
+ GetStringForNotificationAction(NotificationAction::NoAction);
+ }
+ return S_OK;
+}
+
+HRESULT ReadPreviousNotificationAction(std::string& prevAction) {
+ MaybeStringResult maybePrevActionResult = RegistryGetValueString(
+ IsPrefixed::Unprefixed, PREV_NOTIFICATION_ACTION_REG_NAME);
+ if (maybePrevActionResult.isErr()) {
+ HRESULT hr = maybePrevActionResult.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to read prev action from registry: %#X", hr);
+ return hr;
+ }
+ mozilla::Maybe<std::string> maybePrevAction = maybePrevActionResult.unwrap();
+ if (maybePrevAction.isNothing()) {
+ prevAction = GetStringForNotificationAction(NotificationAction::NoAction);
+ } else {
+ prevAction = maybePrevAction.value();
+ // There's no good reason why there should be an invalid value stored here.
+ // But it's also not worth aborting the whole ping over. This function will
+ // silently change it to "no-action" if the value isn't valid to prevent us
+ // from sending unexpected telemetry values.
+ EnsureValidNotificationAction(prevAction);
+ }
+ return S_OK;
+}
+
+// Writes the previous notification action to the registry, but only if a
+// notification was shown.
+HRESULT MaybeWritePreviousNotificationAction(
+ const NotificationActivities& activitiesPerformed) {
+ if (activitiesPerformed.shown != NotificationShown::Shown) {
+ return S_OK;
+ }
+ std::string notificationAction =
+ GetStringForNotificationAction(activitiesPerformed.action);
+ mozilla::WindowsErrorResult<mozilla::Ok> result = RegistrySetValueString(
+ IsPrefixed::Unprefixed, PREV_NOTIFICATION_ACTION_REG_NAME,
+ notificationAction.c_str());
+ if (result.isErr()) {
+ HRESULT hr = result.unwrapErr().AsHResult();
+ LOG_ERROR_MESSAGE(L"Unable to write prev action to registry: %#X", hr);
+ return hr;
+ }
+ return S_OK;
+}
+
+// Sends Firefox Desktop and Glean telemetry for the Default Agent in parallel.
+HRESULT SendDefaultAgentPing(
+ const DefaultBrowserInfo& browserInfo, const DefaultPdfInfo& pdfInfo,
+ const NotificationActivities& activitiesPerformed) {
+ std::string currentDefaultBrowser =
+ GetStringForBrowser(browserInfo.currentDefaultBrowser);
+ std::string currentDefaultPdf =
+ GetStringForPDFHandler(pdfInfo.currentDefaultPdf);
+ std::string notificationType =
+ GetStringForNotificationType(activitiesPerformed.type);
+ std::string notificationShown =
+ GetStringForNotificationShown(activitiesPerformed.shown);
+ std::string notificationAction =
+ GetStringForNotificationAction(activitiesPerformed.action);
+
+ TelemetryFieldResult osVersionResult = GetOSVersion();
+ if (osVersionResult.isErr()) {
+ return osVersionResult.unwrapErr().AsHResult();
+ }
+ std::string osVersion = osVersionResult.unwrap();
+
+ TelemetryFieldResult osLocaleResult = GetOSLocale();
+ if (osLocaleResult.isErr()) {
+ return osLocaleResult.unwrapErr().AsHResult();
+ }
+ std::string osLocale = osLocaleResult.unwrap();
+
+ std::string prevNotificationAction;
+ HRESULT hr = ReadPreviousNotificationAction(prevNotificationAction);
+ if (FAILED(hr)) {
+ return hr;
+ }
+ // Intentionally discard the result of this write. There's no real reason
+ // to abort sending the ping in the error case and it already wrote an error
+ // message. So there isn't really anything to do at this point.
+ MaybeWritePreviousNotificationAction(activitiesPerformed);
+
+ Cache cache;
+
+ // Do not send the ping if we are not an official telemetry-enabled build;
+ // don't even generate the ping in fact, because if we write the file out
+ // then some other build might find it later and decide to submit it.
+ if (!IsOfficialTelemetry() || IsTelemetryDisabled()) {
+ return MaybeCache(cache, notificationType, notificationShown,
+ notificationAction, prevNotificationAction);
+ }
+
+ // Glean notification pings are handled asynchronously from system defaults
+ // pings; caching is unnecessary as we need not adhere to the system default
+ // ping's 24 hour cadence.
+ if (activitiesPerformed.shown != NotificationShown::NotShown) {
+ mozilla::glean::notification::show_success.Set(activitiesPerformed.shown ==
+ NotificationShown::Shown);
+ if (activitiesPerformed.shown == NotificationShown::Shown) {
+ mozilla::glean::notification::action.Set(
+ nsDependentCString(notificationAction.c_str()));
+ }
+ }
+
+ // Pings are limited to one per day (across all installations), so check if we
+ // already sent one today.
+ // This will also set a registry entry indicating that the last ping was
+ // just sent, to prevent another one from being sent today. We'll do this
+ // now even though we haven't sent the ping yet. After this check, we send
+ // a ping unconditionally. The only exception is for errors, and any error
+ // that we get now will probably be hit every time.
+ // Because unsent pings attempted with pingsender can get automatically
+ // re-sent later, we don't even want to try again on transient network
+ // failures.
+ hr = [&]() {
+ BoolResult pingAlreadySentResult = GetPingAlreadySentToday();
+ if (pingAlreadySentResult.isErr()) {
+ return pingAlreadySentResult.unwrapErr().AsHResult();
+ }
+ bool pingAlreadySent = pingAlreadySentResult.unwrap();
+ if (pingAlreadySent) {
+ return MaybeCache(cache, notificationType, notificationShown,
+ notificationAction, prevNotificationAction);
+ }
+
+ hr = MaybeSwapForCached(cache, notificationType, notificationShown,
+ notificationAction, prevNotificationAction);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ // Don't update the registry's default browser data until we are sure we
+ // want to send a ping. Otherwise it could be updated to reflect a ping we
+ // never sent. Same logic for witnessing Windows updates, but they're less
+ // valuable, so try (and potentially fail) those first.
+ TelemetryFieldResult previousOSVersionResult =
+ GetAndUpdatePreviousOSVersion(osVersion);
+ if (previousOSVersionResult.isErr()) {
+ return previousOSVersionResult.unwrapErr().AsHResult();
+ }
+ std::string prevOSVersion = previousOSVersionResult.unwrap();
+
+ mozilla::glean::system::os_version.Set(
+ nsDependentCString(osVersion.c_str()));
+ mozilla::glean::system::previous_os_version.Set(
+ nsDependentCString(prevOSVersion.c_str()));
+
+ TelemetryFieldResult previousDefaultBrowserResult =
+ GetAndUpdatePreviousDefaultBrowser(currentDefaultBrowser,
+ browserInfo.previousDefaultBrowser);
+ if (previousDefaultBrowserResult.isErr()) {
+ return previousDefaultBrowserResult.unwrapErr().AsHResult();
+ }
+ std::string previousDefaultBrowser = previousDefaultBrowserResult.unwrap();
+
+ mozilla::glean::system_default::browser.Set(
+ nsDependentCString(currentDefaultBrowser.c_str()));
+ // Glean telemetry doesn't use registry cached ping values for
+ // notifications, so we shouldn't use the registry cached values for the
+ // previous default browser either.
+ std::string uncachedPreviousDefaultBrowser =
+ GetStringForBrowser(browserInfo.previousDefaultBrowser);
+ mozilla::glean::system_default::previous_browser.Set(
+ nsDependentCString(uncachedPreviousDefaultBrowser.c_str()));
+ mozilla::glean::system_default::pdf_handler.Set(
+ nsDependentCString(currentDefaultPdf.c_str()));
+
+ return SendDesktopTelemetryPing(
+ currentDefaultBrowser, previousDefaultBrowser, currentDefaultPdf,
+ osVersion, prevOSVersion, osLocale, notificationType,
+ notificationShown, notificationAction, prevNotificationAction)
+ .AsHResult();
+ }();
+
+ mozilla::glean_pings::DefaultAgent.Submit("daily_ping"_ns);
+
+ return hr;
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/Telemetry.h b/toolkit/mozapps/defaultagent/Telemetry.h
new file mode 100644
index 0000000000..028746b6f1
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/Telemetry.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_TELEMETRY_H__
+#define __DEFAULT_BROWSER_TELEMETRY_H__
+
+#include <windows.h>
+
+#include "DefaultBrowser.h"
+#include "DefaultPDF.h"
+#include "Notification.h"
+
+namespace mozilla::default_agent {
+
+HRESULT SendDefaultAgentPing(const DefaultBrowserInfo& browserInfo,
+ const DefaultPdfInfo& pdfInfo,
+ const NotificationActivities& activitiesPerformed);
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_TELEMETRY_H__
diff --git a/toolkit/mozapps/defaultagent/UtfConvert.cpp b/toolkit/mozapps/defaultagent/UtfConvert.cpp
new file mode 100644
index 0000000000..2259a2db6f
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/UtfConvert.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "UtfConvert.h"
+
+#include <string>
+
+#include "EventLog.h"
+
+#include "mozilla/Buffer.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+namespace mozilla::default_agent {
+
+Utf16ToUtf8Result Utf16ToUtf8(const wchar_t* const utf16) {
+ int utf8Len =
+ WideCharToMultiByte(CP_UTF8, 0, utf16, -1, nullptr, 0, nullptr, nullptr);
+ if (utf8Len == 0) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return Utf16ToUtf8Result(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ mozilla::Buffer<char> utf8(utf8Len);
+ int bytesWritten = WideCharToMultiByte(CP_UTF8, 0, utf16, -1, utf8.Elements(),
+ utf8.Length(), nullptr, nullptr);
+ if (bytesWritten == 0) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return Utf16ToUtf8Result(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return std::string(utf8.Elements());
+}
+
+Utf8ToUtf16Result Utf8ToUtf16(const char* const utf8) {
+ int utf16Len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0);
+ if (utf16Len == 0) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ mozilla::Buffer<wchar_t> utf16(utf16Len);
+ int charsWritten = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, utf16.Elements(),
+ utf16.Length());
+ if (charsWritten == 0) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return mozilla::Err(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return std::wstring(utf16.Elements());
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/UtfConvert.h b/toolkit/mozapps/defaultagent/UtfConvert.h
new file mode 100644
index 0000000000..8cfbb9ad0b
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/UtfConvert.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef DEFAULT_BROWSER_UTF_CONVERT_H__
+#define DEFAULT_BROWSER_UTF_CONVERT_H__
+
+#include <string>
+
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+namespace mozilla::default_agent {
+
+using Utf16ToUtf8Result = mozilla::WindowsErrorResult<std::string>;
+using Utf8ToUtf16Result = mozilla::WindowsErrorResult<std::wstring>;
+
+Utf16ToUtf8Result Utf16ToUtf8(const wchar_t* const utf16);
+Utf8ToUtf16Result Utf8ToUtf16(const char* const utf8);
+
+} // namespace mozilla::default_agent
+
+#endif // DEFAULT_BROWSER_UTF_CONVERT_H__
diff --git a/toolkit/mozapps/defaultagent/WindowsMutex.cpp b/toolkit/mozapps/defaultagent/WindowsMutex.cpp
new file mode 100644
index 0000000000..804c3e6d75
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/WindowsMutex.cpp
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "mozilla/Logging.h"
+
+#include "WindowsMutex.h"
+
+namespace mozilla::default_agent {
+
+using mozilla::LogLevel;
+
+static LazyLogModule gWindowsMutexLog("WindowsMutex");
+
+NS_IMPL_ISUPPORTS(WindowsMutexFactory, nsIWindowsMutexFactory)
+
+NS_IMETHODIMP
+WindowsMutexFactory::CreateMutex(const nsAString& aName,
+ nsIWindowsMutex** aWindowsMutex) {
+ nsAutoHandle mutex;
+ auto name = PromiseFlatString(aName);
+
+ mutex.own(CreateMutexW(nullptr, FALSE, name.get()));
+ if (mutex.get() == nullptr) {
+ MOZ_LOG(gWindowsMutexLog, LogLevel::Error,
+ ("Couldn't open mutex \"%s\": %#lX",
+ NS_ConvertUTF16toUTF8(name).get(), GetLastError()));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<WindowsMutex> nsMutex = new WindowsMutex(name, mutex);
+ nsMutex.forget(aWindowsMutex);
+ return NS_OK;
+}
+
+WindowsMutex::WindowsMutex(const nsString& aName, nsAutoHandle& aMutex)
+ : mName(NS_ConvertUTF16toUTF8(aName)), mLocked(false) {
+ mMutex.steal(aMutex);
+}
+
+WindowsMutex::~WindowsMutex() {
+ Unlock();
+ // nsAutoHandle will take care of closing the mutex's handle.
+}
+
+NS_IMPL_ISUPPORTS(WindowsMutex, nsIWindowsMutex)
+
+NS_IMETHODIMP
+WindowsMutex::TryLock() {
+ // This object may be used on the main thread, so don't wait if it's
+ // not signaled.
+ DWORD mutexStatus = WaitForSingleObject(mMutex.get(), 0);
+ if (mutexStatus == WAIT_OBJECT_0) {
+ mLocked = true;
+ } else if (mutexStatus == WAIT_TIMEOUT) {
+ MOZ_LOG(gWindowsMutexLog, LogLevel::Warning,
+ ("Timed out waiting for mutex \"%s\"", mName.get()));
+ } else if (mutexStatus == WAIT_ABANDONED) {
+ // This status code means that we are supposed to check our data for
+ // consistency as the last locking process didn't signal intentional
+ // unlocking which might indicate it crashed mid-operation. Current uses of
+ // this `WindowsMutex` don't need to worry about corruption of the locked
+ // object, if needed the `nsIWindowsMutex` interface should be extended.
+ MOZ_LOG(gWindowsMutexLog, LogLevel::Warning,
+ ("Found abandoned mutex \"%s\". Continuing...", mName.get()));
+ mLocked = true;
+ } else {
+ // The only other documented status code is WAIT_FAILED. In the case that
+ // we somehow get some other code, that is also an error.
+ MOZ_LOG(gWindowsMutexLog, LogLevel::Error,
+ ("Failed to wait on mutex: mName: %s, error %#lX", mName.get(),
+ GetLastError()));
+ }
+ return mLocked ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+WindowsMutex::IsLocked(bool* aLocked) {
+ *aLocked = mLocked;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WindowsMutex::Unlock() {
+ nsresult rv = NS_OK;
+
+ if (mLocked) {
+ BOOL success = ReleaseMutex(mMutex.get());
+ if (!success) {
+ MOZ_LOG(gWindowsMutexLog, LogLevel::Error,
+ ("Failed to release mutex \"%s\"", mName.get()));
+ rv = NS_ERROR_UNEXPECTED;
+ }
+
+ mLocked = false;
+ }
+
+ return rv;
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/WindowsMutex.h b/toolkit/mozapps/defaultagent/WindowsMutex.h
new file mode 100644
index 0000000000..5e8b32314c
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/WindowsMutex.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_MUTEX_H__
+#define __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_MUTEX_H__
+
+#include "nsString.h"
+#include "nsWindowsHelpers.h"
+
+#include "nsIWindowsMutex.h"
+
+namespace mozilla::default_agent {
+
+class WindowsMutexFactory final : public nsIWindowsMutexFactory {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWINDOWSMUTEXFACTORY
+
+ WindowsMutexFactory() = default;
+
+ private:
+ ~WindowsMutexFactory() = default;
+};
+
+class WindowsMutex final : public nsIWindowsMutex {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWINDOWSMUTEX
+
+ WindowsMutex(const nsString& aName, nsAutoHandle& aMutex);
+
+ private:
+ nsAutoHandle mMutex;
+ nsCString mName;
+ bool mLocked;
+
+ ~WindowsMutex();
+};
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_MUTEX_H__
diff --git a/toolkit/mozapps/defaultagent/common.cpp b/toolkit/mozapps/defaultagent/common.cpp
new file mode 100644
index 0000000000..0e660d1207
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/common.cpp
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include "common.h"
+
+#include "EventLog.h"
+
+#include <windows.h>
+
+namespace mozilla::default_agent {
+
+ULONGLONG GetCurrentTimestamp() {
+ FILETIME filetime;
+ GetSystemTimeAsFileTime(&filetime);
+ ULARGE_INTEGER integerTime;
+ integerTime.u.LowPart = filetime.dwLowDateTime;
+ integerTime.u.HighPart = filetime.dwHighDateTime;
+ return integerTime.QuadPart;
+}
+
+// Passing a zero as the second argument (or omitting it) causes the function
+// to get the current time rather than using a passed value.
+ULONGLONG SecondsPassedSince(ULONGLONG initialTime,
+ ULONGLONG currentTime /* = 0 */) {
+ if (currentTime == 0) {
+ currentTime = GetCurrentTimestamp();
+ }
+ // Since this is returning an unsigned value, let's make sure we don't try to
+ // return anything negative
+ if (initialTime >= currentTime) {
+ return 0;
+ }
+
+ // These timestamps are expressed in 100-nanosecond intervals
+ return (currentTime - initialTime) / 10 // To microseconds
+ / 1000 // To milliseconds
+ / 1000; // To seconds
+}
+
+FilePathResult GenerateUUIDStr() {
+ UUID uuid;
+ RPC_STATUS status = UuidCreate(&uuid);
+ if (status != RPC_S_OK) {
+ HRESULT hr = MAKE_HRESULT(1, FACILITY_RPC, status);
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ // 39 == length of a UUID string including braces and NUL.
+ wchar_t guidBuf[39] = {};
+ if (StringFromGUID2(uuid, guidBuf, 39) != 39) {
+ LOG_ERROR(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER));
+ return FilePathResult(
+ mozilla::WindowsError::FromWin32Error(ERROR_INSUFFICIENT_BUFFER));
+ }
+
+ // Remove the curly braces.
+ return std::wstring(guidBuf + 1, guidBuf + 37);
+}
+
+FilePathResult GetRelativeBinaryPath(const wchar_t* suffix) {
+ // The Path* functions don't set LastError, but this is the only thing that
+ // can really cause them to fail, so if they ever do we assume this is why.
+ HRESULT hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
+
+ mozilla::UniquePtr<wchar_t[]> thisBinaryPath = mozilla::GetFullBinaryPath();
+ if (!PathRemoveFileSpecW(thisBinaryPath.get())) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ wchar_t relativePath[MAX_PATH] = L"";
+
+ if (!PathCombineW(relativePath, thisBinaryPath.get(), suffix)) {
+ LOG_ERROR(hr);
+ return FilePathResult(mozilla::WindowsError::FromHResult(hr));
+ }
+
+ return std::wstring(relativePath);
+}
+
+} // namespace mozilla::default_agent
diff --git a/toolkit/mozapps/defaultagent/common.h b/toolkit/mozapps/defaultagent/common.h
new file mode 100644
index 0000000000..ddd0ca6a67
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/common.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#ifndef __DEFAULT_BROWSER_AGENT_COMMON_H__
+#define __DEFAULT_BROWSER_AGENT_COMMON_H__
+
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+#define AGENT_REGKEY_NAME \
+ L"SOFTWARE\\" MOZ_APP_VENDOR "\\" MOZ_APP_BASENAME "\\Default Browser Agent"
+
+namespace mozilla::default_agent {
+
+ULONGLONG GetCurrentTimestamp();
+// Passing a zero as the second argument (or omitting it) causes the function
+// to get the current time rather than using a passed value.
+ULONGLONG SecondsPassedSince(ULONGLONG initialTime, ULONGLONG currentTime = 0);
+
+using FilePathResult = mozilla::WindowsErrorResult<std::wstring>;
+FilePathResult GenerateUUIDStr();
+
+FilePathResult GetRelativeBinaryPath(const wchar_t* suffix);
+
+} // namespace mozilla::default_agent
+
+#endif // __DEFAULT_BROWSER_AGENT_COMMON_H__
diff --git a/toolkit/mozapps/defaultagent/components.conf b/toolkit/mozapps/defaultagent/components.conf
new file mode 100644
index 0000000000..62b5055d56
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/components.conf
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+if buildconfig.substs['CC_TYPE'] in ('msvc', 'clang-cl'):
+ Classes = [
+ {
+ 'cid': '{edc38cb5-b6f6-4aeb-bd45-7be8e00fc364}',
+ 'contract_ids': ['@mozilla.org/default-agent;1'],
+ 'type': 'mozilla::default_agent::DefaultAgent',
+ 'headers': ['mozilla/DefaultAgent.h'],
+ },
+ {
+ 'cid': '{d54fe2b7-438f-4629-9706-1acda5b51088}',
+ 'contract_ids': ['@mozilla.org/windows-mutex-factory;1'],
+ 'type': 'mozilla::default_agent::WindowsMutexFactory',
+ 'headers': ['mozilla/WindowsMutex.h'],
+ },
+ ]
diff --git a/toolkit/mozapps/defaultagent/defaultagent.ini b/toolkit/mozapps/defaultagent/defaultagent.ini
new file mode 100644
index 0000000000..9300b20c46
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/defaultagent.ini
@@ -0,0 +1,9 @@
+; 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/.
+
+; This file is in the UTF-8 encoding
+[Nonlocalized]
+InitialToastRelativeImagePath=browser/VisualElements/VisualElements_150.png
+FollowupToastRelativeImagePath=browser/VisualElements/VisualElements_150.png
+LocalizedToastRelativeImagePath=browser/VisualElements/VisualElements_150.png
diff --git a/toolkit/mozapps/defaultagent/docs/index.rst b/toolkit/mozapps/defaultagent/docs/index.rst
new file mode 100644
index 0000000000..f977f234cf
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/docs/index.rst
@@ -0,0 +1,49 @@
+=====================
+Default Browser Agent
+=====================
+
+The Default Browser Agent is a Windows-only scheduled task which runs in the background to collect and submit data about the browser that the user has set as their OS default (that is, the browser that will be invoked by the operating system to open web links that the user clicks on in other programs). Its purpose is to help Mozilla understand user's default browser choices and, in the future, to engage with users at a time when they may not be actively running Firefox.
+
+For information about the specific data that the agent sends, see :doc:`the ping documentation </toolkit/components/telemetry/data/default-browser-ping>`.
+
+
+Scheduled Task
+==============
+
+The agent runs as a `Windows scheduled task <https://docs.microsoft.com/en-us/windows/win32/taskschd/about-the-task-scheduler>`_. The scheduled task proxy executable invokes the Firefox ``BackgroundTask_defaultagent`` which executes all of the agent's primary functions; all of its other functions relate to managing the task. The Windows installer is responsible for creating (and the uninstaller for removing) the agent's task entry, but the code for actually doing this resides in the agent itself, and the installers simply call it using dedicated command line parameters (``register-task`` and ``uninstall``). The :doc:`PostUpdate </browser/installer/windows/installer/Helper>` code also calls the agent to update any properties of an existing task registration that need to be updated, or to create one during an application update if none exists.
+
+The tasks are normal entries in the Windows Task Scheduler, managed using `its Win32 API <https://docs.microsoft.com/en-us/windows/win32/api/_taskschd/>`_. They're created in a tasks folder called "Mozilla" (or whatever the application's vendor name is), and there's one for each installation of Firefox (or other Mozilla application). The task is set to run automatically every 24 hours starting at the time it's registered (with the first run being 24 hours after that), or the nearest time after that the computer is awake. The task is configured with one action, which is to run the agent binary with the command line parameter ``do-task``, the command that invokes the actual agent functionality.
+
+The default browser agent needs to run as some OS-level user, as opposed to, say, ``LOCAL SERVICE``, in order to read the user's default browser setting. Therefore, the default browser agent runs as the user that ran the Firefox installer (although always without elevation, whether the installer had it or not).
+
+
+Remote Disablement
+------------------
+
+The default browser agent can be remotely disabled and (re-)enabled. Each time the scheduled task runs it queries `Firefox Remote Settings <https://remote-settings.readthedocs.io/en/latest/>`_ to determine if the agent has been remotely disabled or (re-)enabled.
+
+If the default browser agent is disabled by policy, remote disablement will not be checked. However, the notification functionality of the agent is distinct from the telemetry functionality of the agent, and remote disablement must apply to both functions. Therefore, even if the user has opted out of sending telemetry (by policy or by preference), the agent must check for remote disablement. For a user who is currently opted out of telemetry, they will not be opted in due to the default browser agent being remotely (re-)enabled.
+
+
+Data Management
+===============
+
+The default browser agent has to be able to work with settings at several different levels: a Firefox profile, an OS user, a Firefox installation, and the entire system. This need creates an information architecture mismatch between all of those things, mostly because no Firefox profile is available to the agent while it's running; it's not really feasible to either directly use or to clone Firefox's profile selection functionality, and even if we could select a profile, whatever code we might use to actually work with it would have the same problems. So, in order to allow for controlling the agent from Firefox, certain settings are mirrored from Firefox to a location where the agent can read them. Since the agent operates in the context only of an OS-level user, that means that in this situation a single OS-level user who uses multiple Firefox profiles may be able to observe the agent's settings changing as the different profiles race to be the active mirror, without them knowingly taking any action.
+
+
+Pref Reflection
+---------------
+
+The agent needs to be able to read (but not set) values that have their canonical representation in the form of Firefox prefs. This means those pref values have to be copied out to a place where the agent can read them. The Windows registry was chosen as that place; it's easier to use than a file, and we already have keys there which are reserved by Firefox. Specifically, the subkey used for these prefs is ``HKEY_CURRENT_USER\Software\[app vendor name]\[app name]\Default Browser Agent\``. During Firefox startup, the values of the prefs that control the agent are reflected to this key, and those values are updated whenever the prefs change after that.
+
+The list of reflected prefs includes the global telemetry opt-out pref ``datareporting.healthreport.uploadEnabled`` and a pref called ``default-browser-agent.enabled``, which can enable or disable the entire agent. The agent checks these registry-reflected pref values when its scheduled task runs, they do not actually prevent the scheduled task from running.
+
+Enterprise policies also exist to perform the same functions as these prefs. These work the same way as all other Firefox policies and `the documentation for those <https://mozilla.github.io/policy-templates/>`_ explains how to use them.
+
+In addition, the following Firefox Remote Settings pref is reflected: ``services.settings.server``. It is the service endpoint to consult for remote-disablement.
+
+
+Default Browser Setting
+-----------------------
+
+The agent is responsible for reporting both the user's current default browser and their previous default browser. Nothing in the operating system records past associations, so the agent must do this for itself. First, it gets the current default browser by calling `IApplicationAssociationRegistration::QueryCurrentDefault <https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-iapplicationassociationregistration-querycurrentdefault>`_ for the ``http`` protocol. It then checks that against a value stored in its own registry key and, if those are different, it knows that the default browser has changed, and records the new and old defaults.
diff --git a/toolkit/mozapps/defaultagent/metrics.yaml b/toolkit/mozapps/defaultagent/metrics.yaml
new file mode 100644
index 0000000000..355a80d7e6
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/metrics.yaml
@@ -0,0 +1,208 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Toolkit :: Default Browser Agent'
+
+system:
+ os_version:
+ type: string
+ description: >
+ The current Windows OS version, usually as a dotted quad ("x.y.z.w") with
+ Windows Update Build Revision (UBR), but potentially as a dotted triple
+ ("x.y.z") without UBR.
+ lifetime: application
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1850149
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1850149
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - install-update@mozilla.com
+ expires: never
+ send_in_pings:
+ - default-agent
+
+ previous_os_version:
+ type: string
+ description: >
+ The Windows OS version before it was changed to the current setting. The
+ possible values are the same as for `system.os_version`.
+
+ The OS does not keep track of the previous OS version, so the agent
+ records this information itself. That means that it will be inaccurate
+ until the first time the default is changed after the agent task begins
+ running. Before then, the value of `previous_os_version` will be the same
+ as `os_version`.
+
+ This value is updated every time the Default Agent runs, so when the
+ default browser is first changed the values for `os_version` and
+ `previous_os_version` will be different. But on subsequent executions of
+ the Default Agent, the two values will be the same.
+ lifetime: application
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1850149
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1850149
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - install-update@mozilla.com
+ expires: never
+ send_in_pings:
+ - default-agent
+
+system_default:
+ browser:
+ type: string
+ description: >
+ Which browser is currently set as the system default web browser. This is
+ simply a string with the name of the browser binned to a fixed set of
+ known browsers.
+
+ Possible values currently include the following (from
+ [DefaultBrowser.cpp](https://searchfox.org/mozilla-central/source/toolkit/mozapps/defaultagent/DefaultBrowser.cpp)):
+ * "error"
+ * "" (unknown)
+ * "firefox"
+ * "chrome"
+ * "edge"
+ * "edge-chrome"
+ * "ie"
+ * "opera"
+ * "brave"
+ * "yandex"
+ * "qq-browser"
+ * "360-browser"
+ * "sogou"
+ * "duckduckgo"
+ lifetime: application
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - install-update@mozilla.com
+ expires: never
+ send_in_pings:
+ - default-agent
+
+ previous_browser:
+ type: string
+ description: >
+ Which browser was set as the system default before it was changed to the
+ current setting. The possible values are the same as for
+ `system_default.browser`.
+
+ The OS does not keep track of previous default settings, so the agent
+ records this information itself. That means that it will be inaccurate
+ until the first time the default is changed after the agent task begins
+ running. Before then, the value of `previous_browser` will be the same
+ as `browser`.
+
+ This value is updated every time the Default Agent runs, so when the
+ default browser is first changed the values for `browser` and
+ `previous_browser` will be different. But on subsequent executions of
+ the Default Agent, the two values will be the same.
+ lifetime: application
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - install-update@mozilla.com
+ expires: never
+ send_in_pings:
+ - default-agent
+
+ pdf_handler:
+ type: string
+ description: >
+ Which pdf handler is currently set as the system default handler. This is
+ simply a string with the name of the handler binned to a fixed set of
+ known handlers.
+
+ Possible values currently include the following (from
+ [DefaultPDF.cpp](https://searchfox.org/mozilla-central/source/toolkit/mozapps/defaultagent/DefaultPDF.cpp)):
+ * "Error"
+ * "" (unknown)
+ * "Firefox"
+ * "Microsoft Edge"
+ * "Google Chrome"
+ * "Adobe Acrobat"
+ * "WPS"
+ * "Nitro"
+ * "Foxit"
+ * "PDF-XChange"
+ * "Avast"
+ * "Sumatra"
+ lifetime: application
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1756900
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1756900
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - install-update@mozilla.com
+ expires: never
+ send_in_pings:
+ - default-agent
+
+notification:
+ show_success:
+ type: boolean
+ description: >
+ Whether a notification was shown or not. Possible value include "shown" and "error".
+ notification_emails:
+ - install-update@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293
+ expires: never
+ send_in_pings:
+ - default-agent
+
+ action:
+ type: string
+ description: >
+ The action that the user took in response to the notification. Possible
+ values currently include the following:
+ * “dismissed-by-timeout”
+ * “dismissed-to-action-center”
+ * “dismissed-by-button”
+ * “dismissed-by-application-hidden”
+ * “make-firefox-default-button”
+ * “toast-clicked”
+
+ Many of the values correspond to buttons on the notification and should be
+ pretty self explanatory, but a few are less so.
+ * “dismissed-to-action-center” will be used if the user clicks the arrow in
+ the top right corner of the notification to dismiss it to the
+ action center.
+ * “dismissed-by-application-hidden” is provided because that is a method of
+ dismissal that the notification API could give but, in practice, should
+ never be seen.
+ * “dismissed-by-timeout” indicates that the user did not interact with the
+ notification and it timed out.
+ notification_emails:
+ - install-update@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293
+ expires: never
+ send_in_pings:
+ - default-agent
diff --git a/toolkit/mozapps/defaultagent/module.ver b/toolkit/mozapps/defaultagent/module.ver
new file mode 100644
index 0000000000..92b692b62c
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/module.ver
@@ -0,0 +1 @@
+WIN32_MODULE_DESCRIPTION=@MOZ_APP_DISPLAYNAME@ Default Browser Agent
diff --git a/toolkit/mozapps/defaultagent/moz.build b/toolkit/mozapps/defaultagent/moz.build
new file mode 100644
index 0000000000..86b68c6371
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/moz.build
@@ -0,0 +1,113 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+SPHINX_TREES["default-browser-agent"] = "docs"
+
+DIRS += ["proxy"]
+
+UNIFIED_SOURCES += [
+ "Cache.cpp",
+ "common.cpp",
+ "DefaultAgent.cpp",
+ "DefaultBrowser.cpp",
+ "DefaultPDF.cpp",
+ "EventLog.cpp",
+ "Policy.cpp",
+ "Registry.cpp",
+ "ScheduledTask.cpp",
+ "ScheduledTaskRemove.cpp",
+ "SetDefaultBrowser.cpp",
+ "Telemetry.cpp",
+ "UtfConvert.cpp",
+ "WindowsMutex.cpp",
+]
+
+SOURCES += [
+ "/third_party/WinToast/wintoastlib.cpp",
+ "/toolkit/mozapps/update/common/readstrings.cpp",
+ "Notification.cpp",
+]
+
+# Suppress warnings from third-party code.
+SOURCES["/third_party/WinToast/wintoastlib.cpp"].flags += [
+ "-Wno-implicit-fallthrough",
+ "-Wno-nonportable-include-path", # Needed for wintoastlib.h including "Windows.h"
+]
+SOURCES["Notification.cpp"].flags += [
+ "-Wno-nonportable-include-path", # Needed for wintoastlib.h including "Windows.h"
+]
+
+EXPORTS.mozilla += [
+ "DefaultAgent.h",
+ "WindowsMutex.h",
+]
+
+USE_LIBS += [
+ "jsoncpp",
+]
+
+LOCAL_INCLUDES += [
+ "/browser/components/shell/",
+ "/other-licenses/nsis/Contrib/CityHash/cityhash",
+ "/third_party/WinToast",
+ "/toolkit/components/jsoncpp/include",
+ "/toolkit/mozapps/update/common",
+]
+
+OS_LIBS += [
+ "advapi32",
+ "bcrypt",
+ "comsupp",
+ "crypt32",
+ "kernel32",
+ "netapi32",
+ "ole32",
+ "oleaut32",
+ "rpcrt4",
+ "shell32",
+ "shlwapi",
+ "taskschd",
+ "userenv",
+ "wininet",
+ "ws2_32",
+ "ntdll",
+]
+
+XPIDL_SOURCES += [
+ "nsIDefaultAgent.idl",
+ "nsIWindowsMutex.idl",
+]
+
+XPIDL_MODULE = "default-agent"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+
+# If defines are added to this list that are required by the Cache,
+# SetDefaultBrowser, or their dependencies (Registry, EventLog, common),
+# tests/gtest/moz.build will need to be updated as well.
+for var in ("MOZ_APP_BASENAME", "MOZ_APP_DISPLAYNAME", "MOZ_APP_VENDOR"):
+ DEFINES[var] = '"%s"' % CONFIG[var]
+
+DEFINES["UNICODE"] = True
+DEFINES["_UNICODE"] = True
+
+FINAL_TARGET_FILES += ["defaultagent.ini"]
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["tests/gtest"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Default Browser Agent")
+
+EXTRA_JS_MODULES.backgroundtasks += [
+ "BackgroundTask_defaultagent.sys.mjs",
+]
diff --git a/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl b/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl
new file mode 100644
index 0000000000..7e78e1b30d
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl
@@ -0,0 +1,167 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+ #include "nsISupports.idl"
+
+[scriptable, uuid(edc38cb5-b6f6-4aeb-bd45-7be8e00fc364)]
+interface nsIDefaultAgent : nsISupports
+{
+ /**
+ * Create a Windows scheduled task that will launch this binary with the
+ * do-task command every 24 hours, starting from 24 hours after register-task
+ * is run.
+ *
+ * @param {AString} aUniqueToken
+ * A unique identifier for this installation; typically the install path
+ * hash that's used for the update directory, the AppUserModelID, and
+ * other related purposes.
+ */
+ void registerTask(in AString aUniqueToken);
+
+ /**
+ * Update an existing task registration, without changing its schedule. This
+ * should be called during updates of the application, in case this program
+ * has been updated and any of the task parameters have changed.
+ *
+ * @param {AString} aUniqueToken
+ * A unique identifier for this installation; the same one provided when
+ * the task was registered.
+ */
+ void updateTask(in AString aUniqueToken);
+
+ /**
+ * Removes the previously created task. The unique token argument is required
+ * and should be the same one that was passed in when the task was registered.
+ *
+ * @param {AString} aUniqueToken
+ * A unique identifier for this installation; the same one provided when
+ * the task was registered.
+ */
+ void unregisterTask(in AString aUniqueToken);
+
+ /**
+ * Removes the previously created task, and also removes all registry entries
+ * running the task may have created.
+ *
+ * @param {AString} aUniqueToken
+ * A unique identifier for this installation; the same one provided when
+ * the task was registered.
+ */
+ void uninstall(in AString aUniqueToken);
+
+ /**
+ * Actually performs the default agent task, which currently means generating
+ * and sending our telemetry ping and possibly showing a notification to the
+ * user if their browser has switched from Firefox to Edge with Blink.
+ *
+ * @param {AString} aUniqueToken
+ * A unique identifier for this installation; the same one provided when
+ * the task was registered.
+ * @param {boolean} aForce
+ * For debugging, forces the task to run even if it has run in the last
+ * 24 hours, and forces the notification to show.
+ */
+ void doTask(in AString aUniqueToken, in boolean aForce);
+
+ /**
+ * Checks that the main app ran recently.
+ *
+ * @return {boolean} true if the app ran recently.
+ */
+ boolean appRanRecently();
+
+ /**
+ * Returns a string for the default browser if known, binned to known browsers.
+ *
+ * @return {AString}
+ * The current default browser.
+ */
+ AString getDefaultBrowser();
+
+ /**
+ * Gets and replaces the previously found default browser from the registry.
+ *
+ * @param {AString} aCurrentBrowser
+ * The current known browser to save to the registry.
+ * @return {AString}
+ * The previous known browser from the registry.
+ */
+ AString getReplacePreviousDefaultBrowser(in AString aCurrentBrowser);
+
+ /**
+ * Returns a string for the default PDF handler if known, binned to known
+ * PDF handlers.
+ *
+ * @return {AString}
+ * The previous default PDF handler.
+ */
+ AString getDefaultPdfHandler();
+
+ /**
+ * Sends a Default Agent telemetry ping.
+ *
+ * @param {AString} aCurrentBrowser
+ * The current known browser.
+ * @param {AString} aPreviousBrowser
+ * The previous known browser.
+ * @param {AString} aPdfHandler
+ * The current known PDF handler.
+ * @param {AString} aNotificationShown
+ * If the notification was or wasn't shown. See
+ * `toolkit/mozapps/defaultagent/Notification.h` for valid values.
+ * @param {AString} aNotificationAction
+ * The notification action taken by the user. See
+ * `toolkit/mozapps/defaultagent/Notification.h` for valid values.
+ *
+ */
+ void sendPing(in AString aCurrentBrowser, in AString aPreviousBrowser, in AString aPdfHandler, in AString aNotificationShown, in AString aNotificationAction);
+
+ /**
+ * Set the default browser and optionally additional file extensions via the
+ * UserChoice registry keys.
+ *
+ * @param {AString} aAumid
+ * Suffix to be appended to ProgIDs when registering system defaults.
+ * @param {Array<AString>} aExtraFileExtensions
+ * Additional optional file extensions to register specified as argument
+ * pairs: the first element is the file extension, the second element is
+ * the root of a ProgID, which will be suffixed with `-{aAumid}`.
+ */
+ void setDefaultBrowserUserChoice(in AString aAumid, in Array<AString> aExtraFileExtensions);
+
+ /**
+ * Set the default browser and optionally additional file extensions via the
+ * UserChoice registry keys, asynchronously. Does the actual work on a
+ * background thread.
+ *
+ * @param {AString} aAumid
+ * Suffix to be appended to ProgIDs when registering system defaults.
+ * @param {Array<AString>} aExtraFileExtensions
+ * Additional optional file extensions to register specified as argument
+ * pairs: the first element is the file extension, the second element is
+ * the root of a ProgID, which will be suffixed with `-{aAumid}`.
+ */
+ [implicit_jscontext]
+ Promise setDefaultBrowserUserChoiceAsync(in AString aAumid, in Array<AString> aExtraFileExtensions);
+
+ /**
+ * Sets file extensions via the UserChoice registry keys.
+ *
+ * @param {AString} aAumid
+ * Suffix to be appended to ProgIDs when registering system defaults.
+ * @param {Array<AString>} aExtraFileExtensions
+ * File extensions to register specified as argument pairs: the first
+ * element is the file extension, the second element is the root of a
+ * ProgID, which will be suffixed with `-{aAumid}`.
+ */
+ void setDefaultExtensionHandlersUserChoice(in AString aAumid, in Array<AString> aFileExtensions);
+
+ /**
+ * Checks if the default agent has been disabled.
+ *
+ * @return {boolean} true if the default agent is disabled.
+ */
+ boolean agentDisabled();
+};
diff --git a/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl b/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl
new file mode 100644
index 0000000000..69090aa764
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl
@@ -0,0 +1,62 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+ #include "nsISupports.idl"
+
+/**
+* Interact with Windows named mutexes.
+*
+* Generally you don't want a Windows named mutex, you want one of the many Gecko
+* locking primitives. But when you do want cross-application instance or even
+* cross-installation coordination, a Windows named mutex might be an
+* appropriate tool.
+*/
+[scriptable, uuid(26f09999-c26e-4b72-8747-5adaefa0914c)]
+interface nsIWindowsMutex : nsISupports
+{
+ /**
+ * Locks the mutex.
+ *
+ * Note that this will not block waiting to lock. It attempts to lock the mutex
+ * and if it can't immediately, NS_ERROR_NOT_AVAILABLE will be thrown.
+ *
+ * This function succeeds when an abandoned mutex is found, therefore is
+ * inappropriate for use if an abandoned mutex might imply the locked resource
+ * is in a corrupt state.
+ *
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * If unable to lock the mutex.
+ */
+ void tryLock();
+
+ /**
+ * Returns whether the mutex is locked.
+ *
+ * @return {boolean} true if locked, false if unlocked.
+ */
+ bool isLocked();
+
+ /**
+ * Unlocks the mutex.
+ * @throws NS_ERROR_UNEXPECTED
+ * If unable to release mutex.
+ */
+ void unlock();
+};
+
+[scriptable, uuid(d54fe2b7-438f-4629-9706-1acda5b51088)]
+interface nsIWindowsMutexFactory : nsISupports {
+ /**
+ * Creates a Windows named mutex.
+ *
+ * @param {AString} aName
+ * The system-wide name of the mutex.
+ * @return {nsIWindowsMutex}
+ * The created Windows mutex.
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * If unable to create mutex.
+ */
+ nsIWindowsMutex createMutex(in AString aName);
+};
diff --git a/toolkit/mozapps/defaultagent/pings.yaml b/toolkit/mozapps/defaultagent/pings.yaml
new file mode 100644
index 0000000000..5e607f8c8f
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/pings.yaml
@@ -0,0 +1,42 @@
+# 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/.
+
+---
+$schema: moz://mozilla.org/schemas/glean/pings/2-0-0
+
+default-agent:
+ description: >
+ This opt-out ping is sent from the Default Agent, which is a Windows-only
+ Firefox Background Task that is registered during Firefox installation with
+ the Windows scheduled tasks system so that it runs automatically every 24
+ hours, whether Firefox is running or not.
+
+ Opting out of telemetry is handled via the pref value being copied to the
+ registry so that the Default Agent can read it without needing to work with
+ profiles. Relevant policies are consulted as well. The agent also has its own
+ pref, `default-agent.enabled`, which if set to false disables all agent
+ functionality, including generating this ping.
+
+ Each installation of Firefox has its own copy of the agent and its own
+ scheduled task which shares a common `LastPingSentAt` user registry key with
+ other installations. Installations race to send a single ping per 24 hour
+ window per installing user. If multiple operating system-level users are all
+ using one copy of Firefox, only one scheduled task will have been created and
+ only one ping will be sent, even though the users might have different
+ default browser settings. If multiple users have installed Firefox then each
+ installing user will have a scheduled task and ping.
+
+ Additional information for the Default Agent can be found in the
+ [Default Browser Agent docs](https://firefox-source-docs.mozilla.org/toolkit/mozapps/defaultagent/default-browser-agent/index.html).
+ include_client_id: false
+ send_if_empty: false
+ reasons:
+ daily_ping: |
+ The ping was sent as part of the daily scheduled Default Agent run.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293
+ notification_emails:
+ - install-update@mozilla.com
diff --git a/toolkit/mozapps/defaultagent/proxy/Makefile.in b/toolkit/mozapps/defaultagent/proxy/Makefile.in
new file mode 100644
index 0000000000..dadff2846b
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/proxy/Makefile.in
@@ -0,0 +1,16 @@
+# 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/.
+
+# This binary should never open a console window in release builds, because
+# it's going to run in the background when the user may not expect it, and
+# we don't want a console window to just appear out of nowhere on them.
+# For debug builds though, it's okay to use the existing MOZ_WINCONSOLE value.
+ifndef MOZ_DEBUG
+MOZ_WINCONSOLE = 0
+endif
+
+# Rebuild if the resources or manifest change.
+EXTRA_DEPS += $(srcdir)/default-browser-agent.exe.manifest
+
+include $(topsrcdir)/config/rules.mk
diff --git a/toolkit/mozapps/defaultagent/proxy/default-browser-agent.exe.manifest b/toolkit/mozapps/defaultagent/proxy/default-browser-agent.exe.manifest
new file mode 100644
index 0000000000..ceb2839697
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/proxy/default-browser-agent.exe.manifest
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+<assemblyIdentity
+ version="1.0.0.0"
+ processorArchitecture="*"
+ name="DefaultBrowserAgent"
+ type="win32"
+/>
+<description>Default Browser Agent</description>
+<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
+ <ms_asmv3:security>
+ <ms_asmv3:requestedPrivileges>
+ <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
+ </ms_asmv3:requestedPrivileges>
+ </ms_asmv3:security>
+</ms_asmv3:trustInfo>
+<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+ </application>
+</compatibility>
+<ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
+ <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
+ <dpiAware>True/PM</dpiAware>
+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
+ </ms_asmv3:windowsSettings>
+</ms_asmv3:application>
+</assembly>
diff --git a/toolkit/mozapps/defaultagent/proxy/main.cpp b/toolkit/mozapps/defaultagent/proxy/main.cpp
new file mode 100644
index 0000000000..53efdcb9ae
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/proxy/main.cpp
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+
+#include <windows.h>
+#include <shlwapi.h>
+#include <objbase.h>
+#include <string.h>
+#include <filesystem>
+
+#include "../ScheduledTask.h"
+#include "../ScheduledTaskRemove.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+
+using namespace mozilla::default_agent;
+
+// See BackgroundTask_defaultagent.sys.mjs for arguments.
+int wmain(int argc, wchar_t** argv) {
+ // Firefox deescalates process permissions, so handle task unscheduling step
+ // here instead of the Firefox Background Tasks to ensure cleanup for other
+ // users. See Bug 1710143.
+ if (!wcscmp(argv[1], L"uninstall")) {
+ if (argc < 3 || !argv[2]) {
+ return E_INVALIDARG;
+ }
+
+ HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ RemoveTasks(argv[2], WhichTasks::AllTasksForInstallation);
+
+ CoUninitialize();
+
+ // Background Task handles remainder of uninstall.
+ }
+
+ std::vector<wchar_t> path(MAX_PATH, 0);
+ DWORD charsWritten = GetModuleFileNameW(nullptr, path.data(), path.size());
+
+ // GetModuleFileNameW returns the count of characters written including null
+ // when truncated, excluding null otherwise. Therefore the count will always
+ // be less than the buffer size when not truncated.
+ while (charsWritten == path.size()) {
+ path.resize(path.size() * 2, 0);
+ charsWritten = GetModuleFileNameW(nullptr, path.data(), path.size());
+ }
+
+ if (charsWritten == 0) {
+ return E_UNEXPECTED;
+ }
+
+ std::filesystem::path programPath = path.data();
+ programPath = programPath.parent_path();
+ programPath += L"\\" MOZ_APP_NAME L".exe";
+
+ std::vector<const wchar_t*> childArgv;
+ childArgv.push_back(programPath.c_str());
+ childArgv.push_back(L"--backgroundtask");
+ childArgv.push_back(L"defaultagent");
+ // Skip argv[0], path to this exectuable.
+ for (int i = 1; i < argc; i++) {
+ childArgv.push_back(argv[i]);
+ }
+
+ auto cmdLine = mozilla::MakeCommandLine(childArgv.size(), childArgv.data());
+
+ STARTUPINFOW si = {};
+ si.cb = sizeof(STARTUPINFOW);
+ PROCESS_INFORMATION pi = {};
+
+ // Runs `{program path} --backgoundtask defaultagent`.
+ CreateProcessW(programPath.c_str(), cmdLine.get(), nullptr, nullptr, false,
+ DETACHED_PROCESS | NORMAL_PRIORITY_CLASS, nullptr, nullptr,
+ &si, &pi);
+
+ // Wait until process exists so uninstalling doesn't interrupt the background
+ // task cleaning registry entries.
+ DWORD exitCode;
+ if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_OBJECT_0 &&
+ ::GetExitCodeProcess(pi.hProcess, &exitCode)) {
+ // Match EXIT_CODE in BackgroundTasksManager.sys.mjs and
+ // BackgroundTask_defaultagent.sys.mjs.
+ enum EXIT_CODE {
+ SUCCESS = 0,
+ NOT_FOUND = 2,
+ EXCEPTION = 3,
+ TIMEOUT = 4,
+ DISABLED_BY_POLICY = 11,
+ INVALID_ARGUMENT = 12,
+ MUTEX_NOT_LOCKABLE = 13,
+ };
+
+ switch (exitCode) {
+ case SUCCESS:
+ return S_OK;
+ case NOT_FOUND:
+ return E_UNEXPECTED;
+ case EXCEPTION:
+ return E_FAIL;
+ case TIMEOUT:
+ return E_FAIL;
+ case DISABLED_BY_POLICY:
+ return HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY);
+ case INVALID_ARGUMENT:
+ return E_INVALIDARG;
+ case MUTEX_NOT_LOCKABLE:
+ return HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION);
+ default:
+ return E_UNEXPECTED;
+ }
+ }
+
+ return E_UNEXPECTED;
+}
diff --git a/toolkit/mozapps/defaultagent/proxy/moz.build b/toolkit/mozapps/defaultagent/proxy/moz.build
new file mode 100644
index 0000000000..40d7655dfa
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/proxy/moz.build
@@ -0,0 +1,68 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Program("default-browser-agent")
+
+SPHINX_TREES["default-browser-agent"] = "docs"
+
+UNIFIED_SOURCES += [
+ "../EventLog.cpp",
+ "../ScheduledTaskRemove.cpp",
+ "main.cpp",
+]
+
+SOURCES += [
+ "/browser/components/shell/WindowsDefaultBrowser.cpp",
+ "/other-licenses/nsis/Contrib/CityHash/cityhash/city.cpp",
+ "/toolkit/mozapps/update/common/readstrings.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "../",
+ "/browser/components/shell/",
+ "/mfbt/",
+ "/other-licenses/nsis/Contrib/CityHash/cityhash",
+ "/toolkit/mozapps/update/common/",
+]
+
+OS_LIBS += [
+ "advapi32",
+ "comsupp",
+ "netapi32",
+ "ole32",
+ "oleaut32",
+ "shell32",
+ "shlwapi",
+ "taskschd",
+]
+
+DEFINES["NS_NO_XPCOM"] = True
+DEFINES["IMPL_MFBT"] = True
+
+DEFINES["UNICODE"] = True
+DEFINES["_UNICODE"] = True
+
+for var in (
+ "MOZ_APP_NAME",
+ "MOZ_APP_DISPLAYNAME",
+ "MOZ_APP_VENDOR",
+ "MOZ_APP_BASENAME",
+):
+ DEFINES[var] = '"%s"' % CONFIG[var]
+
+# We need STL headers that aren't allowed when wrapping is on (at least
+# <filesystem>, and possibly others).
+DisableStlWrapping()
+
+# We need this to be able to use wmain as the entry point on MinGW;
+# otherwise it will try to use WinMain.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ WIN32_EXE_LDFLAGS += ["-ENTRY:wmainCRTStartup"]
+else:
+ WIN32_EXE_LDFLAGS += ["-municode"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Default Browser Agent")
diff --git a/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp b/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp
new file mode 100644
index 0000000000..892be6b2f7
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp
@@ -0,0 +1,301 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+#include "gtest/gtest.h"
+
+#include <string>
+
+#include "Cache.h"
+#include "common.h"
+#include "Registry.h"
+#include "UtfConvert.h"
+
+#include "mozilla/Result.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+using namespace mozilla::default_agent;
+
+class WDBACacheTest : public ::testing::Test {
+ protected:
+ std::wstring mCacheRegKey;
+
+ void SetUp() override {
+ // Create a unique registry key to put the cache in for each test.
+ const ::testing::TestInfo* const testInfo =
+ ::testing::UnitTest::GetInstance()->current_test_info();
+ Utf8ToUtf16Result testCaseResult = Utf8ToUtf16(testInfo->test_case_name());
+ ASSERT_TRUE(testCaseResult.isOk());
+ mCacheRegKey = testCaseResult.unwrap();
+
+ Utf8ToUtf16Result testNameResult = Utf8ToUtf16(testInfo->name());
+ ASSERT_TRUE(testNameResult.isOk());
+ mCacheRegKey += L'.';
+ mCacheRegKey += testNameResult.unwrap();
+
+ FilePathResult uuidResult = GenerateUUIDStr();
+ ASSERT_TRUE(uuidResult.isOk());
+ mCacheRegKey += L'.';
+ mCacheRegKey += uuidResult.unwrap();
+ }
+
+ void TearDown() override {
+ // It seems like the TearDown probably doesn't run if SetUp doesn't
+ // succeed, but I can't find any documentation saying that. And we don't
+ // want to accidentally clobber the entirety of AGENT_REGKEY_NAME.
+ if (!mCacheRegKey.empty()) {
+ std::wstring regKey = AGENT_REGKEY_NAME;
+ regKey += L'\\';
+ regKey += mCacheRegKey;
+ RegDeleteTreeW(HKEY_CURRENT_USER, regKey.c_str());
+ }
+ }
+};
+
+TEST_F(WDBACacheTest, BasicFunctionality) {
+ Cache cache(mCacheRegKey.c_str());
+ VoidResult result = cache.Init();
+ ASSERT_TRUE(result.isOk());
+
+ // Test that the cache starts empty
+ Cache::MaybeEntryResult entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ Cache::MaybeEntry entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isNothing());
+
+ // Test that the cache stops accepting items when it is full.
+ ASSERT_EQ(Cache::kDefaultCapacity, 2U);
+ Cache::Entry toWrite = Cache::Entry{
+ .notificationType = "string1",
+ .notificationShown = "string2",
+ .notificationAction = "string3",
+ .prevNotificationAction = "string4",
+ };
+ result = cache.Enqueue(toWrite);
+ ASSERT_TRUE(result.isOk());
+ toWrite = Cache::Entry{
+ .notificationType = "string5",
+ .notificationShown = "string6",
+ .notificationAction = "string7",
+ .prevNotificationAction = "string8",
+ };
+ result = cache.Enqueue(toWrite);
+ ASSERT_TRUE(result.isOk());
+ toWrite = Cache::Entry{
+ .notificationType = "string9",
+ .notificationShown = "string10",
+ .notificationAction = "string11",
+ .prevNotificationAction = "string12",
+ };
+ result = cache.Enqueue(toWrite);
+ ASSERT_TRUE(result.isErr());
+
+ // Read the two cache entries back out and test that they match the expected
+ // values.
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isSome());
+ ASSERT_EQ(entry.value().entryVersion, 2U);
+ ASSERT_EQ(entry.value().notificationType, "string1");
+ ASSERT_EQ(entry.value().notificationShown, "string2");
+ ASSERT_EQ(entry.value().notificationAction, "string3");
+ ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
+ ASSERT_EQ(entry.value().prevNotificationAction.value(), "string4");
+
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isSome());
+ ASSERT_EQ(entry.value().entryVersion, 2U);
+ ASSERT_EQ(entry.value().notificationType, "string5");
+ ASSERT_EQ(entry.value().notificationShown, "string6");
+ ASSERT_EQ(entry.value().notificationAction, "string7");
+ ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
+ ASSERT_EQ(entry.value().prevNotificationAction.value(), "string8");
+
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isNothing());
+}
+
+TEST_F(WDBACacheTest, Version1Migration) {
+ // Set up 2 version 1 cache entries
+ VoidResult result = RegistrySetValueString(
+ IsPrefixed::Unprefixed, L"PingCacheNotificationType0", "string1");
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ L"PingCacheNotificationShown0", "string2");
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ L"PingCacheNotificationAction0", "string3");
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ L"PingCacheNotificationType1", "string4");
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ L"PingCacheNotificationShown1", "string5");
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ L"PingCacheNotificationAction1", "string6");
+ ASSERT_TRUE(result.isOk());
+
+ Cache cache(mCacheRegKey.c_str());
+ result = cache.Init();
+ ASSERT_TRUE(result.isOk());
+
+ Cache::MaybeEntryResult entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ Cache::MaybeEntry entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isSome());
+ ASSERT_EQ(entry.value().entryVersion, 1U);
+ ASSERT_EQ(entry.value().notificationType, "string1");
+ ASSERT_EQ(entry.value().notificationShown, "string2");
+ ASSERT_EQ(entry.value().notificationAction, "string3");
+ ASSERT_TRUE(entry.value().prevNotificationAction.isNothing());
+
+ // Insert a new item to test coexistence of different versions
+ Cache::Entry toWrite = Cache::Entry{
+ .notificationType = "string7",
+ .notificationShown = "string8",
+ .notificationAction = "string9",
+ .prevNotificationAction = "string10",
+ };
+ result = cache.Enqueue(toWrite);
+ ASSERT_TRUE(result.isOk());
+
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isSome());
+ ASSERT_EQ(entry.value().entryVersion, 1U);
+ ASSERT_EQ(entry.value().notificationType, "string4");
+ ASSERT_EQ(entry.value().notificationShown, "string5");
+ ASSERT_EQ(entry.value().notificationAction, "string6");
+ ASSERT_TRUE(entry.value().prevNotificationAction.isNothing());
+
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isSome());
+ ASSERT_EQ(entry.value().entryVersion, 2U);
+ ASSERT_EQ(entry.value().notificationType, "string7");
+ ASSERT_EQ(entry.value().notificationShown, "string8");
+ ASSERT_EQ(entry.value().notificationAction, "string9");
+ ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
+ ASSERT_EQ(entry.value().prevNotificationAction.value(), "string10");
+
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isNothing());
+}
+
+TEST_F(WDBACacheTest, ForwardsCompatibility) {
+ // Set up a cache that might have been made by a future version with a larger
+ // capacity set and more keys per entry.
+ std::wstring settingsKey = mCacheRegKey + L"\\version2";
+ VoidResult result = RegistrySetValueDword(
+ IsPrefixed::Unprefixed, Cache::kCapacityRegName, 8, settingsKey.c_str());
+ ASSERT_TRUE(result.isOk());
+ // We're going to insert the future version's entry at index 6 so there's
+ // space for 1 more before we loop back to index 0. Then we are going to
+ // enqueue 2 new values to test that this works properly.
+ result = RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kFrontRegName,
+ 6, settingsKey.c_str());
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kSizeRegName, 1,
+ settingsKey.c_str());
+ ASSERT_TRUE(result.isOk());
+
+ // Insert an entry as if it was inserted by a future version
+ std::wstring entryRegKey = settingsKey + L"\\6";
+ result =
+ RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kEntryVersionKey,
+ 9999, entryRegKey.c_str());
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ Cache::kNotificationTypeKey, "string1",
+ entryRegKey.c_str());
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ Cache::kNotificationShownKey, "string2",
+ entryRegKey.c_str());
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ Cache::kNotificationActionKey, "string3",
+ entryRegKey.c_str());
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed,
+ Cache::kPrevNotificationActionKey, "string4",
+ entryRegKey.c_str());
+ ASSERT_TRUE(result.isOk());
+ result = RegistrySetValueString(IsPrefixed::Unprefixed, L"UnknownFutureKey",
+ "string5", entryRegKey.c_str());
+ ASSERT_TRUE(result.isOk());
+
+ Cache cache(mCacheRegKey.c_str());
+ result = cache.Init();
+ ASSERT_TRUE(result.isOk());
+
+ // Insert 2 new items to test that these features work with a different
+ // capacity.
+ Cache::Entry toWrite = Cache::Entry{
+ .notificationType = "string6",
+ .notificationShown = "string7",
+ .notificationAction = "string8",
+ .prevNotificationAction = "string9",
+ };
+ result = cache.Enqueue(toWrite);
+ ASSERT_TRUE(result.isOk());
+ toWrite = Cache::Entry{
+ .notificationType = "string10",
+ .notificationShown = "string11",
+ .notificationAction = "string12",
+ .prevNotificationAction = "string13",
+ };
+ result = cache.Enqueue(toWrite);
+ ASSERT_TRUE(result.isOk());
+
+ // Read cache and verify the output
+ Cache::MaybeEntryResult entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ Cache::MaybeEntry entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isSome());
+ ASSERT_EQ(entry.value().entryVersion, 9999U);
+ ASSERT_EQ(entry.value().notificationType, "string1");
+ ASSERT_EQ(entry.value().notificationShown, "string2");
+ ASSERT_EQ(entry.value().notificationAction, "string3");
+ ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
+ ASSERT_EQ(entry.value().prevNotificationAction.value(), "string4");
+
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isSome());
+ ASSERT_EQ(entry.value().entryVersion, 2U);
+ ASSERT_EQ(entry.value().notificationType, "string6");
+ ASSERT_EQ(entry.value().notificationShown, "string7");
+ ASSERT_EQ(entry.value().notificationAction, "string8");
+ ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
+ ASSERT_EQ(entry.value().prevNotificationAction.value(), "string9");
+
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isSome());
+ ASSERT_EQ(entry.value().entryVersion, 2U);
+ ASSERT_EQ(entry.value().notificationType, "string10");
+ ASSERT_EQ(entry.value().notificationShown, "string11");
+ ASSERT_EQ(entry.value().notificationAction, "string12");
+ ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
+ ASSERT_EQ(entry.value().prevNotificationAction.value(), "string13");
+
+ entryResult = cache.Dequeue();
+ ASSERT_TRUE(entryResult.isOk());
+ entry = entryResult.unwrap();
+ ASSERT_TRUE(entry.isNothing());
+}
diff --git a/toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp b/toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp
new file mode 100644
index 0000000000..7c491184d9
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/. */
+#include "gtest/gtest.h"
+
+#include <windows.h>
+#include "mozilla/UniquePtr.h"
+#include "WindowsUserChoice.h"
+
+#include "SetDefaultBrowser.h"
+
+using namespace mozilla::default_agent;
+
+TEST(SetDefaultBrowserUserChoice, Hash)
+{
+ // Hashes set by System Settings on 64-bit Windows 10 Pro 20H2 (19042.928).
+ const wchar_t* sid = L"S-1-5-21-636376821-3290315252-1794850287-1001";
+
+ // length mod 8 = 0
+ EXPECT_STREQ(
+ GenerateUserChoiceHash(L"https", sid, L"FirefoxURL-308046B0AF4A39CB",
+ (SYSTEMTIME){2021, 4, 1, 19, 23, 7, 56, 506})
+ .get(),
+ L"uzpIsMVyZ1g=");
+
+ // length mod 8 = 2 (confirm that the incomplete last block is dropped)
+ EXPECT_STREQ(
+ GenerateUserChoiceHash(L".html", sid, L"FirefoxHTML-308046B0AF4A39CB",
+ (SYSTEMTIME){2021, 4, 1, 19, 23, 7, 56, 519})
+ .get(),
+ L"7fjRtUPASlc=");
+
+ // length mod 8 = 4
+ EXPECT_STREQ(
+ GenerateUserChoiceHash(L"https", sid, L"MSEdgeHTM",
+ (SYSTEMTIME){2021, 4, 1, 19, 23, 3, 48, 119})
+ .get(),
+ L"Fz0kA3Ymmps=");
+
+ // length mod 8 = 6
+ EXPECT_STREQ(
+ GenerateUserChoiceHash(L".html", sid, L"ChromeHTML",
+ (SYSTEMTIME){2021, 4, 1, 19, 23, 6, 3, 628})
+ .get(),
+ L"R5TD9LGJ5Xw=");
+
+ // non-ASCII
+ EXPECT_STREQ(
+ GenerateUserChoiceHash(L".html", sid, L"FirefoxHTML-ÀBÇDË😀†",
+ (SYSTEMTIME){2021, 4, 2, 20, 0, 38, 55, 101})
+ .get(),
+ L"F3NsK3uNv5E=");
+}
diff --git a/toolkit/mozapps/defaultagent/tests/gtest/moz.build b/toolkit/mozapps/defaultagent/tests/gtest/moz.build
new file mode 100644
index 0000000000..07fa68228c
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/tests/gtest/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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 https://mozilla.org/MPL/2.0/.
+
+Library("DefaultAgentTest")
+
+UNIFIED_SOURCES += [
+ "CacheTest.cpp",
+ "SetDefaultBrowserTest.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/browser/components/shell/",
+ "/toolkit/mozapps/defaultagent",
+]
+
+OS_LIBS += [
+ "advapi32",
+ "bcrypt",
+ "crypt32",
+ "kernel32",
+ "rpcrt4",
+]
+
+DEFINES["UNICODE"] = True
+DEFINES["_UNICODE"] = True
+
+for var in ("MOZ_APP_BASENAME", "MOZ_APP_DISPLAYNAME", "MOZ_APP_VENDOR"):
+ DEFINES[var] = '"%s"' % CONFIG[var]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/mozapps/defaultagent/tests/xpcshell/test_windows_mutex.js b/toolkit/mozapps/defaultagent/tests/xpcshell/test_windows_mutex.js
new file mode 100644
index 0000000000..ac020ae9f2
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/tests/xpcshell/test_windows_mutex.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Multiple instances of a named mutex on Windows can lock on the same thread, so
+// we have to run each test across at least two distinct threads. Running on a
+// separate process achieves the same.
+do_load_child_test_harness();
+
+let parentFactory = Cc["@mozilla.org/windows-mutex-factory;1"].createInstance(
+ Ci.nsIWindowsMutexFactory
+);
+
+function promiseCommand(aCommand) {
+ // Exceptions don't propogate to the process that called `sendCommand` nor
+ // tigger a test failure, so wrap the command to ensure we fail appropriately.
+ let wrappedCommand = `try {${aCommand}} catch(e) {Assert.ok(false, "Error running command received in child process. Note the passed in function must be self-contained. Error: \${e.toString()}");}`;
+ return new Promise(resolve => sendCommand(wrappedCommand, resolve));
+}
+
+// This is passed as a string to a child process, thus must be self-contained.
+function assertLockOkOnChild(aName, aTestString) {
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ Assert.ok(false, `${assertLockOkOnChild.name} run on child process.`);
+ }
+
+ let childFactory = Cc["@mozilla.org/windows-mutex-factory;1"].createInstance(
+ Ci.nsIWindowsMutexFactory
+ );
+
+ let lockingMutex = childFactory.createMutex(aName);
+
+ info(`Locking mutex for subtest "${aTestString}"`);
+ lockingMutex.tryLock();
+ try {
+ Assert.ok(lockingMutex.isLocked(), aTestString);
+ } finally {
+ lockingMutex.unlock();
+ }
+}
+
+// This is passed as a string to a child process, thus must be self-contained.
+function assertLockThrowsOnChild(aName, aTestString) {
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+ Assert.ok(false, `${assertLockThrowsOnChild.name} run on child process.`);
+ }
+
+ let childFactory = Cc["@mozilla.org/windows-mutex-factory;1"].createInstance(
+ Ci.nsIWindowsMutexFactory
+ );
+
+ let blockedMutex = childFactory.createMutex(aName);
+
+ info(`Locking mutex for subtest "${aTestString}"`);
+ Assert.throws(blockedMutex.tryLock, /NS_ERROR_NOT_AVAILABLE/, aTestString);
+ Assert.ok(!blockedMutex.isLocked(), "Not locked after error.");
+}
+
+add_task(async function test_lock_blocks() {
+ const kTestMutexName = Services.uuid.generateUUID().toString();
+ let lockingMutex = parentFactory.createMutex(kTestMutexName);
+
+ Assert.ok(!lockingMutex.isLocked(), "Reported unlocked before locking.");
+
+ info(`Locking mutex named "${kTestMutexName}"`);
+ lockingMutex.tryLock();
+ try {
+ Assert.ok(lockingMutex.isLocked(), "Reported locked after locking.");
+
+ await promiseCommand(
+ `(${assertLockThrowsOnChild.toString()})("${kTestMutexName}", "Concurrent attempts to lock identically named mutex throws.");`
+ );
+ } finally {
+ lockingMutex.unlock();
+ }
+});
+
+add_task(async function test_unlock_unblocks() {
+ const kTestMutexName = Services.uuid.generateUUID().toString();
+ let lockingMutex = parentFactory.createMutex(kTestMutexName);
+
+ info(`Locking mutex named "${kTestMutexName}"`);
+ lockingMutex.tryLock();
+ lockingMutex.unlock();
+
+ Assert.ok(!lockingMutex.isLocked(), "Reported unlocked after unlocking.");
+
+ await promiseCommand(
+ `(${assertLockOkOnChild.toString()})("${kTestMutexName}", "Locked previously unlocked mutex.");`
+ );
+});
+
+add_task(async function test_names_dont_conflict() {
+ const kTestMutexName = Services.uuid.generateUUID().toString();
+ let mutex1 = parentFactory.createMutex(kTestMutexName);
+
+ info(`Locking mutex named "${kTestMutexName}"`);
+ mutex1.tryLock();
+ try {
+ await promiseCommand(
+ `(${assertLockOkOnChild.toString()})(Services.uuid.generateUUID().toString(), "Differently named mutexes don't conflict");`
+ );
+ } finally {
+ mutex1.unlock();
+ }
+});
+
+add_task(async function test_relock_when_locked() {
+ const kTestMutexName = Services.uuid.generateUUID().toString();
+ let mutex = parentFactory.createMutex(kTestMutexName);
+
+ mutex.tryLock();
+ try {
+ Assert.ok(() => mutex.tryLock(), "Relocking locked mutex succeeds.");
+ Assert.ok(
+ mutex.isLocked(),
+ "Reported locked after relocking locked mutex."
+ );
+ } finally {
+ mutex.unlock();
+ }
+});
+
+add_task(async function test_unlock_without_lock() {
+ const kTestMutexName = Services.uuid.generateUUID().toString();
+ let mutex = parentFactory.createMutex(kTestMutexName);
+
+ mutex.unlock();
+ Assert.ok(
+ !mutex.isLocked(),
+ "Reported unlocked after unnecessarily unlocking mutex."
+ );
+
+ mutex.tryLock();
+ try {
+ Assert.ok(
+ mutex.isLocked(),
+ "Reported locked after locking unnecessarily unlocked mutex."
+ );
+ } finally {
+ mutex.unlock();
+ }
+});
diff --git a/toolkit/mozapps/defaultagent/tests/xpcshell/xpcshell.toml b/toolkit/mozapps/defaultagent/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..df1992fb13
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+run-if = ["os == 'win'"]
+
+["test_windows_mutex.js"]