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/. */
13 #include "nsAutoRef.h"
15 #include "nsProxyRelease.h"
16 #include "nsWindowsHelpers.h"
20 #include "DefaultBrowser.h"
21 #include "DefaultPDF.h"
23 #include "Notification.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
57 RegistryMutex() : mMutex(nullptr), mLocked(false) {}
60 // nsAutoHandle will take care of closing the mutex's handle.
63 // Returns true on success, false on failure.
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());
82 WaitForSingleObject(mMutex
.get(), REGISTRY_MUTEX_TIMEOUT_MS
);
83 if (mutexStatus
== WAIT_OBJECT_0
) {
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...");
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",
104 bool IsLocked() { return mLocked
; }
108 if (mMutex
.get() == nullptr) {
109 LOG_ERROR_MESSAGE(L
"Unexpectedly missing registry mutex");
112 BOOL success
= ReleaseMutex(mMutex
.get());
114 LOG_ERROR_MESSAGE(L
"Failed to release registry mutex");
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"
125 // ex: "InitialNotificationShown"
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())) {
139 HKEY rawRegKey
= nullptr;
141 RegOpenKeyExW(HKEY_CURRENT_USER
, AGENT_REGKEY_NAME
, 0,
142 KEY_WRITE
| KEY_QUERY_VALUE
| KEY_WOW64_64KEY
,
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,
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;
168 DWORD valueNameLen
= maxValueNameLen
;
170 RegEnumValueW(regKey
.get(), valueIndex
, valueName
.get(), &valueNameLen
,
171 nullptr, nullptr, nullptr, nullptr);
172 if (ls
!= ERROR_SUCCESS
) {
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.
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;
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()) {
228 mozilla::Maybe
<ULONGLONG
> lastRunTimeMaybe
= lastRunTimeResult
.unwrap();
229 if (!lastRunTimeMaybe
.isSome()) {
233 ULONGLONG secondsSinceLastRunTime
=
234 SecondsPassedSince(lastRunTimeMaybe
.value());
236 *aResult
= secondsSinceLastRunTime
< kTaskExpirationSeconds
;
240 // Use the macro to inject all of the definitions for nsISupports.
241 NS_IMPL_ISUPPORTS(DefaultAgent
, nsIDefaultAgent
)
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
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
;
256 WriteInstallationRegistryEntry();
259 default_agent::RegisterTask(PromiseFlatString(aUniqueToken
).get());
260 return SUCCEEDED(hr
) ? NS_OK
: NS_ERROR_FAILURE
;
264 DefaultAgent::UpdateTask(const nsAString
& aUniqueToken
) {
265 // Not checking if we got the mutex for the same reason we didn't in
267 RegistryMutex regMutex
;
270 WriteInstallationRegistryEntry();
272 HRESULT hr
= default_agent::UpdateTask(PromiseFlatString(aUniqueToken
).get());
273 return SUCCEEDED(hr
) ? NS_OK
: NS_ERROR_FAILURE
;
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
;
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
;
302 RemoveAllRegistryEntries();
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
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();
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();
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
;
363 DefaultAgent::AppRanRecently(bool* aRanRecently
) {
364 bool ranRecently
= false;
365 *aRanRecently
= CheckIfAppRanRecently(&ranRecently
) && ranRecently
;
370 DefaultAgent::GetDefaultBrowser(nsAString
& aDefaultBrowser
) {
371 Browser browser
= default_agent::GetDefaultBrowser();
372 aDefaultBrowser
= NS_ConvertUTF8toUTF16(GetStringForBrowser(browser
));
377 DefaultAgent::GetReplacePreviousDefaultBrowser(
378 const nsAString
& aDefaultBrowser
, nsAString
& aPreviousDefaultBrowser
) {
380 GetBrowserFromString(std::string(NS_ConvertUTF16toUTF8(aDefaultBrowser
)));
381 Browser previousBrowser
=
382 default_agent::GetReplacePreviousDefaultBrowser(browser
);
383 aPreviousDefaultBrowser
=
384 NS_ConvertUTF8toUTF16(GetStringForBrowser(previousBrowser
));
389 DefaultAgent::GetDefaultPdfHandler(nsAString
& aDefaultPdfHandler
) {
390 PDFHandler pdf
= default_agent::GetDefaultPdfInfo()
391 .unwrapOr({PDFHandler::Error
})
393 aDefaultPdfHandler
= NS_ConvertUTF8toUTF16(GetStringForPDFHandler(pdf
));
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
,
419 HRESULT hr
= SendDefaultAgentPing(browserInfo
, pdfInfo
, activitiesPerformed
);
420 return SUCCEEDED(hr
) ? NS_OK
: NS_ERROR_FAILURE
;
424 DefaultAgent::SetDefaultBrowserUserChoice(
425 const nsAString
& aAumid
, const nsTArray
<nsString
>& aExtraFileExtensions
) {
426 return default_agent::SetDefaultBrowserUserChoice(
427 PromiseFlatString(aAumid
).get(), aExtraFileExtensions
);
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
;
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();
468 promise
->MaybeReject(rv
);
472 NS_DISPATCH_EVENT_MAY_BLOCK
);
474 promise
.forget(aPromise
);
479 DefaultAgent::SetDefaultExtensionHandlersUserChoice(
480 const nsAString
& aAumid
, const nsTArray
<nsString
>& aFileExtensions
) {
481 return default_agent::SetDefaultExtensionHandlersUserChoice(
482 PromiseFlatString(aAumid
).get(), aFileExtensions
);
486 DefaultAgent::AgentDisabled(bool* aDisabled
) {
487 *aDisabled
= IsAgentDisabled();
491 } // namespace mozilla::default_agent