diff options
Diffstat (limited to '')
44 files changed, 5821 insertions, 0 deletions
diff --git a/toolkit/mozapps/defaultagent/Cache.cpp b/toolkit/mozapps/defaultagent/Cache.cpp new file mode 100644 index 0000000000..439fdf57f8 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Cache.cpp @@ -0,0 +1,590 @@ +/* -*- 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" + +// 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)); +} diff --git a/toolkit/mozapps/defaultagent/Cache.h b/toolkit/mozapps/defaultagent/Cache.h new file mode 100644 index 0000000000..a3c825d4f2 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Cache.h @@ -0,0 +1,185 @@ +/* -*- 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" + +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); +}; + +#endif // __DEFAULT_BROWSER_AGENT_CACHE_H__ diff --git a/toolkit/mozapps/defaultagent/DefaultBrowser.cpp b/toolkit/mozapps/defaultagent/DefaultBrowser.cpp new file mode 100644 index 0000000000..69f4a509c1 --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultBrowser.cpp @@ -0,0 +1,216 @@ +/* -*- 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/WinHeaderOnlyUtils.h" + +using BrowserResult = mozilla::WindowsErrorResult<Browser>; + +constexpr std::pair<std::string_view, Browser> kStringBrowserMap[]{ + {"", 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}, +}; + +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(""); +} + +Browser GetBrowserFromString(const std::string& browserString) { + for (const auto& [mapString, mapBrowser] : kStringBrowserMap) { + if (browserString == mapString) { + return mapBrowser; + } + } + + return Browser::Unknown; +} + +static BrowserResult GetDefaultBrowser() { + 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}, + }; + + 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 progIdEdgeHtml{ + L"AppXq0fevzme2pys62n3e0fbqa7peapykr8v"}; + + if (!wcsnicmp(registeredApp.get(), progIdEdgeHtml.data(), + progIdEdgeHtml.length())) { + return Browser::EdgeWithEdgeHTML; + } + } + + return browser; + } + } + + // The default browser is one that we don't know about. + return Browser::Unknown; +} + +static BrowserResult GetPreviousDefaultBrowser(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; + + BrowserResult defaultBrowserResult = GetDefaultBrowser(); + if (defaultBrowserResult.isErr()) { + return DefaultBrowserResult(defaultBrowserResult.unwrapErr()); + } + browserInfo.currentDefaultBrowser = defaultBrowserResult.unwrap(); + + BrowserResult previousDefaultBrowserResult = + GetPreviousDefaultBrowser(browserInfo.currentDefaultBrowser); + if (previousDefaultBrowserResult.isErr()) { + return DefaultBrowserResult(previousDefaultBrowserResult.unwrapErr()); + } + browserInfo.previousDefaultBrowser = previousDefaultBrowserResult.unwrap(); + + 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()); + } +} diff --git a/toolkit/mozapps/defaultagent/DefaultBrowser.h b/toolkit/mozapps/defaultagent/DefaultBrowser.h new file mode 100644 index 0000000000..870f923f76 --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultBrowser.h @@ -0,0 +1,31 @@ +/* -*- 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" + +MOZ_DEFINE_ENUM_CLASS(Browser, (Unknown, Firefox, Chrome, EdgeWithEdgeHTML, + EdgeWithBlink, InternetExplorer, Opera, Brave, + Yandex, QQBrowser, _360Browser, Sogou)); + +struct DefaultBrowserInfo { + Browser currentDefaultBrowser; + Browser previousDefaultBrowser; +}; + +using DefaultBrowserResult = mozilla::WindowsErrorResult<DefaultBrowserInfo>; + +DefaultBrowserResult GetDefaultBrowserInfo(); + +std::string GetStringForBrowser(Browser browser); +void MaybeMigrateCurrentDefault(); + +#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..27dbb3480c --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultPDF.cpp @@ -0,0 +1,75 @@ +/* -*- 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 "UtfConvert.h" + +#include "mozilla/Buffer.h" +#include "mozilla/RefPtr.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +using PdfResult = mozilla::WindowsErrorResult<std::string>; + +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)); + } + + return Utf16ToUtf8(friendlyNameBuffer.Elements()); +} + +DefaultPdfResult GetDefaultPdfInfo() { + DefaultPdfInfo pdfInfo; + MOZ_TRY_VAR(pdfInfo.currentDefaultPdf, GetDefaultPdf()); + + return pdfInfo; +} diff --git a/toolkit/mozapps/defaultagent/DefaultPDF.h b/toolkit/mozapps/defaultagent/DefaultPDF.h new file mode 100644 index 0000000000..35a0a499fd --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultPDF.h @@ -0,0 +1,22 @@ +/* -*- 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/WinHeaderOnlyUtils.h" + +struct DefaultPdfInfo { + std::string currentDefaultPdf; +}; + +using DefaultPdfResult = mozilla::WindowsErrorResult<DefaultPdfInfo>; + +DefaultPdfResult GetDefaultPdfInfo(); + +#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/Makefile.in b/toolkit/mozapps/defaultagent/Makefile.in new file mode 100644 index 0000000000..dadff2846b --- /dev/null +++ b/toolkit/mozapps/defaultagent/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/Notification.cpp b/toolkit/mozapps/defaultagent/Notification.cpp new file mode 100644 index 0000000000..1ccb670e40 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Notification.cpp @@ -0,0 +1,690 @@ +/* -*- 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/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.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" + +#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) + +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; + } + 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 stringsReader(iniPath.get()); + stringsReader.AddKey("DefaultBrowserNotificationTitle", + &strings.initialToast.text1); + stringsReader.AddKey("DefaultBrowserNotificationTitle", + &strings.followupToast.text1); + stringsReader.AddKey("DefaultBrowserNotificationText", + &strings.initialToast.text2); + stringsReader.AddKey("DefaultBrowserNotificationText", + &strings.followupToast.text2); + stringsReader.AddKey("DefaultBrowserNotificationMakeFirefoxDefault", + &strings.initialToast.action1); + stringsReader.AddKey("DefaultBrowserNotificationMakeFirefoxDefault", + &strings.followupToast.action1); + stringsReader.AddKey("DefaultBrowserNotificationDontShowAgain", + &strings.initialToast.action2); + stringsReader.AddKey("DefaultBrowserNotificationDontShowAgain", + &strings.followupToast.action2); + int result = stringsReader.Read(); + if (result != OK) { + LOG_ERROR_MESSAGE(L"Unable to read English strings: %d", result); + return false; + } + + const wchar_t* localizedIniFormat = L"%s\\defaultagent_localized.ini"; + bufferSize = _scwprintf(localizedIniFormat, installPath.get()); + ++bufferSize; // Extra character for terminating null + mozilla::UniquePtr<wchar_t[]> localizedIniPath = + mozilla::MakeUnique<wchar_t[]>(bufferSize); + _snwprintf_s(localizedIniPath.get(), bufferSize, _TRUNCATE, + localizedIniFormat, installPath.get()); + + IniReader localizedReader(localizedIniPath.get()); + localizedReader.AddKey("DefaultBrowserNotificationHeaderText", + &strings.localizedToast.text1); + localizedReader.AddKey("DefaultBrowserNotificationBodyText", + &strings.localizedToast.text2); + localizedReader.AddKey("DefaultBrowserNotificationYesButtonText", + &strings.localizedToast.action1); + localizedReader.AddKey("DefaultBrowserNotificationNoButtonText", + &strings.localizedToast.action2); + result = localizedReader.Read(); + if (result != OK) { + LOG_ERROR_MESSAGE(L"Unable to read localized strings: %d", result); + return false; + } + + // IniReader is only capable of reading from one section at a time, so we need + // to make another one to read the other section. + IniReader nonlocalizedReader(iniPath.get(), "Nonlocalized"); + nonlocalizedReader.AddKey("InitialToastRelativeImagePath", + &strings.initialToast.relImagePath); + nonlocalizedReader.AddKey("FollowupToastRelativeImagePath", + &strings.followupToast.relImagePath); + nonlocalizedReader.AddKey("LocalizedToastRelativeImagePath", + &strings.localizedToast.relImagePath); + 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(); + + const wchar_t* firefoxArgs[] = {firefoxPath.c_str(), + L"-to-handle-default-browser-agent"}; + mozilla::UniquePtr<wchar_t[]> firefoxCmdLine(mozilla::MakeCommandLine( + mozilla::ArrayLength(firefoxArgs), const_cast<wchar_t**>(firefoxArgs))); + + PROCESS_INFORMATION pi; + STARTUPINFOW si = {sizeof(si)}; + if (!::CreateProcessW(firefoxPath.c_str(), firefoxCmdLine.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(); +} + +/* + * 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. + * + * @return Success (SUCCEEDED(hr)) if all associations were set with + * UserChoice and checked successfully. + * Other return codes indicate a failure which causes us to + * fall back to Settings, see return codes of + * SetDefaultBrowserUserChoice(). + */ +static HRESULT SetDefaultBrowserFromNotification(const wchar_t* aumi) { + // TODO maybe fall back to protocol dialog on Windows 11 (bug 1719832)? + + HRESULT hr = E_FAIL; + if (GetPrefSetDefaultBrowserUserChoice()) { + hr = SetDefaultBrowserUserChoice(aumi); + } + + if (!FAILED(hr)) { + mozilla::Unused << LaunchFirefoxToHandleDefaultBrowserAgent(); + } else { + LOG_ERROR_MESSAGE(L"Failed to SetDefaultBrowserUserChoice: %#X", + GetLastError()); + LaunchModernSettingsDialogDefaultApps(); + } + return hr; +} + +// 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}; + using namespace WinToastLib; + + if (!WinToast::isCompatible()) { + LOG_ERROR_MESSAGE(L"System is not compatible with WinToast"); + return activitiesPerformed; + } + + 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 activitiesPerformed; + } + + bool isEnglishInstall = FirefoxInstallIsEnglish(); + + Strings strings; + if (!GetStrings(strings)) { + return activitiesPerformed; + } + const ToastStrings* toastStrings = + strings.GetToastStrings(whichNotification, isEnglishInstall); + + // 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 activitiesPerformed; + } + + 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 activitiesPerformed; + } + + // 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 activitiesPerformed; + } + 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 activitiesPerformed; + } + // 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 activitiesPerformed; + } + + 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}; + + if (!mozilla::IsWin10OrLater()) { + // Notifications aren't shown in versions prior to Windows 10 because the + // notification API we want isn't available. + return activitiesPerformed; + } + + // 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"); + } +} + +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"); + } +} + +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"; + } +} diff --git a/toolkit/mozapps/defaultagent/Notification.h b/toolkit/mozapps/defaultagent/Notification.h new file mode 100644 index 0000000000..9ea192adb5 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Notification.h @@ -0,0 +1,53 @@ +/* -*- 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" + +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); +std::string GetStringForNotificationAction(NotificationAction 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); + +#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..56d868866b --- /dev/null +++ b/toolkit/mozapps/defaultagent/Policy.cpp @@ -0,0 +1,158 @@ +/* -*- 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 + +// 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); +} diff --git a/toolkit/mozapps/defaultagent/Policy.h b/toolkit/mozapps/defaultagent/Policy.h new file mode 100644 index 0000000000..a1b8c47883 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Policy.h @@ -0,0 +1,13 @@ +/* -*- 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__ + +bool IsAgentDisabled(); +bool IsTelemetryDisabled(); + +#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..d0d1ea5a41 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Registry.cpp @@ -0,0 +1,325 @@ +/* -*- 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" + +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(); +} diff --git a/toolkit/mozapps/defaultagent/Registry.h b/toolkit/mozapps/defaultagent/Registry.h new file mode 100644 index 0000000000..08f83ea9a6 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Registry.h @@ -0,0 +1,96 @@ +/* -*- 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" + +// 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); + +#endif // __DEFAULT_BROWSER_AGENT_REGISTRY_H__ diff --git a/toolkit/mozapps/defaultagent/RemoteSettings.cpp b/toolkit/mozapps/defaultagent/RemoteSettings.cpp new file mode 100644 index 0000000000..37a376ff90 --- /dev/null +++ b/toolkit/mozapps/defaultagent/RemoteSettings.cpp @@ -0,0 +1,105 @@ +/* -*- 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 "RemoteSettings.h" + +#include <iostream> +#include <string> +#include <windows.h> +#include <shlwapi.h> + +#include "common.h" +#include "EventLog.h" +#include "Registry.h" + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/Unused.h" + +// All strings cross the C/C++ <-> Rust FFI boundary as +// null-terminated UTF-8. +extern "C" { +HRESULT IsAgentRemoteDisabledRust(const char* szUrl, DWORD* lpdwDisabled); +} + +#define PROD_ENDPOINT "https://firefox.settings.services.mozilla.com/v1" +#define PROD_BID "main" +#define PROD_CID "windows-default-browser-agent" +#define PROD_ID "state" + +#define PATH "buckets/" PROD_BID "/collections/" PROD_CID "/records/" PROD_ID + +using BoolResult = mozilla::WindowsErrorResult<bool>; + +// Use Rust library to query remote service endpoint to determine if +// WDBA is disabled. +// +// Pass through errors. +static BoolResult IsAgentRemoteDisabledInternal() { + // Fetch remote settings server root from registry. + auto serverResult = + RegistryGetValueString(IsPrefixed::Prefixed, L"ServicesSettingsServer"); + + // Unconfigured? Fallback to production. + std::string url = + serverResult.unwrapOr(mozilla::Some(std::string(PROD_ENDPOINT))) + .valueOr(std::string(PROD_ENDPOINT)); + + if (url.length() > 0 && url[url.length() - 1] != '/') { + url += '/'; + } + url += PATH; + + std::cerr << "default-browser-agent: Remote service disabled state URL: '" + << url << "'" << std::endl; + + // Invoke Rust to query remote settings. + DWORD isRemoteDisabled; + HRESULT hr = IsAgentRemoteDisabledRust(url.c_str(), &isRemoteDisabled); + + std::cerr << "default-browser-agent: HRESULT: 0x" << std::hex << hr + << std::endl; + + if (SUCCEEDED(hr)) { + return (0 != isRemoteDisabled); + } + + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); +} + +// Query remote service endpoint to determine if WDBA is disabled. +// +// Handle errors, failing to the last state witnessed without error. +bool IsAgentRemoteDisabled() { + // Fetch last witnessed state from registry. If we can't get the last + // state, or there is no last state, assume we're not disabled. + bool lastRemoteDisabled = + RegistryGetValueBool(IsPrefixed::Prefixed, + L"DefaultAgentLastRemoteDisabled") + .unwrapOr(mozilla::Some(false)) + .valueOr(false); + + std::cerr << "default-browser-agent: Last remote disabled: " + << lastRemoteDisabled << std::endl; + + auto remoteDisabledResult = IsAgentRemoteDisabledInternal(); + if (remoteDisabledResult.isErr()) { + // Fail to the last state witnessed without error. + return lastRemoteDisabled; + } + + bool remoteDisabled = remoteDisabledResult.unwrap(); + + std::cerr << "default-browser-agent: Next remote disabled: " << remoteDisabled + << std::endl; + + // Update last witnessed state in registry. + mozilla::Unused << RegistrySetValueBool( + IsPrefixed::Prefixed, L"DefaultAgentLastRemoteDisabled", remoteDisabled); + + return remoteDisabled; +} diff --git a/toolkit/mozapps/defaultagent/RemoteSettings.h b/toolkit/mozapps/defaultagent/RemoteSettings.h new file mode 100644 index 0000000000..152b077aed --- /dev/null +++ b/toolkit/mozapps/defaultagent/RemoteSettings.h @@ -0,0 +1,14 @@ +/* -*- 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_REMOTE_SETTINGS_H__ +#define __DEFAULT_BROWSER_AGENT_REMOTE_SETTINGS_H__ + +#include <windows.h> + +bool IsAgentRemoteDisabled(); + +#endif // __DEFAULT_BROWSER_AGENT_REMOTE_SETTINGS_H__ diff --git a/toolkit/mozapps/defaultagent/ScheduledTask.cpp b/toolkit/mozapps/defaultagent/ScheduledTask.cpp new file mode 100644 index 0000000000..9c77dc15ed --- /dev/null +++ b/toolkit/mozapps/defaultagent/ScheduledTask.cpp @@ -0,0 +1,409 @@ +/* -*- 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 <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" + +const wchar_t* kTaskVendor = L"" MOZ_APP_VENDOR; +// kTaskName should have the unique token appended before being used. +const wchar_t* kTaskName = L"" MOZ_APP_DISPLAYNAME " Default Browser 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; \ + } + +struct SysFreeStringDeleter { + void operator()(BSTR aPtr) { ::SysFreeString(aPtr); } +}; +using BStrPtr = mozilla::UniquePtr<OLECHAR, SysFreeStringDeleter>; + +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; + } + const wchar_t* iniFormat = L"%s\\defaultagent_localized.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 reader(iniPath.get()); + reader.AddKey("DefaultBrowserAgentTaskDescription", &description); + int status = reader.Read(); + if (status != OK) { + LOG_ERROR_MESSAGE(L"Failed to read task description: %d", status); + return false; + } + 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)) { + // The folder already existing isn't an error, but anything else is. + 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))); + + BStrPtr binaryPathBStr = + BStrPtr(SysAllocString(mozilla::GetFullBinaryPath().get())); + 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()); +} + +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; +} diff --git a/toolkit/mozapps/defaultagent/ScheduledTask.h b/toolkit/mozapps/defaultagent/ScheduledTask.h new file mode 100644 index 0000000000..1d775c64ab --- /dev/null +++ b/toolkit/mozapps/defaultagent/ScheduledTask.h @@ -0,0 +1,25 @@ +/* -*- 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> + +// 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); + +enum class WhichTasks { + WdbaTaskOnly, + AllTasksForInstallation, +}; +HRESULT RemoveTasks(const wchar_t* uniqueToken, WhichTasks tasksToRemove); + +#endif // __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__ diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp new file mode 100644 index 0000000000..047d8b7a31 --- /dev/null +++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp @@ -0,0 +1,365 @@ +/* -*- 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 <shlobj.h> // for SHChangeNotify and IApplicationAssociationRegistration + +#include "mozilla/ArrayUtils.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "WindowsUserChoice.h" + +#include "EventLog.h" +#include "SetDefaultBrowser.h" + +/* + * 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 aFileExtensions Optional null-terminated list of file association + * pairs to set as default, like `{ L".pdf", "FirefoxPDF", nullptr }`. + * + * @returns S_OK All associations set and checked successfully. + * MOZ_E_REJECTED UserChoice was set, but checking the default did not + * return our ProgID. + * E_FAIL Failed to set at least one association. + */ +static HRESULT SetDefaultExtensionHandlersUserChoiceImpl( + const wchar_t* aAumi, const wchar_t* const aSid, + const wchar_t* const* 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); +} + +/* + * 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 + * + * @return true if successful, false on error. + */ +static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid, + const wchar_t* aProgID) { + 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 = 100; + // 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; + } + } + + 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(hash.get()) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"Hash", 0, REG_SZ, + reinterpret_cast<const unsigned char*>(hash.get()), + hashByteCount); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + + return true; +} + +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; +} + +HRESULT SetDefaultBrowserUserChoice( + const wchar_t* aAumi, const wchar_t* const* aExtraFileExtensions) { + auto urlProgID = FormatProgID(L"FirefoxURL", aAumi); + if (!CheckProgIDExists(urlProgID.get())) { + LOG_ERROR_MESSAGE(L"ProgID %s not found", urlProgID.get()); + return MOZ_E_NO_PROGID; + } + + auto htmlProgID = FormatProgID(L"FirefoxHTML", aAumi); + if (!CheckProgIDExists(htmlProgID.get())) { + LOG_ERROR_MESSAGE(L"ProgID %s not found", htmlProgID.get()); + return MOZ_E_NO_PROGID; + } + + auto pdfProgID = FormatProgID(L"FirefoxPDF", aAumi); + if (!CheckProgIDExists(pdfProgID.get())) { + LOG_ERROR_MESSAGE(L"ProgID %s not found", pdfProgID.get()); + return MOZ_E_NO_PROGID; + } + + if (!CheckBrowserUserChoiceHashes()) { + LOG_ERROR_MESSAGE(L"UserChoice Hash mismatch"); + return MOZ_E_HASH_CHECK; + } + + if (!mozilla::IsWin10CreatorsUpdateOrLater()) { + LOG_ERROR_MESSAGE(L"UserChoice hash matched, but Windows build is too old"); + return MOZ_E_BUILD; + } + + auto sid = GetCurrentUserStringSid(); + if (!sid) { + return E_FAIL; + } + + bool ok = true; + bool defaultRejected = false; + + struct { + const wchar_t* ext; + const wchar_t* progID; + } associations[] = {{L"https", urlProgID.get()}, + {L"http", urlProgID.get()}, + {L".html", htmlProgID.get()}, + {L".htm", htmlProgID.get()}}; + for (size_t i = 0; i < mozilla::ArrayLength(associations); ++i) { + if (!SetUserChoice(associations[i].ext, sid.get(), + associations[i].progID)) { + ok = false; + break; + } else if (!VerifyUserDefault(associations[i].ext, + associations[i].progID)) { + defaultRejected = true; + ok = false; + break; + } + } + + if (ok) { + HRESULT hr = SetDefaultExtensionHandlersUserChoiceImpl( + aAumi, sid.get(), aExtraFileExtensions); + if (hr == MOZ_E_REJECTED) { + ok = false; + defaultRejected = true; + } else if (hr == E_FAIL) { + ok = false; + } + } + + // Notify shell to refresh icons + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); + + if (!ok) { + LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi); + if (defaultRejected) { + return MOZ_E_REJECTED; + } + return E_FAIL; + } + + return S_OK; +} + +HRESULT SetDefaultExtensionHandlersUserChoice( + const wchar_t* aAumi, const wchar_t* const* aFileExtensions) { + auto sid = GetCurrentUserStringSid(); + if (!sid) { + return E_FAIL; + } + + bool ok = true; + bool defaultRejected = false; + + HRESULT hr = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(), + aFileExtensions); + if (hr == MOZ_E_REJECTED) { + ok = false; + defaultRejected = true; + } else if (hr == E_FAIL) { + ok = false; + } + + // Notify shell to refresh icons + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); + + if (!ok) { + LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi); + if (defaultRejected) { + return MOZ_E_REJECTED; + } + return E_FAIL; + } + + return S_OK; +} + +HRESULT SetDefaultExtensionHandlersUserChoiceImpl( + const wchar_t* aAumi, const wchar_t* const aSid, + const wchar_t* const* aFileExtensions) { + const wchar_t* const* extraFileExtension = aFileExtensions; + const wchar_t* const* extraProgIDRoot = aFileExtensions + 1; + while (extraFileExtension && *extraFileExtension && extraProgIDRoot && + *extraProgIDRoot) { + // Formatting the ProgID here prevents using this helper to target arbitrary + // ProgIDs. + auto extraProgID = FormatProgID(*extraProgIDRoot, aAumi); + + if (!SetUserChoice(*extraFileExtension, aSid, extraProgID.get())) { + return E_FAIL; + } + + if (!VerifyUserDefault(*extraFileExtension, extraProgID.get())) { + return MOZ_E_REJECTED; + } + + extraFileExtension += 2; + extraProgIDRoot += 2; + } + + return S_OK; +} diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.h b/toolkit/mozapps/defaultagent/SetDefaultBrowser.h new file mode 100644 index 0000000000..4ac1ad24d9 --- /dev/null +++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.h @@ -0,0 +1,64 @@ +/* -*- 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__ + +/* + * 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 null-terminated list of extra file + * association pairs to set as default, like `{ L".pdf", "FirefoxPDF", nullptr + * }`. + * + * @return S_OK All associations set and checked successfully. + * MOZ_E_NO_PROGID The ProgID classes had not been registered. + * MOZ_E_HASH_CHECK The existing UserChoice Hash could not be verified. + * MOZ_E_REJECTED UserChoice was set, but checking the default + * did not return our ProgID. + * MOZ_E_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. + * E_FAIL other failure + */ +HRESULT SetDefaultBrowserUserChoice( + const wchar_t* aAumi, const wchar_t* const* aExtraFileExtensions = nullptr); + +/* + * 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 aFileExtensions Optional null-terminated list of file association + * pairs to set as default, like `{ L".pdf", "FirefoxPDF", nullptr }`. + * + * @returns S_OK All associations set and checked successfully. + * MOZ_E_REJECTED UserChoice was set, but checking the default did not + * return our ProgID. + * E_FAIL Failed to set at least one association. + */ +HRESULT SetDefaultExtensionHandlersUserChoice( + const wchar_t* aAumi, const wchar_t* const* aFileExtensions = nullptr); + +/* + * Additional HRESULT error codes from SetDefaultBrowserUserChoice + * + * 0x20000000 is set to put these in the customer-defined range. + */ +const HRESULT MOZ_E_NO_PROGID = 0xa0000001L; +const HRESULT MOZ_E_HASH_CHECK = 0xa0000002L; +const HRESULT MOZ_E_REJECTED = 0xa0000003L; +const HRESULT MOZ_E_BUILD = 0xa0000004L; + +#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..23f684d486 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Telemetry.cpp @@ -0,0 +1,498 @@ +/* -*- 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/HelperMacros.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WinHeaderOnlyUtils.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) + +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); +} + +static mozilla::WindowsError SendPing( + const std::string defaultBrowser, const std::string previousDefaultBrowser, + const std::string defaultPdf, const std::string osVersion, + 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["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, 0, 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; +} + +// 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; +} + +HRESULT SendDefaultBrowserPing( + const DefaultBrowserInfo& browserInfo, const DefaultPdfInfo& pdfInfo, + const NotificationActivities& activitiesPerformed) { + std::string currentDefaultBrowser = + GetStringForBrowser(browserInfo.currentDefaultBrowser); + std::string currentDefaultPdf = 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); + } + + // 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. + 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. + TelemetryFieldResult previousDefaultBrowserResult = + GetAndUpdatePreviousDefaultBrowser(currentDefaultBrowser, + browserInfo.previousDefaultBrowser); + if (previousDefaultBrowserResult.isErr()) { + return previousDefaultBrowserResult.unwrapErr().AsHResult(); + } + std::string previousDefaultBrowser = previousDefaultBrowserResult.unwrap(); + + return SendPing(currentDefaultBrowser, previousDefaultBrowser, + currentDefaultPdf, osVersion, osLocale, notificationType, + notificationShown, notificationAction, prevNotificationAction) + .AsHResult(); +} diff --git a/toolkit/mozapps/defaultagent/Telemetry.h b/toolkit/mozapps/defaultagent/Telemetry.h new file mode 100644 index 0000000000..45914fae3f --- /dev/null +++ b/toolkit/mozapps/defaultagent/Telemetry.h @@ -0,0 +1,20 @@ +/* -*- 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" + +HRESULT SendDefaultBrowserPing( + const DefaultBrowserInfo& browserInfo, const DefaultPdfInfo& pdfInfo, + const NotificationActivities& activitiesPerformed); + +#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..cf967130ab --- /dev/null +++ b/toolkit/mozapps/defaultagent/UtfConvert.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 "UtfConvert.h" + +#include <string> + +#include "EventLog.h" + +#include "mozilla/Buffer.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +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()); +} diff --git a/toolkit/mozapps/defaultagent/UtfConvert.h b/toolkit/mozapps/defaultagent/UtfConvert.h new file mode 100644 index 0000000000..8d69384857 --- /dev/null +++ b/toolkit/mozapps/defaultagent/UtfConvert.h @@ -0,0 +1,20 @@ +/* -*- 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" + +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); + +#endif // DEFAULT_BROWSER_UTF_CONVERT_H__ diff --git a/toolkit/mozapps/defaultagent/common.cpp b/toolkit/mozapps/defaultagent/common.cpp new file mode 100644 index 0000000000..588ede5d91 --- /dev/null +++ b/toolkit/mozapps/defaultagent/common.cpp @@ -0,0 +1,81 @@ +/* -*- 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> + +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); +} diff --git a/toolkit/mozapps/defaultagent/common.h b/toolkit/mozapps/defaultagent/common.h new file mode 100644 index 0000000000..12bebf5bfb --- /dev/null +++ b/toolkit/mozapps/defaultagent/common.h @@ -0,0 +1,25 @@ +/* -*- 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" + +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); + +#endif // __DEFAULT_BROWSER_AGENT_COMMON_H__ diff --git a/toolkit/mozapps/defaultagent/default-browser-agent.exe.manifest b/toolkit/mozapps/defaultagent/default-browser-agent.exe.manifest new file mode 100644 index 0000000000..ceb2839697 --- /dev/null +++ b/toolkit/mozapps/defaultagent/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/defaultagent.ini b/toolkit/mozapps/defaultagent/defaultagent.ini new file mode 100644 index 0000000000..2e8ea9e4b8 --- /dev/null +++ b/toolkit/mozapps/defaultagent/defaultagent.ini @@ -0,0 +1,11 @@ +; 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 +[Strings] +DefaultBrowserNotificationTitle=Switch back to %MOZ_APP_DISPLAYNAME%? +DefaultBrowserNotificationText=Your default browser was recently changed. +DefaultBrowserNotificationRemindMeLater=Remind me later +DefaultBrowserNotificationMakeFirefoxDefault=Yes, switch back +DefaultBrowserNotificationDontShowAgain=Don’t show again diff --git a/toolkit/mozapps/defaultagent/defaultagent_append.ini b/toolkit/mozapps/defaultagent/defaultagent_append.ini new file mode 100644 index 0000000000..77e26eb9e2 --- /dev/null +++ b/toolkit/mozapps/defaultagent/defaultagent_append.ini @@ -0,0 +1,8 @@ + +; IMPORTANT: This file should always start with a newline in case a locale +; provided INI does not end with a newline. + +[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..c625151ae4 --- /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 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/main.cpp b/toolkit/mozapps/defaultagent/main.cpp new file mode 100644 index 0000000000..42eabb8d77 --- /dev/null +++ b/toolkit/mozapps/defaultagent/main.cpp @@ -0,0 +1,428 @@ +/* -*- 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 <iostream> + +#include "nsAutoRef.h" +#include "nsWindowsHelpers.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +#include "common.h" +#include "Policy.h" +#include "DefaultBrowser.h" +#include "DefaultPDF.h" +#include "EventLog.h" +#include "Notification.h" +#include "Registry.h" +#include "RemoteSettings.h" +#include "ScheduledTask.h" +#include "SetDefaultBrowser.h" +#include "Telemetry.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) + +// 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()))) { + RegDeleteValue(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()); + } +} + +// 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 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; +} + +// 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`. +int wmain(int argc, wchar_t** argv) { + if (argc < 2 || !argv[1]) { + return E_INVALIDARG; + } + + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(hr)) { + return hr; + } + + const struct ComUninitializer { + ~ComUninitializer() { CoUninitialize(); } + } kCUi; + + RegistryMutex regMutex; + + // 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. + if (!wcscmp(argv[1], L"uninstall")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + // 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. + regMutex.Acquire(); + + RemoveAllRegistryEntries(); + return RemoveTasks(argv[2], WhichTasks::AllTasksForInstallation); + } else if (!wcscmp(argv[1], L"unregister-task")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + return RemoveTasks(argv[2], WhichTasks::WdbaTaskOnly); + } else if (!wcscmp(argv[1], L"debug-remote-disabled")) { + int disabled = IsAgentRemoteDisabled(); + std::cerr << "default-browser-agent: IsAgentRemoteDisabled: " << disabled + << std::endl; + return S_OK; + } + + // 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 (IsAgentDisabled()) { + return HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY); + } + + if (!wcscmp(argv[1], L"register-task")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + // 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. + regMutex.Acquire(); + + WriteInstallationRegistryEntry(); + + return RegisterTask(argv[2]); + } else if (!wcscmp(argv[1], L"update-task")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + // Not checking if we got the mutex for the same reason we didn't in + // register-task + regMutex.Acquire(); + + WriteInstallationRegistryEntry(); + + return UpdateTask(argv[2]); + } else if (!wcscmp(argv[1], L"do-task")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + bool force = (argc > 3) && ((0 == wcscmp(argv[3], L"--force")) || + (0 == wcscmp(argv[3], L"-force"))); + + // 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. + if (!regMutex.Acquire()) { + return HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); + } + + // 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 (!force && (!CheckIfAppRanRecently(&ranRecently) || !ranRecently)) { + return SCHED_E_TASK_ATTEMPTED; + } + + // Check for remote disable and (re-)enable before (potentially) + // updating registry entries and showing notifications. + if (IsAgentRemoteDisabled()) { + return S_OK; + } + + DefaultBrowserResult defaultBrowserResult = GetDefaultBrowserInfo(); + if (defaultBrowserResult.isErr()) { + return defaultBrowserResult.unwrapErr().AsHResult(); + } + DefaultBrowserInfo browserInfo = defaultBrowserResult.unwrap(); + + DefaultPdfResult defaultPdfResult = GetDefaultPdfInfo(); + if (defaultPdfResult.isErr()) { + return defaultPdfResult.unwrapErr().AsHResult(); + } + DefaultPdfInfo pdfInfo = defaultPdfResult.unwrap(); + + NotificationActivities activitiesPerformed = + MaybeShowNotification(browserInfo, argv[2], force); + + return SendDefaultBrowserPing(browserInfo, pdfInfo, activitiesPerformed); + } else if (!wcscmp(argv[1], L"set-default-browser-user-choice")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + // `argv` is itself null-terminated, so we can safely pass the tail of the + // array here. + return SetDefaultBrowserUserChoice(argv[2], &argv[3]); + } else if (!wcscmp(argv[1], L"set-default-extension-handlers-user-choice")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + // `argv` is itself null-terminated, so we can safely pass the tail of the + // array here. + return SetDefaultExtensionHandlersUserChoice(argv[2], &argv[3]); + } else { + return E_INVALIDARG; + } +} 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..c675da6e99 --- /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/. + +Program("default-browser-agent") + +SPHINX_TREES["default-browser-agent"] = "docs" + +DIRS += ["rust"] + +UNIFIED_SOURCES += [ + "/mfbt/Poison.cpp", + "/mfbt/Unused.cpp", + "Cache.cpp", + "common.cpp", + "DefaultBrowser.cpp", + "DefaultPDF.cpp", + "EventLog.cpp", + "main.cpp", + "Notification.cpp", + "Policy.cpp", + "Registry.cpp", + "RemoteSettings.cpp", + "ScheduledTask.cpp", + "SetDefaultBrowser.cpp", + "Telemetry.cpp", + "UtfConvert.cpp", +] + +SOURCES += [ + "/browser/components/shell/WindowsDefaultBrowser.cpp", + "/browser/components/shell/WindowsUserChoice.cpp", + "/other-licenses/nsis/Contrib/CityHash/cityhash/city.cpp", + "/third_party/WinToast/wintoastlib.cpp", + "/toolkit/mozapps/update/common/readstrings.cpp", +] + +# Suppress warnings from third-party code. +SOURCES["/third_party/WinToast/wintoastlib.cpp"].flags += ["-Wno-implicit-fallthrough"] + +USE_LIBS += [ + "defaultagent-static", + "jsoncpp", +] + +LOCAL_INCLUDES += [ + "/browser/components/shell/", + "/mfbt/", + "/other-licenses/nsis/Contrib/CityHash/cityhash", + "/third_party/WinToast", + "/toolkit/components/jsoncpp/include", + "/toolkit/mozapps/update/common", + "/xpcom/build", +] + +OS_LIBS += [ + "advapi32", + "bcrypt", + "comsupp", + "crypt32", + "kernel32", + "netapi32", + "ole32", + "oleaut32", + "rpcrt4", + "shell32", + "shlwapi", + "taskschd", + "userenv", + "wininet", + "ws2_32", + "ntdll", +] + +DEFINES["NS_NO_XPCOM"] = True +DEFINES["IMPL_MFBT"] = True + +DEFINES["UNICODE"] = True +DEFINES["_UNICODE"] = True + +# 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] + +# 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"] + +GENERATED_FILES += ["defaultagent.ini"] +defaultagentini = GENERATED_FILES["defaultagent.ini"] +defaultagentini.script = "/browser/locales/generate_ini.py" +defaultagentini.inputs = [ + "defaultagent.ini", + "defaultagent_append.ini", +] +FINAL_TARGET_FILES += ["!defaultagent.ini"] + +if CONFIG["ENABLE_TESTS"]: + DIRS += ["tests/gtest"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Default Browser Agent") diff --git a/toolkit/mozapps/defaultagent/rust/Cargo.toml b/toolkit/mozapps/defaultagent/rust/Cargo.toml new file mode 100644 index 0000000000..4bd44a9df8 --- /dev/null +++ b/toolkit/mozapps/defaultagent/rust/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "defaultagent-static" +version = "0.1.0" +authors = ["The Mozilla Install/Update Team <install-update@mozilla.com>"] +edition = "2018" +description = "FFI to Rust for use in Firefox's default browser agent." +repository = "https://github.com/mozilla/defaultagent-static" +license = "MPL-2.0" + +[dependencies] +log = { version = "0.4", features = ["std"] } +mozilla-central-workspace-hack = { path = "../../../../build/workspace-hack" } +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +url = "2.1" +viaduct = "0.1" +wineventlog = { path = "wineventlog"} +wio = "0.2" +winapi = { version = "0.3", features = ["errhandlingapi", "handleapi", "minwindef", "winerror", "wininet", "winuser"] } + +[lib] +crate-type = ["staticlib"] diff --git a/toolkit/mozapps/defaultagent/rust/moz.build b/toolkit/mozapps/defaultagent/rust/moz.build new file mode 100644 index 0000000000..accce52265 --- /dev/null +++ b/toolkit/mozapps/defaultagent/rust/moz.build @@ -0,0 +1,7 @@ +# -*- 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/. + +RustLibrary("defaultagent-static") diff --git a/toolkit/mozapps/defaultagent/rust/src/lib.rs b/toolkit/mozapps/defaultagent/rust/src/lib.rs new file mode 100644 index 0000000000..de5f1ba03a --- /dev/null +++ b/toolkit/mozapps/defaultagent/rust/src/lib.rs @@ -0,0 +1,166 @@ +/* -*- Mode: rust; rust-indent-offset: 4 -*- */ +/* 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/. */ + +#![allow(non_snake_case)] + +use std::ffi::{CStr, OsString}; +use std::os::raw::c_char; + +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate log; +use winapi::shared::ntdef::HRESULT; +use winapi::shared::winerror::{HRESULT_FROM_WIN32, S_OK}; +use wio::wide::FromWide; + +mod viaduct_wininet; +use viaduct_wininet::WinInetBackend; + +// HRESULT with 0x80000000 is an error, 0x20000000 set is a customer error code. +#[allow(overflowing_literals)] +const HR_NETWORK_ERROR: HRESULT = 0x80000000 | 0x20000000 | 0x1; +#[allow(overflowing_literals)] +const HR_SETTINGS_ERROR: HRESULT = 0x80000000 | 0x20000000 | 0x2; + +#[derive(Debug, Deserialize)] +pub struct EnabledRecord { + // Unknown fields are ignored by serde: see the docs for `#[serde(deny_unknown_fields)]`. + pub(crate) enabled: bool, +} + +pub enum Error { + /// A backend error with an attached Windows error code from `GetLastError()`. + WindowsError(u32), + + /// A network or otherwise transient error. + NetworkError, + + /// A configuration or settings data error that probably requires code, configuration, or + /// server-side changes to address. + SettingsError, +} + +impl From<viaduct::UnexpectedStatus> for Error { + fn from(_err: viaduct::UnexpectedStatus) -> Self { + Error::NetworkError + } +} + +impl From<viaduct::Error> for Error { + fn from(err: viaduct::Error) -> Self { + match err { + viaduct::Error::NetworkError(_) => Error::NetworkError, + viaduct::Error::BackendError(raw) => { + // If we have a string that's a hex error code like + // "0xabcde", that's a Windows error. + if raw.starts_with("0x") { + let without_prefix = raw.trim_start_matches("0x"); + let parse_result = u32::from_str_radix(without_prefix, 16); + if let Ok(parsed) = parse_result { + return Error::WindowsError(parsed); + } + } + Error::SettingsError + } + _ => Error::SettingsError, + } + } +} + +impl From<serde_json::Error> for Error { + fn from(_err: serde_json::Error) -> Self { + Error::SettingsError + } +} + +impl From<url::ParseError> for Error { + fn from(_err: url::ParseError) -> Self { + Error::SettingsError + } +} + +fn is_agent_remote_disabled<S: AsRef<str>>(url: S) -> Result<bool, Error> { + // Be careful setting the viaduct backend twice. If the backend + // has been set already, assume that it's our backend: we may as + // well at least try to continue. + match viaduct::set_backend(&WinInetBackend) { + Ok(_) => {} + Err(viaduct::Error::SetBackendError) => {} + e => e?, + } + + let url = url::Url::parse(url.as_ref())?; + let req = viaduct::Request::new(viaduct::Method::Get, url); + let resp = req.send()?; + + let resp = resp.require_success()?; + + let body: serde_json::Value = resp.json()?; + let data = body.get("data").ok_or(Error::SettingsError)?; + let record: EnabledRecord = serde_json::from_value(data.clone())?; + + let disabled = !record.enabled; + Ok(disabled) +} + +// This is an easy way to consume `MOZ_APP_DISPLAYNAME` from Rust code. +extern "C" { + static gWinEventLogSourceName: *const u16; +} + +#[allow(dead_code)] +#[no_mangle] +extern "C" fn IsAgentRemoteDisabledRust(szUrl: *const c_char, lpdwDisabled: *mut u32) -> HRESULT { + let wineventlog_name = unsafe { OsString::from_wide_ptr_null(gWinEventLogSourceName) }; + let logger = wineventlog::EventLogger::new(&wineventlog_name); + // It's fine to initialize logging twice. + let _ = log::set_boxed_logger(Box::new(logger)); + log::set_max_level(log::LevelFilter::Info); + + // Use an IIFE for `?`. + let disabled_result = (|| { + if lpdwDisabled.is_null() { + return Err(Error::SettingsError); + } + + let url = unsafe { CStr::from_ptr(szUrl).to_str().map(|x| x.to_string()) } + .map_err(|_| Error::SettingsError)?; + + info!("Using remote settings URL: {}", url); + + is_agent_remote_disabled(url) + })(); + + match disabled_result { + Err(e) => { + return match e { + Error::WindowsError(errno) => { + let hr = HRESULT_FROM_WIN32(errno); + error!("Error::WindowsError({}) (HRESULT: 0x{:x})", errno, hr); + hr + } + Error::NetworkError => { + let hr = HR_NETWORK_ERROR; + error!("Error::NetworkError (HRESULT: 0x{:x})", hr); + hr + } + Error::SettingsError => { + let hr = HR_SETTINGS_ERROR; + error!("Error::SettingsError (HRESULT: 0x{:x})", hr); + hr + } + }; + } + + Ok(remote_disabled) => { + // We null-checked `lpdwDisabled` earlier, but just to be safe. + if !lpdwDisabled.is_null() { + unsafe { *lpdwDisabled = if remote_disabled { 1 } else { 0 } }; + } + return S_OK; + } + } +} diff --git a/toolkit/mozapps/defaultagent/rust/src/viaduct_wininet/internet_handle.rs b/toolkit/mozapps/defaultagent/rust/src/viaduct_wininet/internet_handle.rs new file mode 100644 index 0000000000..85f4254c88 --- /dev/null +++ b/toolkit/mozapps/defaultagent/rust/src/viaduct_wininet/internet_handle.rs @@ -0,0 +1,53 @@ +// Licensed under the Apache License, Version 2.0 +// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// All files in the project carrying such notice may not be copied, modified, or distributed +// except according to those terms. + +//! Wrapping and automatically closing Internet handles. Copy-pasted from +//! [comedy-rs](https://github.com/agashlin/comedy-rs/blob/c244b91e9237c887f6a7bc6cd03db98b51966494/src/handle.rs). + +use winapi::shared::minwindef::DWORD; +use winapi::shared::ntdef::NULL; +use winapi::um::errhandlingapi::GetLastError; +use winapi::um::wininet::{InternetCloseHandle, HINTERNET}; + +/// Check and automatically close a Windows `HINTERNET`. +#[repr(transparent)] +#[derive(Debug)] +pub struct InternetHandle(HINTERNET); + +impl InternetHandle { + /// Take ownership of a `HINTERNET`, which will be closed with `InternetCloseHandle` upon drop. + /// Returns an error in case of `NULL`. + /// + /// # Safety + /// + /// `h` should be the only copy of the handle. `GetLastError()` is called to + /// return an error, so the last Windows API called on this thread should have been + /// what produced the invalid handle. + pub unsafe fn new(h: HINTERNET) -> Result<InternetHandle, DWORD> { + if h == NULL { + Err(GetLastError()) + } else { + Ok(InternetHandle(h)) + } + } + + /// Obtains the raw `HINTERNET` without transferring ownership. + /// + /// Do __not__ close this handle because it is still owned by the `InternetHandle`. + /// + /// Do __not__ use this handle beyond the lifetime of the `InternetHandle`. + pub fn as_raw(&self) -> HINTERNET { + self.0 + } +} + +impl Drop for InternetHandle { + fn drop(&mut self) { + unsafe { + InternetCloseHandle(self.0); + } + } +} diff --git a/toolkit/mozapps/defaultagent/rust/src/viaduct_wininet/mod.rs b/toolkit/mozapps/defaultagent/rust/src/viaduct_wininet/mod.rs new file mode 100644 index 0000000000..1abd170f8f --- /dev/null +++ b/toolkit/mozapps/defaultagent/rust/src/viaduct_wininet/mod.rs @@ -0,0 +1,257 @@ +// Licensed under the Apache License, Version 2.0 +// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// All files in the project carrying such notice may not be copied, modified, or distributed +// except according to those terms. + +use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER; +use winapi::um::errhandlingapi::GetLastError; +use winapi::um::wininet; +use wio::wide::ToWide; + +use viaduct::Backend; + +mod internet_handle; +use internet_handle::InternetHandle; + +pub struct WinInetBackend; + +/// Errors +fn to_viaduct_error(e: u32) -> viaduct::Error { + // Like "0xabcde". + viaduct::Error::BackendError(format!("{:#x}", e)) +} + +fn get_status(req: wininet::HINTERNET) -> Result<u16, viaduct::Error> { + let mut status: u32 = 0; + let mut size: u32 = std::mem::size_of::<u32>() as u32; + let result = unsafe { + wininet::HttpQueryInfoW( + req, + wininet::HTTP_QUERY_STATUS_CODE | wininet::HTTP_QUERY_FLAG_NUMBER, + &mut status as *mut _ as *mut _, + &mut size, + std::ptr::null_mut(), + ) + }; + if 0 == result { + return Err(to_viaduct_error(unsafe { GetLastError() })); + } + + Ok(status as u16) +} + +fn get_headers(req: wininet::HINTERNET) -> Result<viaduct::Headers, viaduct::Error> { + // We follow https://docs.microsoft.com/en-us/windows/win32/wininet/retrieving-http-headers. + // + // Per + // https://docs.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-httpqueryinfoa: + // The `HttpQueryInfoA` function represents headers as ISO-8859-1 characters + // not ANSI characters. + let mut size: u32 = 0; + + let result = unsafe { + wininet::HttpQueryInfoA( + req, + wininet::HTTP_QUERY_RAW_HEADERS, + std::ptr::null_mut(), + &mut size, + std::ptr::null_mut(), + ) + }; + if 0 == result { + let error = unsafe { GetLastError() }; + if error == wininet::ERROR_HTTP_HEADER_NOT_FOUND { + return Ok(viaduct::Headers::new()); + } else if error != ERROR_INSUFFICIENT_BUFFER { + return Err(to_viaduct_error(error)); + } + } + + let mut buffer = vec![0 as u8; size as usize]; + let result = unsafe { + wininet::HttpQueryInfoA( + req, + wininet::HTTP_QUERY_RAW_HEADERS, + buffer.as_mut_ptr() as *mut _, + &mut size, + std::ptr::null_mut(), + ) + }; + if 0 == result { + let error = unsafe { GetLastError() }; + if error == wininet::ERROR_HTTP_HEADER_NOT_FOUND { + return Ok(viaduct::Headers::new()); + } else { + return Err(to_viaduct_error(error)); + } + } + + // The API returns all of the headers as a single char buffer in + // ISO-8859-1 encoding. Each header is terminated by '\0' and + // there's a trailing '\0' terminator as well. + // + // We want UTF-8. It's not worth include a non-trivial encoding + // library like `encoding_rs` just for these headers, so let's use + // the fact that ISO-8859-1 and UTF-8 intersect on the lower 7 bits + // and decode lossily. It will at least be reasonably clear when + // there is an encoding issue. + let allheaders = String::from_utf8_lossy(&buffer); + + let mut headers = viaduct::Headers::new(); + for header in allheaders.split(0 as char) { + let mut it = header.splitn(2, ":"); + if let (Some(name), Some(value)) = (it.next(), it.next()) { + headers.insert(name.trim().to_string(), value.trim().to_string())?; + } + } + + return Ok(headers); +} + +fn get_body(req: wininet::HINTERNET) -> Result<Vec<u8>, viaduct::Error> { + let mut body = Vec::new(); + + const BUFFER_SIZE: usize = 65535; + let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; + + loop { + let mut bytes_downloaded: u32 = 0; + let result = unsafe { + wininet::InternetReadFile( + req, + buffer.as_mut_ptr() as *mut _, + BUFFER_SIZE as u32, + &mut bytes_downloaded, + ) + }; + if 0 == result { + return Err(to_viaduct_error(unsafe { GetLastError() })); + } + if bytes_downloaded == 0 { + break; + } + + body.extend_from_slice(&buffer[0..bytes_downloaded as usize]); + } + Ok(body) +} + +impl Backend for WinInetBackend { + fn send(&self, request: viaduct::Request) -> Result<viaduct::Response, viaduct::Error> { + viaduct::note_backend("wininet.dll"); + + let request_method = request.method; + let url = request.url; + + let session = unsafe { + InternetHandle::new(wininet::InternetOpenW( + "DefaultAgent/1.0".to_wide_null().as_ptr(), + wininet::INTERNET_OPEN_TYPE_PRECONFIG, + std::ptr::null_mut(), + std::ptr::null_mut(), + 0, + )) + } + .map_err(to_viaduct_error)?; + + // Consider asserting the scheme here too, for documentation purposes. + // Viaduct itself only allows HTTPS at this time, but that might change. + let host = url + .host_str() + .ok_or(viaduct::Error::BackendError("no host".to_string()))?; + + let conn = unsafe { + InternetHandle::new(wininet::InternetConnectW( + session.as_raw(), + host.to_wide_null().as_ptr(), + wininet::INTERNET_DEFAULT_HTTPS_PORT as u16, + std::ptr::null_mut(), + std::ptr::null_mut(), + wininet::INTERNET_SERVICE_HTTP, + 0, + 0, + )) + } + .map_err(to_viaduct_error)?; + + let path = url[url::Position::BeforePath..].to_string(); + let req = unsafe { + wininet::HttpOpenRequestW( + conn.as_raw(), + request_method.as_str().to_wide_null().as_ptr(), + path.to_wide_null().as_ptr(), + std::ptr::null_mut(), /* lpszVersion */ + std::ptr::null_mut(), /* lpszReferrer */ + std::ptr::null_mut(), /* lplpszAcceptTypes */ + // Avoid the cache as best we can. + wininet::INTERNET_FLAG_NO_AUTH + | wininet::INTERNET_FLAG_NO_CACHE_WRITE + | wininet::INTERNET_FLAG_NO_COOKIES + | wininet::INTERNET_FLAG_NO_UI + | wininet::INTERNET_FLAG_PRAGMA_NOCACHE + | wininet::INTERNET_FLAG_RELOAD + | wininet::INTERNET_FLAG_SECURE, + 0, + ) + }; + if req.is_null() { + return Err(to_viaduct_error(unsafe { GetLastError() })); + } + + for header in request.headers { + // Per + // https://docs.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-httpaddrequestheadersw, + // "Each header must be terminated by a CR/LF (carriage return/line + // feed) pair." + let h = format!("{}: {}\r\n", header.name(), header.value()); + let result = unsafe { + wininet::HttpAddRequestHeadersW( + req, + h.to_wide_null().as_ptr(), /* lpszHeaders */ + -1i32 as u32, /* dwHeadersLength */ + wininet::HTTP_ADDREQ_FLAG_ADD | wininet::HTTP_ADDREQ_FLAG_REPLACE, /* dwModifiers */ + ) + }; + if 0 == result { + return Err(to_viaduct_error(unsafe { GetLastError() })); + } + } + + // Future work: support sending a body. + if request.body.is_some() { + return Err(viaduct::Error::BackendError( + "non-empty body is not yet supported".to_string(), + )); + } + + let result = unsafe { + wininet::HttpSendRequestW( + req, + std::ptr::null_mut(), /* lpszHeaders */ + 0, /* dwHeadersLength */ + std::ptr::null_mut(), /* lpOptional */ + 0, /* dwOptionalLength */ + ) + }; + if 0 == result { + return Err(to_viaduct_error(unsafe { GetLastError() })); + } + + let status = get_status(req)?; + let headers = get_headers(req)?; + + // Not all responses have a body. + let has_body = headers.get_header("content-type").is_some() + || headers.get_header("content-length").is_some(); + let body = if has_body { get_body(req)? } else { Vec::new() }; + + Ok(viaduct::Response { + request_method, + body, + url, + status, + headers, + }) + } +} diff --git a/toolkit/mozapps/defaultagent/rust/wineventlog/Cargo.toml b/toolkit/mozapps/defaultagent/rust/wineventlog/Cargo.toml new file mode 100644 index 0000000000..b38f704ca1 --- /dev/null +++ b/toolkit/mozapps/defaultagent/rust/wineventlog/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "wineventlog" +version = "0.1.0" +authors = ["The Mozilla Project Developers"] +license = "MPL-2.0" +autobins = false +edition = "2018" + +[target."cfg(windows)".dependencies] +log = "0.4" +wio = "0.2" + +[target."cfg(windows)".dependencies.winapi] +version = "0.3.7" +features = ["errhandlingapi", "minwindef", "ntdef", "oaidl", "oleauto", "sysinfoapi", "taskschd", "winbase", "winerror", "winnt", "winreg", "wtypes"] diff --git a/toolkit/mozapps/defaultagent/rust/wineventlog/src/lib.rs b/toolkit/mozapps/defaultagent/rust/wineventlog/src/lib.rs new file mode 100644 index 0000000000..c98b75c53a --- /dev/null +++ b/toolkit/mozapps/defaultagent/rust/wineventlog/src/lib.rs @@ -0,0 +1,76 @@ +/* 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/. */ + +//! Very simple implementation of logging via the Windows Event Log + +use std::ffi::OsStr; +use std::ptr; + +use log::{Level, Metadata, Record}; +use winapi::shared::minwindef::WORD; +use winapi::um::{winbase, winnt}; +use wio::wide::ToWide; + +pub struct EventLogger { + pub name: Vec<u16>, +} + +impl EventLogger { + pub fn new(name: impl AsRef<OsStr>) -> Self { + EventLogger { + name: name.to_wide_null(), + } + } +} + +impl log::Log for EventLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + + let name = self.name.as_ptr(); + let msg = format!("{} - {}", record.level(), record.args()).to_wide_null(); + + // Open and close the event log handle on every message, for simplicity. + let event_log; + unsafe { + event_log = winbase::RegisterEventSourceW(ptr::null(), name); + if event_log.is_null() { + return; + } + } + + let level = match record.level() { + Level::Error => winnt::EVENTLOG_ERROR_TYPE, + Level::Warn => winnt::EVENTLOG_WARNING_TYPE, + Level::Info | Level::Debug | Level::Trace => winnt::EVENTLOG_INFORMATION_TYPE, + }; + + unsafe { + // mut only to match the LPCWSTR* signature + let mut msg_array: [*const u16; 1] = [msg.as_ptr()]; + + let _ = winbase::ReportEventW( + event_log, + level, + 0, // no category + 0, // event id 0 + ptr::null_mut(), // no user sid + msg_array.len() as WORD, // string count + 0, // 0 bytes raw data + msg_array.as_mut_ptr(), // strings + ptr::null_mut(), // no raw data + ); + + let _ = winbase::DeregisterEventSource(event_log); + } + } + + fn flush(&self) {} +} diff --git a/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp b/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp new file mode 100644 index 0000000000..a5a618ff23 --- /dev/null +++ b/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp @@ -0,0 +1,299 @@ +/* -*- 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" + +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..ba85b0a105 --- /dev/null +++ b/toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp @@ -0,0 +1,53 @@ +/* -*- 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" + +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..edaf8798d8 --- /dev/null +++ b/toolkit/mozapps/defaultagent/tests/gtest/moz.build @@ -0,0 +1,45 @@ +# -*- 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") + +# Normally, we only include the test code in gtest sources because they get +# linked against libxul. But the code we are testing is not part of libxul, so +# if we want it to be available to us to test, we have to include it here. +UNIFIED_SOURCES += [ + "../../Cache.cpp", + "../../common.cpp", + "../../EventLog.cpp", + "../../Registry.cpp", + "../../UtfConvert.cpp", + "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] + +# We need STL headers that aren't allowed when wrapping is on (at least +# <filesystem>, and possibly others). +DisableStlWrapping() + +FINAL_LIBRARY = "xul-gtest" |