Bug 1867190 - Add prefs for PHC probablities r=glandium
[gecko.git] / toolkit / mozapps / defaultagent / SetDefaultBrowser.cpp
blob8bc0889e6765590c6bc94a40242add3f6feb59a7
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/. */
6 #include <windows.h>
7 #include <appmodel.h>
8 #include <shlobj.h> // for SHChangeNotify and IApplicationAssociationRegistration
9 #include <functional>
10 #include <timeapi.h>
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"
21 #include "EventLog.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
40 * successfully.
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) {
51 FILETIME fileTime;
52 ULARGE_INTEGER fileTimeInt;
53 if (!::SystemTimeToFileTime(&aSystemTime, &fileTime)) {
54 return false;
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)) {
66 return false;
69 aSystemTime = tmpSystemTime;
70 return true;
73 // Compare two SYSTEMTIMEs as FILETIME after clearing everything
74 // below minutes.
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;
83 FILETIME fileTime1;
84 FILETIME fileTime2;
85 if (!::SystemTimeToFileTime(&aSystemTime1, &fileTime1) ||
86 !::SystemTimeToFileTime(&aSystemTime2, &fileTime2)) {
87 return false;
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);
97 if (!assocKeyPath) {
98 return false;
101 LSTATUS ls;
102 HKEY rawAssocKey;
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));
107 return false;
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));
117 return false;
120 HKEY rawUserChoiceKey;
121 ls = ::RegCreateKeyExW(assocKey.get(), L"UserChoice", 0, nullptr,
122 0 /* options */, KEY_READ | KEY_WRITE,
123 0 /* security attributes */, &rawUserChoiceKey,
124 nullptr);
125 if (ls != ERROR_SUCCESS) {
126 LOG_ERROR(HRESULT_FROM_WIN32(ls));
127 return false;
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),
134 progIdByteCount);
135 if (ls != ERROR_SUCCESS) {
136 LOG_ERROR(HRESULT_FROM_WIN32(ls));
137 return false;
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()),
143 hashByteCount);
144 if (ls != ERROR_SUCCESS) {
145 LOG_ERROR(HRESULT_FROM_WIN32(ls));
146 return false;
149 return true;
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) {
167 if (inMsix) {
168 LOG_ERROR_MESSAGE(L"SetUserChoice does not work on MSIX builds.");
169 return false;
172 SYSTEMTIME hashTimestamp;
173 ::GetSystemTime(&hashTimestamp);
174 auto hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp);
175 if (!hash) {
176 return false;
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)) {
187 return false;
189 if (!CheckEqualMinutes(hashTimestamp, writeEndTimestamp)) {
190 LOG_ERROR_MESSAGE(
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);
197 if (!hash) {
198 return false;
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));
211 if (FAILED(hr)) {
212 LOG_ERROR(hr);
213 return false;
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);
222 if (FAILED(hr)) {
223 if (hr == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) {
224 LOG_ERROR_MESSAGE(L"UserChoice ProgID %s for %s was rejected", aProgID,
225 aExt);
226 } else {
227 LOG_ERROR(hr);
229 return false;
231 mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> registeredApp(
232 rawRegisteredApp);
234 if (::CompareStringOrdinal(registeredApp.get(), -1, aProgID, -1, FALSE) !=
235 CSTR_EQUAL) {
236 LOG_ERROR_MESSAGE(
237 L"Default was %s after writing ProgID %s to UserChoice for %s",
238 registeredApp.get(), aProgID, aExt);
239 return false;
242 return true;
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();
260 if (!sid) {
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(),
271 browserDefaults);
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);
279 return rv;
282 nsresult SetDefaultExtensionHandlersUserChoice(
283 const wchar_t* aAumi, const nsTArray<nsString>& aFileExtensions) {
284 auto sid = GetCurrentUserStringSid();
285 if (!sid) {
286 return NS_ERROR_FAILURE;
289 nsresult rv = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(),
290 aFileExtensions);
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);
298 return rv;
301 nsresult SetDefaultExtensionHandlersUserChoiceImpl(
302 const wchar_t* aAumi, const wchar_t* const aSid,
303 const nsTArray<nsString>& aFileExtensions) {
304 UINT32 pfnLen = 0;
305 bool inMsix =
306 GetCurrentPackageFullName(&pfnLen, nullptr) != APPMODEL_ERROR_NO_PACKAGE;
308 if (inMsix) {
309 // MSIX packages can not meaningfully modify the registry keys related to
310 // default handlers
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
318 // ProgIDs.
319 mozilla::UniquePtr<wchar_t[]> extraProgID;
320 if (inMsix) {
321 nsresult rv = GetMsixProgId(extraFileExtension, extraProgID);
322 if (NS_FAILED(rv)) {
323 LOG_ERROR_MESSAGE(L"Failed to retrieve MSIX progID for %s",
324 extraFileExtension);
325 return rv;
327 } else {
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;
344 return NS_OK;
347 } // namespace mozilla::default_agent