From 24147f62347447b6553c4ff68402da7f729e04c0 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Sat, 4 Nov 2023 02:49:46 +0000 Subject: [PATCH] Bug 1818418 - Use `reg.exe` to implement MSIX 1-click set-to-default. r=mhughes This approach uses `reg.exe` to delete and recreate the relevant HKCU registry key-value pairs. Differential Revision: https://phabricator.services.mozilla.com/D170717 --- .../components/shell/WindowsUserChoice.cpp | 53 +++ browser/components/shell/WindowsUserChoice.h | 13 + .../shell/nsWindowsShellService.cpp | 39 ++- python/mozbuild/mozbuild/repackaging/msix.py | 5 +- .../defaultagent/SetDefaultBrowser.cpp | 320 +++++++++++------- 5 files changed, 300 insertions(+), 130 deletions(-) diff --git a/browser/components/shell/WindowsUserChoice.cpp b/browser/components/shell/WindowsUserChoice.cpp index 4d6f24704a16..71c9595d4264 100644 --- a/browser/components/shell/WindowsUserChoice.cpp +++ b/browser/components/shell/WindowsUserChoice.cpp @@ -20,13 +20,16 @@ */ #include +#include // for GetPackageFamilyName #include // for ConvertSidToStringSidW #include // for CryptoAPI base64 #include // for CNG MD5 #include // for NT_SUCCESS() +#include "ErrorList.h" #include "mozilla/ArrayUtils.h" #include "mozilla/UniquePtr.h" +#include "nsDebug.h" #include "nsWindowsHelpers.h" #include "WindowsUserChoice.h" @@ -420,3 +423,53 @@ bool CheckProgIDExists(const wchar_t* aProgID) { ::RegCloseKey(key); return true; } + +nsresult GetMsixProgId(const wchar_t* assoc, UniquePtr& aProgId) { + // Retrieve the registry path to the package from registry path: + // clang-format off + // HKEY_CLASSES_ROOT\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\[Package Full Name]\App\Capabilities\[FileAssociations | URLAssociations]\[File | URL] + // clang-format on + + UINT32 pfnLen = 0; + LONG rv = GetCurrentPackageFullName(&pfnLen, nullptr); + NS_ENSURE_TRUE(rv != APPMODEL_ERROR_NO_PACKAGE, NS_ERROR_FAILURE); + + auto pfn = mozilla::MakeUnique(pfnLen); + rv = GetCurrentPackageFullName(&pfnLen, pfn.get()); + NS_ENSURE_TRUE(rv == ERROR_SUCCESS, NS_ERROR_FAILURE); + + const wchar_t* assocSuffix; + if (assoc[0] == L'.') { + // File association. + assocSuffix = LR"(App\Capabilities\FileAssociations)"; + } else { + // URL association. + assocSuffix = LR"(App\Capabilities\URLAssociations)"; + } + + const wchar_t* assocPathFmt = + LR"(Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\%s\%s)"; + int assocPathLen = _scwprintf(assocPathFmt, pfn.get(), assocSuffix); + assocPathLen += 1; // _scwprintf does not include the terminator + + auto assocPath = MakeUnique(assocPathLen); + _snwprintf_s(assocPath.get(), assocPathLen, _TRUNCATE, assocPathFmt, + pfn.get(), assocSuffix); + + LSTATUS ls; + + // Retrieve the package association's ProgID, always in the form `AppX[32 hash + // characters]`. + const size_t appxProgIdLen = 37; + auto progId = MakeUnique(appxProgIdLen); + DWORD progIdLen = appxProgIdLen * sizeof(wchar_t); + ls = ::RegGetValueW(HKEY_CLASSES_ROOT, assocPath.get(), assoc, RRF_RT_REG_SZ, + nullptr, (LPBYTE)progId.get(), &progIdLen); + if (ls != ERROR_SUCCESS) { + return NS_ERROR_WDBA_NO_PROGID; + } + + aProgId.swap(progId); + + return NS_OK; +} diff --git a/browser/components/shell/WindowsUserChoice.h b/browser/components/shell/WindowsUserChoice.h index d7e887b66739..5a6d562c4977 100644 --- a/browser/components/shell/WindowsUserChoice.h +++ b/browser/components/shell/WindowsUserChoice.h @@ -100,4 +100,17 @@ mozilla::UniquePtr FormatProgID(const wchar_t* aProgIDBase, */ bool CheckProgIDExists(const wchar_t* aProgID); +/* + * Get the ProgID registered by Windows for the given association. + * + * The MSIX `AppManifest.xml` declares supported protocols and file + * type associations. Upon installation, Windows generates + * corresponding ProgIDs for them, of the form `AppX*`. This function + * retrieves those generated ProgIDs (from the Windows registry). + * + * @return ProgID. + */ +nsresult GetMsixProgId(const wchar_t* assoc, + mozilla::UniquePtr& aProgId); + #endif // SHELL_WINDOWSUSERCHOICE_H__ diff --git a/browser/components/shell/nsWindowsShellService.cpp b/browser/components/shell/nsWindowsShellService.cpp index ec082c637621..71ba1c9a93c8 100644 --- a/browser/components/shell/nsWindowsShellService.cpp +++ b/browser/components/shell/nsWindowsShellService.cpp @@ -300,10 +300,41 @@ nsWindowsShellService::CheckAllProgIDsExist(bool* aResult) { if (!mozilla::widget::WinTaskbar::GetAppUserModelID(aumid)) { return NS_OK; } - *aResult = - CheckProgIDExists(FormatProgID(L"FirefoxURL", aumid.get()).get()) && - CheckProgIDExists(FormatProgID(L"FirefoxHTML", aumid.get()).get()) && - CheckProgIDExists(FormatProgID(L"FirefoxPDF", aumid.get()).get()); + + if (widget::WinUtils::HasPackageIdentity()) { + UniquePtr extraProgID; + nsresult rv; + bool result = true; + + // "FirefoxURL". + rv = GetMsixProgId(L"https", extraProgID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + result = result && CheckProgIDExists(extraProgID.get()); + + // "FirefoxHTML". + rv = GetMsixProgId(L".htm", extraProgID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + result = result && CheckProgIDExists(extraProgID.get()); + + // "FirefoxPDF". + rv = GetMsixProgId(L".pdf", extraProgID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + result = result && CheckProgIDExists(extraProgID.get()); + + *aResult = result; + } else { + *aResult = + CheckProgIDExists(FormatProgID(L"FirefoxURL", aumid.get()).get()) && + CheckProgIDExists(FormatProgID(L"FirefoxHTML", aumid.get()).get()) && + CheckProgIDExists(FormatProgID(L"FirefoxPDF", aumid.get()).get()); + } + return NS_OK; } diff --git a/python/mozbuild/mozbuild/repackaging/msix.py b/python/mozbuild/mozbuild/repackaging/msix.py index 707096c49998..762a33f1d1d2 100644 --- a/python/mozbuild/mozbuild/repackaging/msix.py +++ b/python/mozbuild/mozbuild/repackaging/msix.py @@ -198,9 +198,8 @@ def get_branding(use_official, topsrcdir, build_app, finder, log=None): conf_vars = mozpath.join(topsrcdir, build_app, "confvars.sh") def conf_vars_value(key): - lines = open(conf_vars).readlines() + lines = [line.strip() for line in open(conf_vars).readlines()] for line in lines: - line = line.strip() if line and line[0] == "#": continue if key not in line: @@ -833,7 +832,7 @@ def _sign_msix_win(output, force, log, verbose): else: thumbprint = None - if not thumbprint: + if force or not thumbprint: thumbprint = ( powershell( ( diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp index e848fd909e56..71bfe46c09c4 100644 --- a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp +++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp @@ -4,9 +4,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include +#include #include // for SHChangeNotify and IApplicationAssociationRegistration #include "mozilla/ArrayUtils.h" +#include "mozilla/CmdLineAndEnvUtils.h" #include "mozilla/RefPtr.h" #include "mozilla/UniquePtr.h" #include "mozilla/WindowsVersion.h" @@ -86,50 +88,8 @@ static bool CheckEqualMinutes(SYSTEMTIME aSystemTime1, (fileTime1.dwHighDateTime == fileTime2.dwHighDateTime); } -/* - * Set an association with a UserChoice key - * - * Removes the old key, creates a new one with ProgID and Hash set to - * enable a new asociation. - * - * @param aExt File type or protocol to associate - * @param aSid Current user's string SID - * @param aProgID ProgID to use for the asociation - * - * @return true if successful, false on error. - */ -static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid, - const wchar_t* aProgID) { - SYSTEMTIME hashTimestamp; - ::GetSystemTime(&hashTimestamp); - auto hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp); - if (!hash) { - return false; - } - - // The hash changes at the end of each minute, so check that the hash should - // be the same by the time we're done writing. - const ULONGLONG kWriteTimingThresholdMilliseconds = 100; - // Generating the hash could have taken some time, so start from now. - SYSTEMTIME writeEndTimestamp; - ::GetSystemTime(&writeEndTimestamp); - if (!AddMillisecondsToSystemTime(writeEndTimestamp, - kWriteTimingThresholdMilliseconds)) { - return false; - } - if (!CheckEqualMinutes(hashTimestamp, writeEndTimestamp)) { - LOG_ERROR_MESSAGE( - L"Hash is too close to expiration, sleeping until next hash."); - ::Sleep(kWriteTimingThresholdMilliseconds * 2); - - // For consistency, use the current time. - ::GetSystemTime(&hashTimestamp); - hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp); - if (!hash) { - return false; - } - } - +static bool SetUserChoiceRegistry(const wchar_t* aExt, const wchar_t* aProgID, + mozilla::UniquePtr aHash) { auto assocKeyPath = GetAssociationKeyPath(aExt); if (!assocKeyPath) { return false; @@ -174,9 +134,9 @@ static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid, return false; } - DWORD hashByteCount = (::lstrlenW(hash.get()) + 1) * sizeof(wchar_t); + DWORD hashByteCount = (::lstrlenW(aHash.get()) + 1) * sizeof(wchar_t); ls = ::RegSetValueExW(userChoiceKey.get(), L"Hash", 0, REG_SZ, - reinterpret_cast(hash.get()), + reinterpret_cast(aHash.get()), hashByteCount); if (ls != ERROR_SUCCESS) { LOG_ERROR(HRESULT_FROM_WIN32(ls)); @@ -186,6 +146,162 @@ static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid, return true; } +static bool LaunchReg(int aArgsLength, const wchar_t* const* aArgs) { + mozilla::UniquePtr regPath = + mozilla::MakeUnique(MAX_PATH + 1); + if (!ConstructSystem32Path(L"reg.exe", regPath.get(), MAX_PATH + 1)) { + LOG_ERROR_MESSAGE(L"Failed to construct path to reg.exe"); + return false; + } + + const wchar_t* regArgs[] = {regPath.get()}; + mozilla::UniquePtr regCmdLine(mozilla::MakeCommandLine( + mozilla::ArrayLength(regArgs), const_cast(regArgs), + aArgsLength, const_cast(aArgs))); + + PROCESS_INFORMATION pi; + STARTUPINFOW si = {sizeof(si)}; + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + if (!::CreateProcessW(regPath.get(), regCmdLine.get(), nullptr, nullptr, + FALSE, 0, nullptr, nullptr, &si, &pi)) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return false; + } + + nsAutoHandle process(pi.hProcess); + nsAutoHandle mainThread(pi.hThread); + + DWORD exitCode; + if (::WaitForSingleObject(process.get(), INFINITE) == WAIT_OBJECT_0 && + ::GetExitCodeProcess(process.get(), &exitCode)) { + // N.b.: `reg.exe` returns 0 (unchanged) or 2 (changed) on success. + bool success = (exitCode == 0 || exitCode == 2); + if (!success) { + LOG_ERROR_MESSAGE(L"%s returned failure exitCode %d", regCmdLine.get(), + exitCode); + } + return success; + } + + return false; +} + +static bool SetUserChoiceCommand(const wchar_t* aExt, const wchar_t* aProgID, + const wchar_t* aHash) { + auto assocKeyPath = GetAssociationKeyPath(aExt); + if (!assocKeyPath) { + return false; + } + + const wchar_t* formatString = L"HKCU\\%s\\UserChoice"; + int bufferSize = _scwprintf(formatString, assocKeyPath.get()); + ++bufferSize; // Extra character for terminating null + mozilla::UniquePtr userChoiceKeyPath = + mozilla::MakeUnique(bufferSize); + _snwprintf_s(userChoiceKeyPath.get(), bufferSize, _TRUNCATE, formatString, + assocKeyPath.get()); + + const wchar_t* deleteArgs[] = { + L"DELETE", + userChoiceKeyPath.get(), + L"/F", + }; + if (!LaunchReg(mozilla::ArrayLength(deleteArgs), deleteArgs)) { + LOG_ERROR_MESSAGE(L"Failed to reg.exe DELETE; ignoring and continuing."); + } + + // Like REG ADD [ROOT\]RegKey /V ValueName [/T DataType] [/S Separator] [/D + // Data] [/F] [/reg:32] [/reg:64] + + const wchar_t* progIDArgs[] = { + L"ADD", userChoiceKeyPath.get(), + L"/F", L"/V", + L"ProgID", L"/T", + L"REG_SZ", L"/D", + aProgID, + }; + + if (!LaunchReg(mozilla::ArrayLength(progIDArgs), progIDArgs)) { + // LaunchReg will have logged an error message already. + return false; + } + + const wchar_t* hashArgs[] = { + L"ADD", userChoiceKeyPath.get(), + L"/F", L"/V", + L"Hash", L"/T", + L"REG_SZ", L"/D", + aHash, + }; + if (!LaunchReg(mozilla::ArrayLength(hashArgs), hashArgs)) { + // LaunchReg will have logged an error message already. + return false; + } + + return true; +} + +/* + * Set an association with a UserChoice key + * + * Removes the old key, creates a new one with ProgID and Hash set to + * enable a new asociation. + * + * @param aExt File type or protocol to associate + * @param aSid Current user's string SID + * @param aProgID ProgID to use for the asociation + * + * @return true if successful, false on error. + */ +static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid, + const wchar_t* aProgID) { + // This might be slow to query, so do it before generating timestamps and + // hashes. + UINT32 pfnLen = 0; + bool inMsix = + GetCurrentPackageFullName(&pfnLen, nullptr) != APPMODEL_ERROR_NO_PACKAGE; + + SYSTEMTIME hashTimestamp; + ::GetSystemTime(&hashTimestamp); + auto hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp); + if (!hash) { + return false; + } + + // The hash changes at the end of each minute, so check that the hash should + // be the same by the time we're done writing. + const ULONGLONG kWriteTimingThresholdMilliseconds = 1000; + // Generating the hash could have taken some time, so start from now. + SYSTEMTIME writeEndTimestamp; + ::GetSystemTime(&writeEndTimestamp); + if (!AddMillisecondsToSystemTime(writeEndTimestamp, + kWriteTimingThresholdMilliseconds)) { + return false; + } + if (!CheckEqualMinutes(hashTimestamp, writeEndTimestamp)) { + LOG_ERROR_MESSAGE( + L"Hash is too close to expiration, sleeping until next hash."); + ::Sleep(kWriteTimingThresholdMilliseconds * 2); + + // For consistency, use the current time. + ::GetSystemTime(&hashTimestamp); + hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp); + if (!hash) { + return false; + } + } + + if (inMsix) { + // We're in an MSIX package, thus need to use reg.exe. + return SetUserChoiceCommand(aExt, aProgID, hash.get()); + } else { + // We're outside of an MSIX package and can use the Win32 Registry API. + return SetUserChoiceRegistry(aExt, aProgID, std::move(hash)); + } +} + static bool VerifyUserDefault(const wchar_t* aExt, const wchar_t* aProgID) { RefPtr pAAR; HRESULT hr = ::CoCreateInstance( @@ -227,29 +343,19 @@ static bool VerifyUserDefault(const wchar_t* aExt, const wchar_t* aProgID) { nsresult SetDefaultBrowserUserChoice( const wchar_t* aAumi, const nsTArray& aExtraFileExtensions) { - auto urlProgID = FormatProgID(L"FirefoxURL", aAumi); - if (!CheckProgIDExists(urlProgID.get())) { - LOG_ERROR_MESSAGE(L"ProgID %s not found", urlProgID.get()); - return NS_ERROR_WDBA_NO_PROGID; - } - - auto htmlProgID = FormatProgID(L"FirefoxHTML", aAumi); - if (!CheckProgIDExists(htmlProgID.get())) { - LOG_ERROR_MESSAGE(L"ProgID %s not found", htmlProgID.get()); - return NS_ERROR_WDBA_NO_PROGID; - } - - auto pdfProgID = FormatProgID(L"FirefoxPDF", aAumi); - if (!CheckProgIDExists(pdfProgID.get())) { - LOG_ERROR_MESSAGE(L"ProgID %s not found", pdfProgID.get()); - return NS_ERROR_WDBA_NO_PROGID; - } - + // Verify that the implementation of UserChoice hashing has not changed by + // computing the current default hash and comparing with the existing value. if (!CheckBrowserUserChoiceHashes()) { LOG_ERROR_MESSAGE(L"UserChoice Hash mismatch"); return NS_ERROR_WDBA_HASH_CHECK; } + nsTArray browserDefaults = { + u"https"_ns, u"FirefoxURL"_ns, u"http"_ns, u"FirefoxURL"_ns, + u".html"_ns, u"FirefoxHTML"_ns, u".htm"_ns, u"FirefoxHTML"_ns}; + + browserDefaults.AppendElements(aExtraFileExtensions); + if (!mozilla::IsWin10CreatorsUpdateOrLater()) { LOG_ERROR_MESSAGE(L"UserChoice hash matched, but Windows build is too old"); return NS_ERROR_WDBA_BUILD; @@ -260,52 +366,16 @@ nsresult SetDefaultBrowserUserChoice( return NS_ERROR_FAILURE; } - bool ok = true; - bool defaultRejected = false; - - struct { - const wchar_t* ext; - const wchar_t* progID; - } associations[] = {{L"https", urlProgID.get()}, - {L"http", urlProgID.get()}, - {L".html", htmlProgID.get()}, - {L".htm", htmlProgID.get()}}; - for (size_t i = 0; i < mozilla::ArrayLength(associations); ++i) { - if (!SetUserChoice(associations[i].ext, sid.get(), - associations[i].progID)) { - ok = false; - break; - } else if (!VerifyUserDefault(associations[i].ext, - associations[i].progID)) { - defaultRejected = true; - ok = false; - break; - } - } - - if (ok) { - nsresult rv = SetDefaultExtensionHandlersUserChoiceImpl( - aAumi, sid.get(), aExtraFileExtensions); - if (rv == NS_ERROR_WDBA_REJECTED) { - ok = false; - defaultRejected = true; - } else if (rv == NS_ERROR_FAILURE) { - ok = false; - } + nsresult rv = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(), + browserDefaults); + if (!NS_SUCCEEDED(rv)) { + LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi); } // Notify shell to refresh icons ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); - if (!ok) { - LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi); - if (defaultRejected) { - return NS_ERROR_WDBA_REJECTED; - } - return NS_ERROR_FAILURE; - } - - return NS_OK; + return rv; } nsresult SetDefaultExtensionHandlersUserChoice( @@ -315,35 +385,25 @@ nsresult SetDefaultExtensionHandlersUserChoice( return NS_ERROR_FAILURE; } - bool ok = true; - bool defaultRejected = false; - nsresult rv = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(), aFileExtensions); - if (rv == NS_ERROR_WDBA_REJECTED) { - ok = false; - defaultRejected = true; - } else if (rv == NS_ERROR_FAILURE) { - ok = false; + if (!NS_SUCCEEDED(rv)) { + LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi); } // Notify shell to refresh icons ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); - if (!ok) { - LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi); - if (defaultRejected) { - return NS_ERROR_WDBA_REJECTED; - } - return NS_ERROR_FAILURE; - } - - return NS_OK; + return rv; } nsresult SetDefaultExtensionHandlersUserChoiceImpl( const wchar_t* aAumi, const wchar_t* const aSid, const nsTArray& aFileExtensions) { + UINT32 pfnLen = 0; + bool inMsix = + GetCurrentPackageFullName(&pfnLen, nullptr) != APPMODEL_ERROR_NO_PACKAGE; + for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) { const wchar_t* extraFileExtension = PromiseFlatString(aFileExtensions[i]).get(); @@ -351,7 +411,21 @@ nsresult SetDefaultExtensionHandlersUserChoiceImpl( PromiseFlatString(aFileExtensions[i + 1]).get(); // Formatting the ProgID here prevents using this helper to target arbitrary // ProgIDs. - auto extraProgID = FormatProgID(extraProgIDRoot, aAumi); + UniquePtr extraProgID; + if (inMsix) { + nsresult rv = GetMsixProgId(extraFileExtension, extraProgID); + if (NS_FAILED(rv)) { + LOG_ERROR_MESSAGE(L"Failed to retrieve MSIX progID for %s", + extraFileExtension); + return rv; + } + } else { + extraProgID = FormatProgID(extraProgIDRoot, aAumi); + if (!CheckProgIDExists(extraProgID.get())) { + LOG_ERROR_MESSAGE(L"ProgID %s not found", extraProgID.get()); + return NS_ERROR_WDBA_NO_PROGID; + } + } if (!SetUserChoice(extraFileExtension, aSid, extraProgID.get())) { return NS_ERROR_FAILURE;