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/. */
8 #include <shlobj.h> // for SHChangeNotify and IApplicationAssociationRegistration
12 #include "mozilla/ArrayUtils.h"
13 #include "mozilla/CmdLineAndEnvUtils.h"
14 #include "mozilla/RefPtr.h"
15 #include "mozilla/UniquePtr.h"
16 #include "mozilla/WindowsVersion.h"
17 #include "mozilla/WinHeaderOnlyUtils.h"
18 #include "WindowsUserChoice.h"
19 #include "nsThreadUtils.h"
22 #include "SetDefaultBrowser.h"
24 namespace mozilla::default_agent
{
27 * The implementation for setting extension handlers by writing UserChoice.
29 * This is used by both SetDefaultBrowserUserChoice and
30 * SetDefaultExtensionHandlersUserChoice.
32 * @param aAumi The AUMI of the installation to set as default.
34 * @param aSid Current user's string SID
36 * @param aExtraFileExtensions Optional array of extra file association pairs to
37 * set as default, like `[ ".pdf", "FirefoxPDF" ]`.
39 * @returns NS_OK All associations set and checked
41 * NS_ERROR_WDBA_REJECTED UserChoice was set, but checking the default
42 * did not return our ProgID.
43 * NS_ERROR_FAILURE Failed to set at least one association.
45 static nsresult
SetDefaultExtensionHandlersUserChoiceImpl(
46 const wchar_t* aAumi
, const wchar_t* const aSid
,
47 const nsTArray
<nsString
>& aFileExtensions
);
49 static bool AddMillisecondsToSystemTime(SYSTEMTIME
& aSystemTime
,
50 ULONGLONG aIncrementMS
) {
52 ULARGE_INTEGER fileTimeInt
;
53 if (!::SystemTimeToFileTime(&aSystemTime
, &fileTime
)) {
56 fileTimeInt
.LowPart
= fileTime
.dwLowDateTime
;
57 fileTimeInt
.HighPart
= fileTime
.dwHighDateTime
;
59 // FILETIME is in units of 100ns.
60 fileTimeInt
.QuadPart
+= aIncrementMS
* 1000 * 10;
62 fileTime
.dwLowDateTime
= fileTimeInt
.LowPart
;
63 fileTime
.dwHighDateTime
= fileTimeInt
.HighPart
;
64 SYSTEMTIME tmpSystemTime
;
65 if (!::FileTimeToSystemTime(&fileTime
, &tmpSystemTime
)) {
69 aSystemTime
= tmpSystemTime
;
73 // Compare two SYSTEMTIMEs as FILETIME after clearing everything
75 static bool CheckEqualMinutes(SYSTEMTIME aSystemTime1
,
76 SYSTEMTIME aSystemTime2
) {
77 aSystemTime1
.wSecond
= 0;
78 aSystemTime1
.wMilliseconds
= 0;
80 aSystemTime2
.wSecond
= 0;
81 aSystemTime2
.wMilliseconds
= 0;
85 if (!::SystemTimeToFileTime(&aSystemTime1
, &fileTime1
) ||
86 !::SystemTimeToFileTime(&aSystemTime2
, &fileTime2
)) {
90 return (fileTime1
.dwLowDateTime
== fileTime2
.dwLowDateTime
) &&
91 (fileTime1
.dwHighDateTime
== fileTime2
.dwHighDateTime
);
94 static bool SetUserChoiceRegistry(const wchar_t* aExt
, const wchar_t* aProgID
,
95 mozilla::UniquePtr
<wchar_t[]> aHash
) {
96 auto assocKeyPath
= GetAssociationKeyPath(aExt
);
103 ls
= ::RegOpenKeyExW(HKEY_CURRENT_USER
, assocKeyPath
.get(), 0,
104 KEY_READ
| KEY_WRITE
, &rawAssocKey
);
105 if (ls
!= ERROR_SUCCESS
) {
106 LOG_ERROR(HRESULT_FROM_WIN32(ls
));
109 nsAutoRegKey
assocKey(rawAssocKey
);
111 // When Windows creates this key, it is read-only (Deny Set Value), so we need
112 // to delete it first.
113 // We don't set any similar special permissions.
114 ls
= ::RegDeleteKeyW(assocKey
.get(), L
"UserChoice");
115 if (ls
!= ERROR_SUCCESS
) {
116 LOG_ERROR(HRESULT_FROM_WIN32(ls
));
120 HKEY rawUserChoiceKey
;
121 ls
= ::RegCreateKeyExW(assocKey
.get(), L
"UserChoice", 0, nullptr,
122 0 /* options */, KEY_READ
| KEY_WRITE
,
123 0 /* security attributes */, &rawUserChoiceKey
,
125 if (ls
!= ERROR_SUCCESS
) {
126 LOG_ERROR(HRESULT_FROM_WIN32(ls
));
129 nsAutoRegKey
userChoiceKey(rawUserChoiceKey
);
131 DWORD progIdByteCount
= (::lstrlenW(aProgID
) + 1) * sizeof(wchar_t);
132 ls
= ::RegSetValueExW(userChoiceKey
.get(), L
"ProgID", 0, REG_SZ
,
133 reinterpret_cast<const unsigned char*>(aProgID
),
135 if (ls
!= ERROR_SUCCESS
) {
136 LOG_ERROR(HRESULT_FROM_WIN32(ls
));
140 DWORD hashByteCount
= (::lstrlenW(aHash
.get()) + 1) * sizeof(wchar_t);
141 ls
= ::RegSetValueExW(userChoiceKey
.get(), L
"Hash", 0, REG_SZ
,
142 reinterpret_cast<const unsigned char*>(aHash
.get()),
144 if (ls
!= ERROR_SUCCESS
) {
145 LOG_ERROR(HRESULT_FROM_WIN32(ls
));
153 * Set an association with a UserChoice key
155 * Removes the old key, creates a new one with ProgID and Hash set to
156 * enable a new asociation.
158 * @param aExt File type or protocol to associate
159 * @param aSid Current user's string SID
160 * @param aProgID ProgID to use for the asociation
161 * @param inMsix Are we running from in an msix package?
163 * @return true if successful, false on error.
165 static bool SetUserChoice(const wchar_t* aExt
, const wchar_t* aSid
,
166 const wchar_t* aProgID
, bool inMsix
) {
168 LOG_ERROR_MESSAGE(L
"SetUserChoice does not work on MSIX builds.");
172 SYSTEMTIME hashTimestamp
;
173 ::GetSystemTime(&hashTimestamp
);
174 auto hash
= GenerateUserChoiceHash(aExt
, aSid
, aProgID
, hashTimestamp
);
179 // The hash changes at the end of each minute, so check that the hash should
180 // be the same by the time we're done writing.
181 const ULONGLONG kWriteTimingThresholdMilliseconds
= 1000;
182 // Generating the hash could have taken some time, so start from now.
183 SYSTEMTIME writeEndTimestamp
;
184 ::GetSystemTime(&writeEndTimestamp
);
185 if (!AddMillisecondsToSystemTime(writeEndTimestamp
,
186 kWriteTimingThresholdMilliseconds
)) {
189 if (!CheckEqualMinutes(hashTimestamp
, writeEndTimestamp
)) {
191 L
"Hash is too close to expiration, sleeping until next hash.");
192 ::Sleep(kWriteTimingThresholdMilliseconds
* 2);
194 // For consistency, use the current time.
195 ::GetSystemTime(&hashTimestamp
);
196 hash
= GenerateUserChoiceHash(aExt
, aSid
, aProgID
, hashTimestamp
);
202 // We're outside of an MSIX package and can use the Win32 Registry API.
203 return SetUserChoiceRegistry(aExt
, aProgID
, std::move(hash
));
206 static bool VerifyUserDefault(const wchar_t* aExt
, const wchar_t* aProgID
) {
207 RefPtr
<IApplicationAssociationRegistration
> pAAR
;
208 HRESULT hr
= ::CoCreateInstance(
209 CLSID_ApplicationAssociationRegistration
, nullptr, CLSCTX_INPROC
,
210 IID_IApplicationAssociationRegistration
, getter_AddRefs(pAAR
));
216 wchar_t* rawRegisteredApp
;
217 bool isProtocol
= aExt
[0] != L
'.';
218 // Note: Checks AL_USER instead of AL_EFFECTIVE.
219 hr
= pAAR
->QueryCurrentDefault(aExt
,
220 isProtocol
? AT_URLPROTOCOL
: AT_FILEEXTENSION
,
221 AL_USER
, &rawRegisteredApp
);
223 if (hr
== HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION
)) {
224 LOG_ERROR_MESSAGE(L
"UserChoice ProgID %s for %s was rejected", aProgID
,
231 mozilla::UniquePtr
<wchar_t, mozilla::CoTaskMemFreeDeleter
> registeredApp(
234 if (::CompareStringOrdinal(registeredApp
.get(), -1, aProgID
, -1, FALSE
) !=
237 L
"Default was %s after writing ProgID %s to UserChoice for %s",
238 registeredApp
.get(), aProgID
, aExt
);
245 nsresult
SetDefaultBrowserUserChoice(
246 const wchar_t* aAumi
, const nsTArray
<nsString
>& aExtraFileExtensions
) {
247 // Verify that the implementation of UserChoice hashing has not changed by
248 // computing the current default hash and comparing with the existing value.
249 if (!CheckBrowserUserChoiceHashes()) {
250 LOG_ERROR_MESSAGE(L
"UserChoice Hash mismatch");
251 return NS_ERROR_WDBA_HASH_CHECK
;
254 if (!mozilla::IsWin10CreatorsUpdateOrLater()) {
255 LOG_ERROR_MESSAGE(L
"UserChoice hash matched, but Windows build is too old");
256 return NS_ERROR_WDBA_BUILD
;
259 auto sid
= GetCurrentUserStringSid();
261 return NS_ERROR_FAILURE
;
264 nsTArray
<nsString
> browserDefaults
= {
265 u
"https"_ns
, u
"FirefoxURL"_ns
, u
"http"_ns
, u
"FirefoxURL"_ns
,
266 u
".html"_ns
, u
"FirefoxHTML"_ns
, u
".htm"_ns
, u
"FirefoxHTML"_ns
};
268 browserDefaults
.AppendElements(aExtraFileExtensions
);
270 nsresult rv
= SetDefaultExtensionHandlersUserChoiceImpl(aAumi
, sid
.get(),
272 if (!NS_SUCCEEDED(rv
)) {
273 LOG_ERROR_MESSAGE(L
"Failed setting default with %s", aAumi
);
276 // Notify shell to refresh icons
277 ::SHChangeNotify(SHCNE_ASSOCCHANGED
, SHCNF_IDLIST
, nullptr, nullptr);
282 nsresult
SetDefaultExtensionHandlersUserChoice(
283 const wchar_t* aAumi
, const nsTArray
<nsString
>& aFileExtensions
) {
284 auto sid
= GetCurrentUserStringSid();
286 return NS_ERROR_FAILURE
;
289 nsresult rv
= SetDefaultExtensionHandlersUserChoiceImpl(aAumi
, sid
.get(),
291 if (!NS_SUCCEEDED(rv
)) {
292 LOG_ERROR_MESSAGE(L
"Failed setting default with %s", aAumi
);
295 // Notify shell to refresh icons
296 ::SHChangeNotify(SHCNE_ASSOCCHANGED
, SHCNF_IDLIST
, nullptr, nullptr);
301 nsresult
SetDefaultExtensionHandlersUserChoiceImpl(
302 const wchar_t* aAumi
, const wchar_t* const aSid
,
303 const nsTArray
<nsString
>& aFileExtensions
) {
306 GetCurrentPackageFullName(&pfnLen
, nullptr) != APPMODEL_ERROR_NO_PACKAGE
;
309 // MSIX packages can not meaningfully modify the registry keys related to
311 return NS_ERROR_FAILURE
;
314 for (size_t i
= 0; i
+ 1 < aFileExtensions
.Length(); i
+= 2) {
315 const wchar_t* extraFileExtension
= aFileExtensions
[i
].get();
316 const wchar_t* extraProgIDRoot
= aFileExtensions
[i
+ 1].get();
317 // Formatting the ProgID here prevents using this helper to target arbitrary
319 mozilla::UniquePtr
<wchar_t[]> extraProgID
;
321 nsresult rv
= GetMsixProgId(extraFileExtension
, extraProgID
);
323 LOG_ERROR_MESSAGE(L
"Failed to retrieve MSIX progID for %s",
328 extraProgID
= FormatProgID(extraProgIDRoot
, aAumi
);
329 if (!CheckProgIDExists(extraProgID
.get())) {
330 LOG_ERROR_MESSAGE(L
"ProgID %s not found", extraProgID
.get());
331 return NS_ERROR_WDBA_NO_PROGID
;
335 if (!SetUserChoice(extraFileExtension
, aSid
, extraProgID
.get(), inMsix
)) {
336 return NS_ERROR_FAILURE
;
339 if (!VerifyUserDefault(extraFileExtension
, extraProgID
.get())) {
340 return NS_ERROR_WDBA_REJECTED
;
347 } // namespace mozilla::default_agent