summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/defaultagent/Telemetry.cpp
blob: 23f684d486badfcf082440f4471f499b5929ea5b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
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();
}