From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp | 365 +++++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp (limited to 'toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp') diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp new file mode 100644 index 0000000000..047d8b7a31 --- /dev/null +++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp @@ -0,0 +1,365 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include +#include // for SHChangeNotify and IApplicationAssociationRegistration + +#include "mozilla/ArrayUtils.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "WindowsUserChoice.h" + +#include "EventLog.h" +#include "SetDefaultBrowser.h" + +/* + * The implementation for setting extension handlers by writing UserChoice. + * + * This is used by both SetDefaultBrowserUserChoice and + * SetDefaultExtensionHandlersUserChoice. + * + * @param aAumi The AUMI of the installation to set as default. + * + * @param aSid Current user's string SID + * + * @param aFileExtensions Optional null-terminated list of file association + * pairs to set as default, like `{ L".pdf", "FirefoxPDF", nullptr }`. + * + * @returns S_OK All associations set and checked successfully. + * MOZ_E_REJECTED UserChoice was set, but checking the default did not + * return our ProgID. + * E_FAIL Failed to set at least one association. + */ +static HRESULT SetDefaultExtensionHandlersUserChoiceImpl( + const wchar_t* aAumi, const wchar_t* const aSid, + const wchar_t* const* aFileExtensions); + +static bool AddMillisecondsToSystemTime(SYSTEMTIME& aSystemTime, + ULONGLONG aIncrementMS) { + FILETIME fileTime; + ULARGE_INTEGER fileTimeInt; + if (!::SystemTimeToFileTime(&aSystemTime, &fileTime)) { + return false; + } + fileTimeInt.LowPart = fileTime.dwLowDateTime; + fileTimeInt.HighPart = fileTime.dwHighDateTime; + + // FILETIME is in units of 100ns. + fileTimeInt.QuadPart += aIncrementMS * 1000 * 10; + + fileTime.dwLowDateTime = fileTimeInt.LowPart; + fileTime.dwHighDateTime = fileTimeInt.HighPart; + SYSTEMTIME tmpSystemTime; + if (!::FileTimeToSystemTime(&fileTime, &tmpSystemTime)) { + return false; + } + + aSystemTime = tmpSystemTime; + return true; +} + +// Compare two SYSTEMTIMEs as FILETIME after clearing everything +// below minutes. +static bool CheckEqualMinutes(SYSTEMTIME aSystemTime1, + SYSTEMTIME aSystemTime2) { + aSystemTime1.wSecond = 0; + aSystemTime1.wMilliseconds = 0; + + aSystemTime2.wSecond = 0; + aSystemTime2.wMilliseconds = 0; + + FILETIME fileTime1; + FILETIME fileTime2; + if (!::SystemTimeToFileTime(&aSystemTime1, &fileTime1) || + !::SystemTimeToFileTime(&aSystemTime2, &fileTime2)) { + return false; + } + + return (fileTime1.dwLowDateTime == fileTime2.dwLowDateTime) && + (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; + } + } + + auto assocKeyPath = GetAssociationKeyPath(aExt); + if (!assocKeyPath) { + return false; + } + + LSTATUS ls; + HKEY rawAssocKey; + ls = ::RegOpenKeyExW(HKEY_CURRENT_USER, assocKeyPath.get(), 0, + KEY_READ | KEY_WRITE, &rawAssocKey); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + nsAutoRegKey assocKey(rawAssocKey); + + // When Windows creates this key, it is read-only (Deny Set Value), so we need + // to delete it first. + // We don't set any similar special permissions. + ls = ::RegDeleteKeyW(assocKey.get(), L"UserChoice"); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + + HKEY rawUserChoiceKey; + ls = ::RegCreateKeyExW(assocKey.get(), L"UserChoice", 0, nullptr, + 0 /* options */, KEY_READ | KEY_WRITE, + 0 /* security attributes */, &rawUserChoiceKey, + nullptr); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + nsAutoRegKey userChoiceKey(rawUserChoiceKey); + + DWORD progIdByteCount = (::lstrlenW(aProgID) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"ProgID", 0, REG_SZ, + reinterpret_cast(aProgID), + progIdByteCount); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + + DWORD hashByteCount = (::lstrlenW(hash.get()) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"Hash", 0, REG_SZ, + reinterpret_cast(hash.get()), + hashByteCount); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + + return true; +} + +static bool VerifyUserDefault(const wchar_t* aExt, const wchar_t* aProgID) { + RefPtr pAAR; + HRESULT hr = ::CoCreateInstance( + CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC, + IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR)); + if (FAILED(hr)) { + LOG_ERROR(hr); + return false; + } + + wchar_t* rawRegisteredApp; + bool isProtocol = aExt[0] != L'.'; + // Note: Checks AL_USER instead of AL_EFFECTIVE. + hr = pAAR->QueryCurrentDefault(aExt, + isProtocol ? AT_URLPROTOCOL : AT_FILEEXTENSION, + AL_USER, &rawRegisteredApp); + if (FAILED(hr)) { + if (hr == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { + LOG_ERROR_MESSAGE(L"UserChoice ProgID %s for %s was rejected", aProgID, + aExt); + } else { + LOG_ERROR(hr); + } + return false; + } + mozilla::UniquePtr registeredApp( + rawRegisteredApp); + + if (::CompareStringOrdinal(registeredApp.get(), -1, aProgID, -1, FALSE) != + CSTR_EQUAL) { + LOG_ERROR_MESSAGE( + L"Default was %s after writing ProgID %s to UserChoice for %s", + registeredApp.get(), aProgID, aExt); + return false; + } + + return true; +} + +HRESULT SetDefaultBrowserUserChoice( + const wchar_t* aAumi, const wchar_t* const* aExtraFileExtensions) { + auto urlProgID = FormatProgID(L"FirefoxURL", aAumi); + if (!CheckProgIDExists(urlProgID.get())) { + LOG_ERROR_MESSAGE(L"ProgID %s not found", urlProgID.get()); + return MOZ_E_NO_PROGID; + } + + auto htmlProgID = FormatProgID(L"FirefoxHTML", aAumi); + if (!CheckProgIDExists(htmlProgID.get())) { + LOG_ERROR_MESSAGE(L"ProgID %s not found", htmlProgID.get()); + return MOZ_E_NO_PROGID; + } + + auto pdfProgID = FormatProgID(L"FirefoxPDF", aAumi); + if (!CheckProgIDExists(pdfProgID.get())) { + LOG_ERROR_MESSAGE(L"ProgID %s not found", pdfProgID.get()); + return MOZ_E_NO_PROGID; + } + + if (!CheckBrowserUserChoiceHashes()) { + LOG_ERROR_MESSAGE(L"UserChoice Hash mismatch"); + return MOZ_E_HASH_CHECK; + } + + if (!mozilla::IsWin10CreatorsUpdateOrLater()) { + LOG_ERROR_MESSAGE(L"UserChoice hash matched, but Windows build is too old"); + return MOZ_E_BUILD; + } + + auto sid = GetCurrentUserStringSid(); + if (!sid) { + return E_FAIL; + } + + 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) { + HRESULT hr = SetDefaultExtensionHandlersUserChoiceImpl( + aAumi, sid.get(), aExtraFileExtensions); + if (hr == MOZ_E_REJECTED) { + ok = false; + defaultRejected = true; + } else if (hr == E_FAIL) { + ok = false; + } + } + + // 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 MOZ_E_REJECTED; + } + return E_FAIL; + } + + return S_OK; +} + +HRESULT SetDefaultExtensionHandlersUserChoice( + const wchar_t* aAumi, const wchar_t* const* aFileExtensions) { + auto sid = GetCurrentUserStringSid(); + if (!sid) { + return E_FAIL; + } + + bool ok = true; + bool defaultRejected = false; + + HRESULT hr = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(), + aFileExtensions); + if (hr == MOZ_E_REJECTED) { + ok = false; + defaultRejected = true; + } else if (hr == E_FAIL) { + ok = false; + } + + // 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 MOZ_E_REJECTED; + } + return E_FAIL; + } + + return S_OK; +} + +HRESULT SetDefaultExtensionHandlersUserChoiceImpl( + const wchar_t* aAumi, const wchar_t* const aSid, + const wchar_t* const* aFileExtensions) { + const wchar_t* const* extraFileExtension = aFileExtensions; + const wchar_t* const* extraProgIDRoot = aFileExtensions + 1; + while (extraFileExtension && *extraFileExtension && extraProgIDRoot && + *extraProgIDRoot) { + // Formatting the ProgID here prevents using this helper to target arbitrary + // ProgIDs. + auto extraProgID = FormatProgID(*extraProgIDRoot, aAumi); + + if (!SetUserChoice(*extraFileExtension, aSid, extraProgID.get())) { + return E_FAIL; + } + + if (!VerifyUserDefault(*extraFileExtension, extraProgID.get())) { + return MOZ_E_REJECTED; + } + + extraFileExtension += 2; + extraProgIDRoot += 2; + } + + return S_OK; +} -- cgit v1.2.3