summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp')
-rw-r--r--toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp393
1 files changed, 387 insertions, 6 deletions
diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
index 8bc0889e67..6d829ff045 100644
--- a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
+++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
@@ -6,17 +6,16 @@
#include <windows.h>
#include <appmodel.h>
#include <shlobj.h> // for SHChangeNotify and IApplicationAssociationRegistration
-#include <functional>
#include <timeapi.h>
#include "mozilla/ArrayUtils.h"
#include "mozilla/CmdLineAndEnvUtils.h"
#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/WindowsVersion.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#include "WindowsUserChoice.h"
-#include "nsThreadUtils.h"
#include "EventLog.h"
#include "SetDefaultBrowser.h"
@@ -70,6 +69,77 @@ static bool AddMillisecondsToSystemTime(SYSTEMTIME& aSystemTime,
return true;
}
+/*
+ * Takes two times: the start time and the current time, and returns the number
+ * of seconds left before the current time hits the next minute from the start
+ * time. Used to check if we are within the same minute as the start time and
+ * how much time we have left to perform an operation in the same minute.
+ *
+ * Used with user choice hashes, which have to be written to the registry
+ * in the same minute as they are generated.
+ *
+ * Example 1:
+ * operationStartTime - 10m 20s 800ms
+ * currentTime - 10m 22s 0ms
+ * The next minute is 11, so the return value is 11m - 10m 22s 0ms, converted to
+ * milliseconds.
+ *
+ * Example 2:
+ * operationStartTime - 10m 59s 800ms
+ * currentTime - 11m 0s 0ms
+ * The next minute is 11, but the minute the operation started on was 10, so the
+ * time to the next minute is 0 (because the current time is already at the next
+ * minute).
+ *
+ * @param operationStartTime
+ * @param currentTime
+ *
+ * @returns the number of milliseconds left from the current time to the
+ * next minute from operationStartTime, or zero if the currentTime is already at
+ * the next minute or greater
+ */
+static WORD GetMillisecondsToNextMinute(SYSTEMTIME operationStartTime,
+ SYSTEMTIME currentTime) {
+ SYSTEMTIME operationStartTimeMinute = operationStartTime;
+ SYSTEMTIME currentTimeMinute = currentTime;
+
+ // Zero out the seconds and milliseconds so that we can confirm they are the
+ // same minutes
+ operationStartTimeMinute.wSecond = 0;
+ operationStartTimeMinute.wMilliseconds = 0;
+
+ currentTimeMinute.wSecond = 0;
+ currentTimeMinute.wMilliseconds = 0;
+
+ // Convert to a 64 bit value so we can compare them directly
+ FILETIME fileTime1;
+ FILETIME fileTime2;
+ if (!::SystemTimeToFileTime(&operationStartTimeMinute, &fileTime1) ||
+ !::SystemTimeToFileTime(&currentTimeMinute, &fileTime2)) {
+ // Error: report that there is 0 milliseconds till the next minute
+ return 0;
+ }
+
+ // The minutes for both times have to be the same, so confirm that they are,
+ // and if they aren't, return 0 milliseconds to indicate that we're already
+ // not on the minute that operationStartTime was on
+ if ((fileTime1.dwLowDateTime != fileTime2.dwLowDateTime) ||
+ (fileTime1.dwHighDateTime != fileTime2.dwHighDateTime)) {
+ return 0;
+ }
+
+ // The minutes are the same; determine the number of milliseconds left until
+ // the next minute
+ const WORD secondsToMilliseconds = 1000;
+ const WORD minutesToSeconds = 60;
+
+ // 1 minute converted to milliseconds - (the current second converted to
+ // milliseconds + the current milliseconds)
+ return (1 * minutesToSeconds * secondsToMilliseconds) -
+ ((currentTime.wSecond * secondsToMilliseconds) +
+ currentTime.wMilliseconds);
+}
+
// Compare two SYSTEMTIMEs as FILETIME after clearing everything
// below minutes.
static bool CheckEqualMinutes(SYSTEMTIME aSystemTime1,
@@ -149,6 +219,75 @@ static bool SetUserChoiceRegistry(const wchar_t* aExt, const wchar_t* aProgID,
return true;
}
+struct LaunchExeErr {};
+using LaunchExeResult =
+ mozilla::Result<std::tuple<DWORD, mozilla::UniquePtr<wchar_t[]>>,
+ LaunchExeErr>;
+
+static LaunchExeResult LaunchExecutable(const wchar_t* exePath, int aArgsLength,
+ const wchar_t* const* aArgs) {
+ const wchar_t* args[] = {exePath};
+ mozilla::UniquePtr<wchar_t[]> cmdLine(mozilla::MakeCommandLine(
+ mozilla::ArrayLength(args), const_cast<wchar_t**>(args), aArgsLength,
+ const_cast<wchar_t**>(aArgs)));
+
+ PROCESS_INFORMATION pi;
+ STARTUPINFOW si = {sizeof(si)};
+ si.dwFlags = STARTF_USESHOWWINDOW;
+ si.wShowWindow = SW_HIDE;
+
+ if (!::CreateProcessW(exePath, cmdLine.get(), nullptr, nullptr, FALSE, 0,
+ nullptr, nullptr, &si, &pi)) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG_ERROR(hr);
+ return Err(LaunchExeErr());
+ }
+
+ nsAutoHandle process(pi.hProcess);
+ nsAutoHandle mainThread(pi.hThread);
+
+ DWORD exitCode;
+ if (::WaitForSingleObject(process.get(), INFINITE) == WAIT_OBJECT_0 &&
+ ::GetExitCodeProcess(process.get(), &exitCode)) {
+ return std::make_tuple(exitCode, std::move(cmdLine));
+ }
+
+ return Err(LaunchExeErr());
+}
+
+static bool LaunchPowershell(const wchar_t* command,
+ const wchar_t* powershellPath) {
+ const wchar_t* args[] = {
+ L"-NoProfile", // ensure nothing is monkeying with powershell
+ L"-c",
+ command,
+ };
+
+ return LaunchExecutable(powershellPath, mozilla::ArrayLength(args), args)
+ .map([](auto result) {
+ auto& [exitCode, regCmdLine] = result;
+ bool success = (exitCode == 0);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"%s returned failure exitCode %d",
+ regCmdLine.get(), exitCode);
+ }
+ return success;
+ })
+ .unwrapOr(false);
+}
+
+static bool FindPowershell(mozilla::UniquePtr<wchar_t[]>& powershellPath) {
+ auto exePath = mozilla::MakeUnique<wchar_t[]>(MAX_PATH + 1);
+ if (!ConstructSystem32Path(L"WindowsPowershell\\v1.0\\powershell.exe",
+ exePath.get(), MAX_PATH + 1)) {
+ LOG_ERROR_MESSAGE(L"Failed to construct path to powershell.exe");
+ return false;
+ }
+
+ powershellPath.swap(exePath);
+ return true;
+}
+
/*
* Set an association with a UserChoice key
*
@@ -165,7 +304,9 @@ static bool SetUserChoiceRegistry(const wchar_t* aExt, const wchar_t* aProgID,
static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid,
const wchar_t* aProgID, bool inMsix) {
if (inMsix) {
- LOG_ERROR_MESSAGE(L"SetUserChoice does not work on MSIX builds.");
+ LOG_ERROR_MESSAGE(
+ L"SetUserChoice should not be called on MSIX builds. Call "
+ L"SetDefaultExtensionHandlersUserChoiceImplMsix instead.");
return false;
}
@@ -298,6 +439,247 @@ nsresult SetDefaultExtensionHandlersUserChoice(
return rv;
}
+/*
+ * Takes the list of file extension pairs and fills a list of program ids for
+ * each pair.
+ *
+ * @param aFileExtensions array of file association pairs to
+ * set as default, like `[ ".pdf", "FirefoxPDF" ]`.
+ */
+static nsresult GenerateProgramIDs(const nsTArray<nsString>& aFileExtensions,
+ nsTArray<nsString>& progIDs) {
+ for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) {
+ const wchar_t* fileExtension = aFileExtensions[i].get();
+
+ // Formatting the ProgID here prevents using this helper to target arbitrary
+ // ProgIDs.
+ mozilla::UniquePtr<wchar_t[]> progID;
+ nsresult rv = GetMsixProgId(fileExtension, progID);
+ if (NS_FAILED(rv)) {
+ LOG_ERROR_MESSAGE(L"Failed to retrieve MSIX progID for %s",
+ fileExtension);
+ return rv;
+ }
+
+ progIDs.AppendElement(nsString(progID.get()));
+ }
+
+ return NS_OK;
+}
+
+/*
+ * Takes the list of file extension pairs and a matching list of program ids for
+ * each of those pairs and verifies that the system is successfully to match
+ * each.
+ *
+ * @param aFileExtensions array of file association pairs to set as default,
+ * like `[ ".pdf", "FirefoxPDF" ]`.
+ * @param aProgIDs array of program ids. The order of this array matches the
+ * file extensions parameter.
+ */
+static nsresult VerifyUserDefaults(const nsTArray<nsString>& aFileExtensions,
+ const nsTArray<nsString>& aProgIDs) {
+ for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) {
+ const wchar_t* fileExtension = aFileExtensions[i].get();
+
+ if (!VerifyUserDefault(fileExtension, aProgIDs[i / 2].get())) {
+ return NS_ERROR_WDBA_REJECTED;
+ }
+ }
+
+ return NS_OK;
+}
+
+/*
+ * Queries the system for the minimum resolution (or tick time) in milliseconds
+ * that can be used with ::Sleep. If a Sleep is not triggered with a time
+ * divisible by the tick time, control can return to the thread before the time
+ * specified to Sleep.
+ *
+ * @param defaultValue what to return if the system query fails.
+ */
+static UINT GetSystemSleepIntervalInMilliseconds(UINT defaultValue) {
+ TIMECAPS timeCapabilities;
+ bool timeCapsFetchSuccessful =
+ (MMSYSERR_NOERROR ==
+ timeGetDevCaps(&timeCapabilities, sizeof(timeCapabilities)));
+ if (!timeCapsFetchSuccessful) {
+ return defaultValue;
+ }
+
+ return timeCapabilities.wPeriodMin > 0 ? timeCapabilities.wPeriodMin
+ : defaultValue;
+}
+
+/*
+ * MSIX implementation for SetDefaultExtensionHandlersUserChoice.
+ *
+ * Due to the fact that MSIX builds run in a virtual, walled off environment,
+ * calling into the Win32 registry APIs doesn't work to set registry keys.
+ * MSIX builds access a virtual registry.
+ *
+ * Sends a "script" via command line to Powershell to modify the registry
+ * in an executable that is outside of the walled garden MSIX FX package
+ * environment, which is the only way to do that so far. Only works
+ * on slightly older versions of Windows 11 (older than 23H2).
+ *
+ * This was originally done using calls to Reg.exe, but permissions can't
+ * be changed that way, requiring using Powershell to call into .Net functions
+ * directly. Launching Powershell is slow (on the order of a second on some
+ * systems) so this method jams the calls to do everything into one launch of
+ * Powershell, to make it as quick as possible.
+ *
+ */
+static nsresult SetDefaultExtensionHandlersUserChoiceImplMsix(
+ const wchar_t* aAumi, const wchar_t* const aSid,
+ const nsTArray<nsString>& aFileExtensions) {
+ mozilla::UniquePtr<wchar_t[]> exePath;
+ if (!FindPowershell(exePath)) {
+ LOG_ERROR_MESSAGE(L"Could not locate Powershell");
+ return NS_ERROR_FAILURE;
+ }
+
+ // The following is the start of the script that will be sent to Powershell.
+ // In includes a function; calls to the function get appended per file
+ // extension, done below.
+ nsString startScript(
+ uR"(
+# Force exceptions to stop execution
+$ErrorActionPreference = 'Stop'
+
+function Set-DefaultHandlerRegistry($Path, $ProgID, $Hash) {
+ $Path = "$Path\UserChoice"
+ $CurrentUser = [Microsoft.Win32.Registry]::CurrentUser
+
+ # DeleteSubKey throws if we don't have sufficient permissions to delete key,
+ # signaling failure to launching process.
+ #
+ # Note: DeleteSubKeyTree fails when DENY permissions are set on key, whereas
+ # DeleteSubKey succeeds.
+ $CurrentUser.DeleteSubKey($Path, $false)
+ $key = $CurrentUser.CreateSubKey($Path)
+
+ $StringType = [Microsoft.Win32.RegistryValueKind]::String
+ $key.SetValue('ProgID', $ProgID, $StringType)
+ $key.SetValue('Hash', $Hash, $StringType)
+}
+
+)"); // Newlines in the above powershell script at the end are important!!!
+
+ // NOTE!!!! CreateProcess / calling things on the command line has a character
+ // count limit. For CMD.exe, it's 8K. For CreateProcessW, it's technically
+ // 32K. I can't find documentation about Powershell, but think 8K is safe to
+ // assume as a good maximum. The above script is about 1000 characters, and we
+ // will append another 100 characters at most per extension, so we'd need to
+ // be setting about 70 handlers at once to worry about the theoretical limit.
+ // Which we won't do. So the length shouldn't be a problem.
+ if (aFileExtensions.Length() >= 70) {
+ LOG_ERROR_MESSAGE(
+ L"SetDefaultExtensionHandlersUserChoiceImplMsix can't cope with 70 or "
+ L"more file extensions at once. Please break it up into multiple calls "
+ L"with fewer extensions per call.");
+ return NS_ERROR_FAILURE;
+ }
+
+ // NOTE!!!!
+ // User choice hashes have to be generated and written to the registry in the
+ // same minute. So we do everything we can upfront before we get to the hash
+ // generation, to ensure that hash generation and the call to Powershell to
+ // write to the registry has as much time as possible to run.
+
+ // Program ID fetch / generation might be slow, so do that ahead of time.
+ nsTArray<nsString> progIDs;
+ nsresult rv = GenerateProgramIDs(aFileExtensions, progIDs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString scriptBuffer;
+
+ // Everyting in the loop below should succeed or fail reasonably fast (within
+ // 20 milliseconds or something well under a second) besides the Powershell
+ // call. The Powershell call itself will either fail or succeed and break out
+ // of the loop, so repeating 10 times should be fine and is mostly to allow
+ // for getting the timing right with the user choice hash generation happening
+ // in the same minute - meaning this should likely only happen twice through
+ // the loop at most.
+ for (int i = 0; i < 10; i++) {
+ // Pre-allocate the memory for the scriptBuffer upfront so that we don't
+ // have to keep allocating every time Append is called.
+ const int scriptBufferCapacity = 16 * 1024;
+ scriptBuffer = startScript;
+ scriptBuffer.SetCapacity(scriptBufferCapacity);
+
+ SYSTEMTIME hashTimestamp;
+ ::GetSystemTime(&hashTimestamp);
+
+ // Time critical stuff starts here:
+
+ for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) {
+ const wchar_t* fileExtension = aFileExtensions[i].get();
+
+ nsAutoString keyPath;
+ AppendAssociationKeyPath(fileExtension, keyPath);
+
+ auto hashWchar = GenerateUserChoiceHash(
+ fileExtension, aSid, progIDs[i / 2].get(), hashTimestamp);
+ if (!hashWchar) {
+ return NS_ERROR_FAILURE;
+ }
+ auto hash = nsDependentString(hashWchar.get());
+
+ // Append a line to the script buffer in the form:
+ // Set-DefaultHandlerRegistry $RegistryKeyPath $ProgID $UserChoiceHash
+ scriptBuffer += u"Set-DefaultHandlerRegistry "_ns + keyPath + u" "_ns +
+ progIDs[i / 2] + u" "_ns + hash + u"\n"_ns;
+ }
+
+ // 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 = 2000;
+
+ // Generating the hash could have taken some time, so figure out what time
+ // we are at right now.
+ SYSTEMTIME writeEndTimestamp;
+ ::GetSystemTime(&writeEndTimestamp);
+
+ // Check if we have enough time to launch Powershell or if we should sleep
+ // to the next minute and try again
+ auto millisecondsLeftUntilNextMinute =
+ GetMillisecondsToNextMinute(hashTimestamp, writeEndTimestamp);
+ if (millisecondsLeftUntilNextMinute >= kWriteTimingThresholdMilliseconds) {
+ break;
+ }
+
+ LOG_ERROR_MESSAGE(
+ L"Hash is too close to next minute, sleeping until next minute to "
+ L"ensure that hash generation matches write to registry.");
+
+ UINT sleepUntilNextMinuteBufferMilliseconds =
+ GetSystemSleepIntervalInMilliseconds(
+ 50); // Default to 50ms if we can't figure out the right interval
+ // to sleep for
+ ::Sleep(millisecondsLeftUntilNextMinute +
+ (sleepUntilNextMinuteBufferMilliseconds * 2));
+
+ // Try again, if we have any attempts left
+ }
+
+ // Call Powershell to set the registry keys now - this is the really time
+ // consuming thing 250ms to launch on a VM in a reasonably fast case, possibly
+ // a lot slower on other systems
+ bool powershellSuccessful =
+ LaunchPowershell(scriptBuffer.get(), exePath.get());
+ if (!powershellSuccessful) {
+ // If powershell failed, it likely means that something got mucked with
+ // the registry and that Windows is popping up notifications to the user,
+ // so don't try again right now, so as to not overwhelm the user and annoy
+ // them.
+ return NS_ERROR_FAILURE;
+ }
+
+ // Validate now
+ return VerifyUserDefaults(aFileExtensions, progIDs);
+}
+
nsresult SetDefaultExtensionHandlersUserChoiceImpl(
const wchar_t* aAumi, const wchar_t* const aSid,
const nsTArray<nsString>& aFileExtensions) {
@@ -306,9 +688,8 @@ nsresult SetDefaultExtensionHandlersUserChoiceImpl(
GetCurrentPackageFullName(&pfnLen, nullptr) != APPMODEL_ERROR_NO_PACKAGE;
if (inMsix) {
- // MSIX packages can not meaningfully modify the registry keys related to
- // default handlers
- return NS_ERROR_FAILURE;
+ return SetDefaultExtensionHandlersUserChoiceImplMsix(aAumi, aSid,
+ aFileExtensions);
}
for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) {