diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/mozapps/defaultagent/Telemetry.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | toolkit/mozapps/defaultagent/Telemetry.cpp | 498 |
1 files changed, 498 insertions, 0 deletions
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(); +} |