Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit f22ce8f

Browse files
committed
Bug 1703578 - Part 2: Add WDBA command to set default browser. r=bytesized
Depends on D113426 Differential Revision: https://phabricator.services.mozilla.com/D113427
1 parent 8587291 commit f22ce8f

6 files changed

Lines changed: 370 additions & 3 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2+
/* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5+
6+
#include <windows.h>
7+
#include <shlobj.h> // for SHChangeNotify and IApplicationAssociationRegistration
8+
9+
#include "mozilla/ArrayUtils.h"
10+
#include "mozilla/RefPtr.h"
11+
#include "mozilla/UniquePtr.h"
12+
#include "mozilla/WinHeaderOnlyUtils.h"
13+
#include "WindowsUserChoice.h"
14+
15+
#include "EventLog.h"
16+
#include "SetDefaultBrowser.h"
17+
18+
static bool AddMillisecondsToSystemTime(SYSTEMTIME& aSystemTime,
19+
ULONGLONG aIncrementMS) {
20+
FILETIME fileTime;
21+
ULARGE_INTEGER fileTimeInt;
22+
if (!::SystemTimeToFileTime(&aSystemTime, &fileTime)) {
23+
return false;
24+
}
25+
fileTimeInt.LowPart = fileTime.dwLowDateTime;
26+
fileTimeInt.HighPart = fileTime.dwHighDateTime;
27+
28+
// FILETIME is in units of 100ns.
29+
fileTimeInt.QuadPart += aIncrementMS * 1000 * 10;
30+
31+
fileTime.dwLowDateTime = fileTimeInt.LowPart;
32+
fileTime.dwHighDateTime = fileTimeInt.HighPart;
33+
SYSTEMTIME tmpSystemTime;
34+
if (!::FileTimeToSystemTime(&fileTime, &tmpSystemTime)) {
35+
return false;
36+
}
37+
38+
aSystemTime = tmpSystemTime;
39+
return true;
40+
}
41+
42+
// Compare two SYSTEMTIMEs as FILETIME after clearing everything
43+
// below minutes.
44+
static bool CheckEqualMinutes(SYSTEMTIME aSystemTime1,
45+
SYSTEMTIME aSystemTime2) {
46+
aSystemTime1.wSecond = 0;
47+
aSystemTime1.wMilliseconds = 0;
48+
49+
aSystemTime2.wSecond = 0;
50+
aSystemTime2.wMilliseconds = 0;
51+
52+
FILETIME fileTime1;
53+
FILETIME fileTime2;
54+
if (!::SystemTimeToFileTime(&aSystemTime1, &fileTime1) ||
55+
!::SystemTimeToFileTime(&aSystemTime2, &fileTime2)) {
56+
return false;
57+
}
58+
59+
return (fileTime1.dwLowDateTime == fileTime2.dwLowDateTime) &&
60+
(fileTime1.dwHighDateTime == fileTime2.dwHighDateTime);
61+
}
62+
63+
/*
64+
* Set an association with a UserChoice key
65+
*
66+
* Removes the old key, creates a new one with ProgID and Hash set to
67+
* enable a new asociation.
68+
*
69+
* @param aExt File type or protocol to associate
70+
* @param aSid Current user's string SID
71+
* @param aProgID ProgID to use for the asociation
72+
*
73+
* @return true if successful, false on error.
74+
*/
75+
static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid,
76+
const wchar_t* aProgID) {
77+
SYSTEMTIME hashTimestamp;
78+
::GetSystemTime(&hashTimestamp);
79+
auto hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp);
80+
if (!hash) {
81+
return false;
82+
}
83+
84+
// The hash changes at the end of each minute, so check that the hash should
85+
// be the same by the time we're done writing.
86+
const ULONGLONG kWriteTimingThresholdMilliseconds = 100;
87+
// Generating the hash could have taken some time, so start from now.
88+
SYSTEMTIME writeEndTimestamp;
89+
::GetSystemTime(&writeEndTimestamp);
90+
if (!AddMillisecondsToSystemTime(writeEndTimestamp,
91+
kWriteTimingThresholdMilliseconds)) {
92+
return false;
93+
}
94+
if (!CheckEqualMinutes(hashTimestamp, writeEndTimestamp)) {
95+
LOG_ERROR_MESSAGE(
96+
L"Hash is too close to expiration, sleeping until next hash.");
97+
::Sleep(kWriteTimingThresholdMilliseconds * 2);
98+
99+
// For consistency, use the current time.
100+
::GetSystemTime(&hashTimestamp);
101+
hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp);
102+
if (!hash) {
103+
return false;
104+
}
105+
}
106+
107+
auto assocKeyPath = GetAssociationKeyPath(aExt);
108+
if (!assocKeyPath) {
109+
return false;
110+
}
111+
112+
LSTATUS ls;
113+
HKEY rawAssocKey;
114+
ls = ::RegOpenKeyExW(HKEY_CURRENT_USER, assocKeyPath.get(), 0,
115+
KEY_READ | KEY_WRITE, &rawAssocKey);
116+
if (ls != ERROR_SUCCESS) {
117+
LOG_ERROR(HRESULT_FROM_WIN32(ls));
118+
return false;
119+
}
120+
nsAutoRegKey assocKey(rawAssocKey);
121+
122+
// When Windows creates this key, it is read-only (Deny Set Value), so we need
123+
// to delete it first.
124+
// We don't set any similar special permissions.
125+
ls = ::RegDeleteKeyW(assocKey.get(), L"UserChoice");
126+
if (ls != ERROR_SUCCESS) {
127+
LOG_ERROR(HRESULT_FROM_WIN32(ls));
128+
return false;
129+
}
130+
131+
HKEY rawUserChoiceKey;
132+
ls = ::RegCreateKeyExW(assocKey.get(), L"UserChoice", 0, nullptr,
133+
0 /* options */, KEY_READ | KEY_WRITE,
134+
0 /* security attributes */, &rawUserChoiceKey,
135+
nullptr);
136+
if (ls != ERROR_SUCCESS) {
137+
LOG_ERROR(HRESULT_FROM_WIN32(ls));
138+
return false;
139+
}
140+
nsAutoRegKey userChoiceKey(rawUserChoiceKey);
141+
142+
DWORD progIdByteCount = (::lstrlenW(aProgID) + 1) * sizeof(wchar_t);
143+
ls = ::RegSetValueExW(userChoiceKey.get(), L"ProgID", 0, REG_SZ,
144+
reinterpret_cast<const unsigned char*>(aProgID),
145+
progIdByteCount);
146+
if (ls != ERROR_SUCCESS) {
147+
LOG_ERROR(HRESULT_FROM_WIN32(ls));
148+
return false;
149+
}
150+
151+
DWORD hashByteCount = (::lstrlenW(hash.get()) + 1) * sizeof(wchar_t);
152+
ls = ::RegSetValueExW(userChoiceKey.get(), L"Hash", 0, REG_SZ,
153+
reinterpret_cast<const unsigned char*>(hash.get()),
154+
hashByteCount);
155+
if (ls != ERROR_SUCCESS) {
156+
LOG_ERROR(HRESULT_FROM_WIN32(ls));
157+
return false;
158+
}
159+
160+
return true;
161+
}
162+
163+
static bool VerifyUserDefault(const wchar_t* aExt, const wchar_t* aProgID) {
164+
RefPtr<IApplicationAssociationRegistration> pAAR;
165+
HRESULT hr = ::CoCreateInstance(
166+
CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
167+
IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
168+
if (FAILED(hr)) {
169+
LOG_ERROR(hr);
170+
return false;
171+
}
172+
173+
wchar_t* rawRegisteredApp;
174+
bool isProtocol = aExt[0] != L'.';
175+
// Note: Checks AL_USER instead of AL_EFFECTIVE.
176+
hr = pAAR->QueryCurrentDefault(aExt,
177+
isProtocol ? AT_URLPROTOCOL : AT_FILEEXTENSION,
178+
AL_USER, &rawRegisteredApp);
179+
if (FAILED(hr)) {
180+
if (hr == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) {
181+
LOG_ERROR_MESSAGE(L"UserChoice ProgID %s for %s was rejected", aProgID,
182+
aExt);
183+
} else {
184+
LOG_ERROR(hr);
185+
}
186+
return false;
187+
}
188+
mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> registeredApp(
189+
rawRegisteredApp);
190+
191+
if (::CompareStringOrdinal(registeredApp.get(), -1, aProgID, -1, FALSE) !=
192+
CSTR_EQUAL) {
193+
LOG_ERROR_MESSAGE(
194+
L"Default was %s after writing ProgID %s to UserChoice for %s",
195+
registeredApp.get(), aProgID, aExt);
196+
return false;
197+
}
198+
199+
return true;
200+
}
201+
202+
HRESULT SetDefaultBrowserUserChoice(const wchar_t* aAumi) {
203+
auto urlProgID = FormatProgID(L"FirefoxURL", aAumi);
204+
if (!CheckProgIDExists(urlProgID.get())) {
205+
LOG_ERROR_MESSAGE(L"ProgID %s not found", urlProgID.get());
206+
return MOZ_E_NO_PROGID;
207+
}
208+
209+
auto htmlProgID = FormatProgID(L"FirefoxHTML", aAumi);
210+
if (!CheckProgIDExists(htmlProgID.get())) {
211+
LOG_ERROR_MESSAGE(L"ProgID %s not found", htmlProgID.get());
212+
return MOZ_E_NO_PROGID;
213+
}
214+
215+
if (!CheckBrowserUserChoiceHashes()) {
216+
LOG_ERROR_MESSAGE(L"UserChoice Hash mismatch");
217+
return MOZ_E_HASH_CHECK;
218+
}
219+
220+
auto sid = GetCurrentUserStringSid();
221+
if (!sid) {
222+
return E_FAIL;
223+
}
224+
225+
bool ok = true;
226+
bool defaultRejected = false;
227+
228+
struct {
229+
const wchar_t* ext;
230+
const wchar_t* progID;
231+
} associations[] = {{L"https", urlProgID.get()},
232+
{L"http", urlProgID.get()},
233+
{L".html", htmlProgID.get()},
234+
{L".htm", htmlProgID.get()}};
235+
for (int i = 0; i < mozilla::ArrayLength(associations); ++i) {
236+
if (!SetUserChoice(associations[i].ext, sid.get(),
237+
associations[i].progID)) {
238+
ok = false;
239+
break;
240+
} else if (!VerifyUserDefault(associations[i].ext,
241+
associations[i].progID)) {
242+
defaultRejected = true;
243+
ok = false;
244+
break;
245+
}
246+
}
247+
248+
// Notify shell to refresh icons
249+
::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
250+
251+
if (!ok) {
252+
LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi);
253+
if (defaultRejected) {
254+
return MOZ_E_REJECTED;
255+
}
256+
return E_FAIL;
257+
} else {
258+
return S_OK;
259+
}
260+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2+
/* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5+
6+
#ifndef DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__
7+
#define DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__
8+
9+
/*
10+
* Set the default browser by writing the UserChoice registry keys.
11+
*
12+
* This sets the associations for https, http, .html, and .htm.
13+
*
14+
* When the agent is run with set-default-browser-user-choice,
15+
* the exit code is the result of this function.
16+
*
17+
* @param aAumi The AUMI of the installation to set as default.
18+
*
19+
* @return S_OK All associations set and checked successfully.
20+
* MOZ_E_NO_PROGID The ProgID classes had not been registered.
21+
* MOZ_E_HASH_CHECK The existing UserChoice Hash could not be verified.
22+
* MOZ_E_REJECTED UserChoice was set, but checking the default
23+
* did not return our ProgID.
24+
* E_FAIL other failure
25+
*/
26+
HRESULT SetDefaultBrowserUserChoice(const wchar_t* aAumi);
27+
28+
/*
29+
* Additional HRESULT error codes from SetDefaultBrowserUserChoice
30+
*
31+
* 0x20000000 is set to put these in the customer-defined range.
32+
*/
33+
const HRESULT MOZ_E_NO_PROGID = 0xa0000001L;
34+
const HRESULT MOZ_E_HASH_CHECK = 0xa0000002L;
35+
const HRESULT MOZ_E_REJECTED = 0xa0000003L;
36+
37+
#endif // DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__

toolkit/mozapps/defaultagent/main.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "Registry.h"
2121
#include "RemoteSettings.h"
2222
#include "ScheduledTask.h"
23+
#include "SetDefaultBrowser.h"
2324
#include "Telemetry.h"
2425

2526
// The AGENT_REGKEY_NAME is dependent on MOZ_APP_VENDOR and MOZ_APP_BASENAME,
@@ -250,6 +251,8 @@ static bool CheckIfAppRanRecently(bool* aResult) {
250251
// Actually performs the default agent task, which currently means generating
251252
// and sending our telemetry ping and possibly showing a notification to the
252253
// user if their browser has switched from Firefox to Edge with Blink.
254+
// set-default-browser-user-choice [app-user-model-id]
255+
// Set the default browser via the UserChoice registry keys.
253256
int wmain(int argc, wchar_t** argv) {
254257
if (argc < 2 || !argv[1]) {
255258
return E_INVALIDARG;
@@ -386,6 +389,12 @@ int wmain(int argc, wchar_t** argv) {
386389
MaybeShowNotification(browserInfo, argv[2]);
387390

388391
return SendDefaultBrowserPing(browserInfo, activitiesPerformed);
392+
} else if (!wcscmp(argv[1], L"set-default-browser-user-choice")) {
393+
if (argc < 3 || !argv[2]) {
394+
return E_INVALIDARG;
395+
}
396+
397+
return SetDefaultBrowserUserChoice(argv[2]);
389398
} else {
390399
return E_INVALIDARG;
391400
}

toolkit/mozapps/defaultagent/moz.build

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ UNIFIED_SOURCES += [
2323
"Registry.cpp",
2424
"RemoteSettings.cpp",
2525
"ScheduledTask.cpp",
26+
"SetDefaultBrowser.cpp",
2627
"Telemetry.cpp",
2728
]
2829

2930
SOURCES += [
3031
"/browser/components/shell/WindowsDefaultBrowser.cpp",
32+
"/browser/components/shell/WindowsUserChoice.cpp",
3133
"/other-licenses/nsis/Contrib/CityHash/cityhash/city.cpp",
3234
"/third_party/WinToast/wintoastlib.cpp",
3335
"/toolkit/mozapps/update/common/readstrings.cpp",
@@ -50,7 +52,9 @@ LOCAL_INCLUDES += [
5052

5153
OS_LIBS += [
5254
"advapi32",
55+
"bcrypt",
5356
"comsupp",
57+
"crypt32",
5458
"kernel32",
5559
"netapi32",
5660
"ole32",
@@ -70,9 +74,9 @@ DEFINES["IMPL_MFBT"] = True
7074
DEFINES["UNICODE"] = True
7175
DEFINES["_UNICODE"] = True
7276

73-
# If defines are added to this list that are required by the Cache or its
74-
# dependencies (Registry, EventLog, common), tests/gtest/moz.build will need to
75-
# be updated as well.
77+
# If defines are added to this list that are required by the Cache,
78+
# SetDefaultBrowser, or their dependencies (Registry, EventLog, common),
79+
# tests/gtest/moz.build will need to be updated as well.
7680
for var in ("MOZ_APP_BASENAME", "MOZ_APP_DISPLAYNAME", "MOZ_APP_VENDOR"):
7781
DEFINES[var] = '"%s"' % CONFIG[var]
7882

0 commit comments

Comments
 (0)