Backed out 3 changesets (bug 1883476, bug 1826375) for causing windows build bustages...
[gecko.git] / toolkit / mozapps / defaultagent / DefaultAgent.cpp
blob2ebb5e466ef91ff9838caaca9ace9e32bc6a9d2a
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include <windows.h>
8 #include <shlwapi.h>
9 #include <objbase.h>
10 #include <string.h>
11 #include <vector>
13 #include "nsAutoRef.h"
14 #include "nsDebug.h"
15 #include "nsProxyRelease.h"
16 #include "nsWindowsHelpers.h"
17 #include "nsString.h"
19 #include "common.h"
20 #include "DefaultBrowser.h"
21 #include "DefaultPDF.h"
22 #include "EventLog.h"
23 #include "Notification.h"
24 #include "Policy.h"
25 #include "Registry.h"
26 #include "ScheduledTask.h"
27 #include "ScheduledTaskRemove.h"
28 #include "SetDefaultBrowser.h"
29 #include "Telemetry.h"
30 #include "xpcpublic.h"
31 #include "mozilla/dom/Promise.h"
32 #include "mozilla/ErrorResult.h"
34 #include "DefaultAgent.h"
36 // The AGENT_REGKEY_NAME is dependent on MOZ_APP_VENDOR and MOZ_APP_BASENAME,
37 // so using those values in the mutex name prevents waiting on processes that
38 // are using completely different data.
39 #define REGISTRY_MUTEX_NAME \
40 L"" MOZ_APP_VENDOR MOZ_APP_BASENAME L"DefaultBrowserAgentRegistryMutex"
41 // How long to wait on the registry mutex before giving up on it. This should
42 // be short. Although the WDBA runs in the background, uninstallation happens
43 // synchronously in the foreground.
44 #define REGISTRY_MUTEX_TIMEOUT_MS (3 * 1000)
46 namespace mozilla::default_agent {
48 // This class is designed to prevent concurrency problems when accessing the
49 // registry. It should be acquired before any usage of unprefixed registry
50 // entries.
51 class RegistryMutex {
52 private:
53 nsAutoHandle mMutex;
54 bool mLocked;
56 public:
57 RegistryMutex() : mMutex(nullptr), mLocked(false) {}
58 ~RegistryMutex() {
59 Release();
60 // nsAutoHandle will take care of closing the mutex's handle.
63 // Returns true on success, false on failure.
64 bool Acquire() {
65 if (mLocked) {
66 return true;
69 if (mMutex.get() == nullptr) {
70 // It seems like we would want to set the second parameter (bInitialOwner)
71 // to TRUE, but the documentation for CreateMutexW suggests that, because
72 // we aren't sure that the mutex doesn't already exist, we can't be sure
73 // whether we got ownership via this mechanism.
74 mMutex.own(CreateMutexW(nullptr, FALSE, REGISTRY_MUTEX_NAME));
75 if (mMutex.get() == nullptr) {
76 LOG_ERROR_MESSAGE(L"Couldn't open registry mutex: %#X", GetLastError());
77 return false;
81 DWORD mutexStatus =
82 WaitForSingleObject(mMutex.get(), REGISTRY_MUTEX_TIMEOUT_MS);
83 if (mutexStatus == WAIT_OBJECT_0) {
84 mLocked = true;
85 } else if (mutexStatus == WAIT_TIMEOUT) {
86 LOG_ERROR_MESSAGE(L"Timed out waiting for registry mutex");
87 } else if (mutexStatus == WAIT_ABANDONED) {
88 // This isn't really an error for us. No one else is using the registry.
89 // This status code means that we are supposed to check our data for
90 // consistency, but there isn't really anything we can fix here.
91 // This is an indication that an agent crashed though, which is clearly an
92 // error, so log an error message.
93 LOG_ERROR_MESSAGE(L"Found abandoned registry mutex. Continuing...");
94 mLocked = true;
95 } else {
96 // The only other documented status code is WAIT_FAILED. In the case that
97 // we somehow get some other code, that is also an error.
98 LOG_ERROR_MESSAGE(L"Failed to wait on registry mutex: %#X",
99 GetLastError());
101 return mLocked;
104 bool IsLocked() { return mLocked; }
106 void Release() {
107 if (mLocked) {
108 if (mMutex.get() == nullptr) {
109 LOG_ERROR_MESSAGE(L"Unexpectedly missing registry mutex");
110 return;
112 BOOL success = ReleaseMutex(mMutex.get());
113 if (!success) {
114 LOG_ERROR_MESSAGE(L"Failed to release registry mutex");
116 mLocked = false;
121 // Returns true if the registry value name given is one of the
122 // install-directory-prefixed values used by the Windows Default Browser Agent.
123 // ex: "C:\Program Files\Mozilla Firefox|PreviousDefault"
124 // Returns true
125 // ex: "InitialNotificationShown"
126 // Returns false
127 static bool IsPrefixedValueName(const wchar_t* valueName) {
128 // Prefixed value names use '|' as a delimiter. None of the
129 // non-install-directory-prefixed value names contain one.
130 return wcschr(valueName, L'|') != nullptr;
133 static void RemoveAllRegistryEntries() {
134 mozilla::UniquePtr<wchar_t[]> installPath = mozilla::GetFullBinaryPath();
135 if (!PathRemoveFileSpecW(installPath.get())) {
136 return;
139 HKEY rawRegKey = nullptr;
140 if (ERROR_SUCCESS !=
141 RegOpenKeyExW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME, 0,
142 KEY_WRITE | KEY_QUERY_VALUE | KEY_WOW64_64KEY,
143 &rawRegKey)) {
144 return;
146 nsAutoRegKey regKey(rawRegKey);
148 DWORD maxValueNameLen = 0;
149 if (ERROR_SUCCESS != RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr,
150 nullptr, nullptr, nullptr, nullptr,
151 &maxValueNameLen, nullptr, nullptr,
152 nullptr)) {
153 return;
155 // The length that RegQueryInfoKeyW returns is without a terminator.
156 maxValueNameLen += 1;
158 mozilla::UniquePtr<wchar_t[]> valueName =
159 mozilla::MakeUnique<wchar_t[]>(maxValueNameLen);
161 DWORD valueIndex = 0;
162 // Set this to true if we encounter values in this key that are prefixed with
163 // different install directories, indicating that this key is still in use
164 // by other installs.
165 bool keyStillInUse = false;
167 while (true) {
168 DWORD valueNameLen = maxValueNameLen;
169 LSTATUS ls =
170 RegEnumValueW(regKey.get(), valueIndex, valueName.get(), &valueNameLen,
171 nullptr, nullptr, nullptr, nullptr);
172 if (ls != ERROR_SUCCESS) {
173 break;
176 if (!wcsnicmp(valueName.get(), installPath.get(),
177 wcslen(installPath.get()))) {
178 RegDeleteValueW(regKey.get(), valueName.get());
179 // Only increment the index if we did not delete this value, because if
180 // we did then the indexes of all the values after that one just got
181 // decremented, meaning the index we already have now refers to a value
182 // that we haven't looked at yet.
183 } else {
184 valueIndex++;
185 if (IsPrefixedValueName(valueName.get())) {
186 // If this is not one of the unprefixed value names, it must be one of
187 // the install-directory prefixed values.
188 keyStillInUse = true;
193 regKey.reset();
195 // If no other installs are using this key, remove it now.
196 if (!keyStillInUse) {
197 // Use RegDeleteTreeW to remove the cache as well, which is in subkey.
198 RegDeleteTreeW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME);
202 // This function adds a registry value with this format:
203 // <install-dir>|Installed=1
204 // RemoveAllRegistryEntries() determines whether the registry key is in use
205 // by other installations by checking for install-directory-prefixed value
206 // names. Although Firefox mirrors some preferences into install-directory-
207 // prefixed values, the WDBA no longer uses any prefixed values. Adding this one
208 // makes uninstallation work as expected slightly more reliably.
209 static void WriteInstallationRegistryEntry() {
210 mozilla::WindowsErrorResult<mozilla::Ok> result =
211 RegistrySetValueBool(IsPrefixed::Prefixed, L"Installed", true);
212 if (result.isErr()) {
213 LOG_ERROR_MESSAGE(L"Failed to write installation registry entry: %#X",
214 result.unwrapErr().AsHResult());
218 // Returns false (without setting aResult) if reading last run time failed.
219 static bool CheckIfAppRanRecently(bool* aResult) {
220 const ULONGLONG kTaskExpirationDays = 90;
221 const ULONGLONG kTaskExpirationSeconds = kTaskExpirationDays * 24 * 60 * 60;
223 MaybeQwordResult lastRunTimeResult =
224 RegistryGetValueQword(IsPrefixed::Prefixed, L"AppLastRunTime");
225 if (lastRunTimeResult.isErr()) {
226 return false;
228 mozilla::Maybe<ULONGLONG> lastRunTimeMaybe = lastRunTimeResult.unwrap();
229 if (!lastRunTimeMaybe.isSome()) {
230 return false;
233 ULONGLONG secondsSinceLastRunTime =
234 SecondsPassedSince(lastRunTimeMaybe.value());
236 *aResult = secondsSinceLastRunTime < kTaskExpirationSeconds;
237 return true;
240 // Use the macro to inject all of the definitions for nsISupports.
241 NS_IMPL_ISUPPORTS(DefaultAgent, nsIDefaultAgent)
243 NS_IMETHODIMP
244 DefaultAgent::RegisterTask(const nsAString& aUniqueToken) {
245 // We aren't actually going to check whether we got the mutex here.
246 // Ideally we would acquire it since registration might migrate registry
247 // entries. But it is preferable to ignore a mutex wait timeout here
248 // because:
249 // 1. Otherwise the task doesn't get registered at all
250 // 2. If another installation's agent is holding the mutex, it either
251 // is far enough out of date that it doesn't yet use the migrated
252 // values, or it already did the migration for us.
253 RegistryMutex regMutex;
254 regMutex.Acquire();
256 WriteInstallationRegistryEntry();
258 HRESULT hr =
259 default_agent::RegisterTask(PromiseFlatString(aUniqueToken).get());
260 return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
263 NS_IMETHODIMP
264 DefaultAgent::UpdateTask(const nsAString& aUniqueToken) {
265 // Not checking if we got the mutex for the same reason we didn't in
266 // register-task
267 RegistryMutex regMutex;
268 regMutex.Acquire();
270 WriteInstallationRegistryEntry();
272 HRESULT hr = default_agent::UpdateTask(PromiseFlatString(aUniqueToken).get());
273 return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
276 NS_IMETHODIMP
277 DefaultAgent::UnregisterTask(const nsAString& aUniqueToken) {
278 HRESULT hr = RemoveTasks(PromiseFlatString(aUniqueToken).get(),
279 WhichTasks::WdbaTaskOnly);
280 return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
283 NS_IMETHODIMP
284 DefaultAgent::Uninstall(const nsAString& aUniqueToken) {
285 // We aren't actually going to check whether we got the mutex here.
286 // Ideally we would acquire it since we are about to access the registry,
287 // so we would like to block simultaneous users of our registry key.
288 // But there are two reasons that it is preferable to ignore a mutex
289 // wait timeout here:
290 // 1. If we fail to uninstall our prefixed registry entries, the
291 // registry key containing them will never be removed, even when the
292 // last installation is uninstalled.
293 // 2. If we timed out waiting on the mutex, it implies that there are
294 // other installations. If there are other installations, there will
295 // be other prefixed registry entries. If there are other prefixed
296 // registry entries, we won't remove the whole key or touch the
297 // unprefixed entries during uninstallation. Therefore, we should
298 // be able to safely uninstall without stepping on anyone's toes.
299 RegistryMutex regMutex;
300 regMutex.Acquire();
302 RemoveAllRegistryEntries();
303 return NS_OK;
306 NS_IMETHODIMP
307 DefaultAgent::DoTask(const nsAString& aUniqueToken, const bool aForce) {
308 // Acquire() has a short timeout. Since this runs in the background, we
309 // could use a longer timeout in this situation. However, if another
310 // installation's agent is already running, it will update CurrentDefault,
311 // possibly send a ping, and possibly show a notification.
312 // Once all that has happened, there is no real reason to do it again. We
313 // only send one ping per day, so we aren't going to do that again. And
314 // the only time we ever show a second notification is 7 days after the
315 // first one, so we aren't going to do that again either.
316 // If the other process didn't take those actions, there is no reason that
317 // this process would take them.
318 // If the other process fails, this one will most likely fail for the same
319 // reason.
320 // So we'll just bail if we can't get the mutex quickly.
321 RegistryMutex regMutex;
322 if (!regMutex.Acquire()) {
323 return NS_ERROR_NOT_AVAILABLE;
326 // Check that Firefox ran recently, if not then stop here.
327 // Also stop if no timestamp was found, which most likely indicates
328 // that Firefox was not yet run.
329 bool ranRecently = false;
330 if (!aForce && (!CheckIfAppRanRecently(&ranRecently) || !ranRecently)) {
331 return NS_ERROR_FAILURE;
334 DefaultBrowserResult defaultBrowserResult = GetDefaultBrowserInfo();
335 DefaultBrowserInfo browserInfo{};
336 if (defaultBrowserResult.isOk()) {
337 browserInfo = defaultBrowserResult.unwrap();
338 } else {
339 browserInfo.currentDefaultBrowser = Browser::Error;
340 browserInfo.previousDefaultBrowser = Browser::Error;
343 DefaultPdfResult defaultPdfResult = GetDefaultPdfInfo();
344 DefaultPdfInfo pdfInfo{};
345 if (defaultPdfResult.isOk()) {
346 pdfInfo = defaultPdfResult.unwrap();
347 } else {
348 pdfInfo.currentDefaultPdf = PDFHandler::Error;
351 NotificationActivities activitiesPerformed;
352 // We block while waiting for the notification which prevents STA thread
353 // callbacks from running as the event loop won't run. Moving notification
354 // handling to an MTA thread prevents this conflict.
355 activitiesPerformed = MaybeShowNotification(
356 browserInfo, PromiseFlatString(aUniqueToken).get(), aForce);
358 HRESULT hr = SendDefaultAgentPing(browserInfo, pdfInfo, activitiesPerformed);
359 return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
362 NS_IMETHODIMP
363 DefaultAgent::AppRanRecently(bool* aRanRecently) {
364 bool ranRecently = false;
365 *aRanRecently = CheckIfAppRanRecently(&ranRecently) && ranRecently;
366 return NS_OK;
369 NS_IMETHODIMP
370 DefaultAgent::GetDefaultBrowser(nsAString& aDefaultBrowser) {
371 Browser browser = default_agent::GetDefaultBrowser();
372 aDefaultBrowser = NS_ConvertUTF8toUTF16(GetStringForBrowser(browser));
373 return NS_OK;
376 NS_IMETHODIMP
377 DefaultAgent::GetReplacePreviousDefaultBrowser(
378 const nsAString& aDefaultBrowser, nsAString& aPreviousDefaultBrowser) {
379 Browser browser =
380 GetBrowserFromString(std::string(NS_ConvertUTF16toUTF8(aDefaultBrowser)));
381 Browser previousBrowser =
382 default_agent::GetReplacePreviousDefaultBrowser(browser);
383 aPreviousDefaultBrowser =
384 NS_ConvertUTF8toUTF16(GetStringForBrowser(previousBrowser));
385 return NS_OK;
388 NS_IMETHODIMP
389 DefaultAgent::GetDefaultPdfHandler(nsAString& aDefaultPdfHandler) {
390 PDFHandler pdf = default_agent::GetDefaultPdfInfo()
391 .unwrapOr({PDFHandler::Error})
392 .currentDefaultPdf;
393 aDefaultPdfHandler = NS_ConvertUTF8toUTF16(GetStringForPDFHandler(pdf));
394 return NS_OK;
397 NS_IMETHODIMP
398 DefaultAgent::SendPing(const nsAString& aDefaultBrowser,
399 const nsAString& aPreviousDefaultBrowser,
400 const nsAString& aDefaultPdfHandler,
401 const nsAString& aNotificationShown,
402 const nsAString& aNotificationAction) {
403 DefaultBrowserInfo browserInfo = {
404 GetBrowserFromString(std::string(NS_ConvertUTF16toUTF8(aDefaultBrowser))),
405 GetBrowserFromString(
406 std::string(NS_ConvertUTF16toUTF8(aPreviousDefaultBrowser)))};
408 DefaultPdfInfo pdfInfo = {GetPDFHandlerFromString(
409 std::string(NS_ConvertUTF16toUTF8(aDefaultPdfHandler)))};
411 // The JS implementation has never supported the "two notification flow",
412 // i.e., displaying a followup notification.
413 NotificationShown shown = GetNotificationShownFromString(aNotificationShown);
414 NotificationAction action =
415 GetNotificationActionFromString(aNotificationAction);
416 NotificationActivities activitiesPerformed = {NotificationType::Initial,
417 shown, action};
419 HRESULT hr = SendDefaultAgentPing(browserInfo, pdfInfo, activitiesPerformed);
420 return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
423 NS_IMETHODIMP
424 DefaultAgent::SetDefaultBrowserUserChoice(
425 const nsAString& aAumid, const nsTArray<nsString>& aExtraFileExtensions) {
426 return default_agent::SetDefaultBrowserUserChoice(
427 PromiseFlatString(aAumid).get(), aExtraFileExtensions);
430 NS_IMETHODIMP
431 DefaultAgent::SetDefaultBrowserUserChoiceAsync(
432 const nsAString& aAumid, const nsTArray<nsString>& aExtraFileExtensions,
433 JSContext* aCx, dom::Promise** aPromise) {
434 if (!NS_IsMainThread()) {
435 return NS_ERROR_NOT_SAME_THREAD;
438 ErrorResult rv;
439 RefPtr<dom::Promise> promise =
440 dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
441 if (MOZ_UNLIKELY(rv.Failed())) {
442 return rv.StealNSResult();
445 // A holder to pass the promise through the background task and back to
446 // the main thread when finished.
447 auto promiseHolder = MakeRefPtr<nsMainThreadPtrHolder<dom::Promise>>(
448 "SetDefaultBrowserUserChoiceAsync promise", promise);
450 nsresult result = NS_DispatchBackgroundTask(
451 NS_NewRunnableFunction(
452 "SetDefaultBrowserUserChoiceAsync",
453 // Make a local copy of the aAudmid parameter which is a reference
454 // which will go out of scope
455 [aumid = nsString(aAumid), promiseHolder = std::move(promiseHolder),
456 aExtraFileExtensions =
457 CopyableTArray<nsString>(aExtraFileExtensions)] {
458 nsresult rv = default_agent::SetDefaultBrowserUserChoice(
459 PromiseFlatString(aumid).get(), aExtraFileExtensions);
461 NS_DispatchToMainThread(NS_NewRunnableFunction(
462 "SetDefaultBrowserUserChoiceAsync callback",
463 [rv, promiseHolder = std::move(promiseHolder)] {
464 dom::Promise* promise = promiseHolder.get()->get();
465 if (NS_SUCCEEDED(rv)) {
466 promise->MaybeResolveWithUndefined();
467 } else {
468 promise->MaybeReject(rv);
470 }));
472 NS_DISPATCH_EVENT_MAY_BLOCK);
474 promise.forget(aPromise);
475 return result;
478 NS_IMETHODIMP
479 DefaultAgent::SetDefaultExtensionHandlersUserChoice(
480 const nsAString& aAumid, const nsTArray<nsString>& aFileExtensions) {
481 return default_agent::SetDefaultExtensionHandlersUserChoice(
482 PromiseFlatString(aAumid).get(), aFileExtensions);
485 NS_IMETHODIMP
486 DefaultAgent::AgentDisabled(bool* aDisabled) {
487 *aDisabled = IsAgentDisabled();
488 return NS_OK;
491 } // namespace mozilla::default_agent