From 7294ce5cf6276c575c7e7513f6627e09b53c42a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sun, 25 Dec 2022 18:13:28 +0100 Subject: [PATCH 1/6] Add `shell_register_protocol` to register protocol handler on Windows This function registers a protocol handler on Windows by creating the necessary registry keys and values. The handler is only registered for the current user in the registry key `HKEY_CURRENT_USER\SOFTWARE\Classes`, so admin privileges are not required. --- src/base/system.cpp | 105 +++++++++++++++++++++++++++++++++++++++++++- src/base/system.h | 12 +++++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/base/system.cpp b/src/base/system.cpp index ac345d236..bdff23701 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -1436,7 +1436,7 @@ static int priv_net_close_all_sockets(NETSOCKET sock) } #if defined(CONF_FAMILY_WINDOWS) -static char *windows_format_system_message(int error) +static char *windows_format_system_message(unsigned long error) { WCHAR *wide_message; const DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_MAX_WIDTH_MASK; @@ -4304,6 +4304,109 @@ CWindowsComLifecycle::~CWindowsComLifecycle() { CoUninitialize(); } + +static void windows_print_error(const char *system, const char *prefix, HRESULT error) +{ + char *message = windows_format_system_message(error); + dbg_msg(system, "%s: %s", prefix, message == nullptr ? "unknown error" : message); + free(message); +} + +static std::wstring utf8_to_wstring(const char *str) +{ + const int orig_length = str_length(str); + int size_needed = MultiByteToWideChar(CP_UTF8, 0, str, orig_length, NULL, 0); + std::wstring wide_string(size_needed, '\0'); + dbg_assert(MultiByteToWideChar(CP_UTF8, 0, str, orig_length, &wide_string[0], size_needed) == size_needed, "MultiByteToWideChar failure"); + return wide_string; +} + +bool shell_register_protocol(const char *protocol_name, const char *executable) +{ + const std::wstring protocol_name_wide = utf8_to_wstring(protocol_name); + const std::wstring executable_wide = utf8_to_wstring(executable); + + // Open registry key for protocol associations of the current user + HKEY handle_subkey_classes; + const LRESULT result_subkey_classes = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes", 0, KEY_ALL_ACCESS, &handle_subkey_classes); + if(result_subkey_classes != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error opening registry key", result_subkey_classes); + return false; + } + + // Create the protocol key + HKEY handle_subkey_protocol; + const LRESULT result_subkey_protocol = RegCreateKeyExW(handle_subkey_classes, protocol_name_wide.c_str(), 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_protocol, NULL); + RegCloseKey(handle_subkey_classes); + if(result_subkey_protocol != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error creating registry key", result_subkey_protocol); + return false; + } + + // Set the default value for the key, which specifies the name of the display name of the protocol + const std::wstring value_protocol = L"URL:" + protocol_name_wide + L" Protocol"; + const LRESULT result_value_protocol = RegSetValueExW(handle_subkey_protocol, L"", 0, REG_SZ, (BYTE *)value_protocol.c_str(), (value_protocol.length() + 1) * sizeof(wchar_t)); + if(result_value_protocol != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error setting registry value", result_value_protocol); + RegCloseKey(handle_subkey_protocol); + return false; + } + + // Set the "URL Protocol" value, to specify that this key describes a URL protocol + const LRESULT result_value_empty = RegSetValueEx(handle_subkey_protocol, L"URL Protocol", 0, REG_SZ, (BYTE *)L"", sizeof(wchar_t)); + if(result_value_empty != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error setting registry value", result_value_empty); + RegCloseKey(handle_subkey_protocol); + return false; + } + + // Create the "DefaultIcon" subkey + HKEY handle_subkey_icon; + const LRESULT result_subkey_icon = RegCreateKeyExW(handle_subkey_protocol, L"DefaultIcon", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_icon, NULL); + if(result_subkey_icon != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error creating registry key", result_subkey_icon); + RegCloseKey(handle_subkey_protocol); + return false; + } + + // Set the default value for the key, which specifies the icon associated with the protocol + const std::wstring value_icon = L"\"" + executable_wide + L"\",0"; + const LRESULT result_value_icon = RegSetValueExW(handle_subkey_icon, L"", 0, REG_SZ, (BYTE *)value_icon.c_str(), (value_icon.length() + 1) * sizeof(wchar_t)); + RegCloseKey(handle_subkey_icon); + if(result_value_icon != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error setting registry value", result_value_icon); + RegCloseKey(handle_subkey_protocol); + return false; + } + + // Create the "shell\open\command" subkeys + HKEY handle_subkey_shell_open_command; + const LRESULT result_subkey_shell_open_command = RegCreateKeyExW(handle_subkey_protocol, L"shell\\open\\command", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_shell_open_command, NULL); + RegCloseKey(handle_subkey_protocol); + if(result_subkey_shell_open_command != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error creating registry key", result_subkey_shell_open_command); + return false; + } + + // Set the default value for the key, which specifies the executable command associated with the protocol + const std::wstring value_executable = L"\"" + executable_wide + L"\" \"%1\""; + const LRESULT result_value_executable = RegSetValueExW(handle_subkey_shell_open_command, L"", 0, REG_SZ, (BYTE *)value_executable.c_str(), (value_executable.length() + 1) * sizeof(wchar_t)); + RegCloseKey(handle_subkey_shell_open_command); + if(result_value_executable != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error setting registry value", result_value_executable); + return false; + } + + return true; +} #endif size_t std::hash::operator()(const NETADDR &Addr) const noexcept diff --git a/src/base/system.h b/src/base/system.h index fc2d324c2..38853c7bf 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -2570,6 +2570,18 @@ public: CWindowsComLifecycle(bool HasWindow); ~CWindowsComLifecycle(); }; + +/** + * Registers a protocol handler. + * + * @ingroup Shell + * + * @param protocol_name The name of the protocol. + * @param executable The absolute path of the executable that will be associated with the protocol. + * + * @return true on success, false on failure. + */ +bool shell_register_protocol(const char *protocol_name, const char *executable); #endif /** From 4e130a29e46c78d71081aa0dc871569abb35fbc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Mon, 26 Dec 2022 14:27:47 +0100 Subject: [PATCH 2/6] Add `shell_register_extension` to register file extension on Windows This function register a file extension on Windows by creating the necessary registry keys and values. The file extension is only registered for the current user in the registry key `HKEY_CURRENT_USER\SOFTWARE\Classes`, so admin privileges are not required. Windows does not allow changing the default file extension handler programatically. When the user opens a file for which a new file extension handler exists, a dialog is shown that allows the user to change the default handler. --- src/base/system.cpp | 114 ++++++++++++++++++++++++++++++++++++++++++++ src/base/system.h | 14 ++++++ 2 files changed, 128 insertions(+) diff --git a/src/base/system.cpp b/src/base/system.cpp index bdff23701..0be3e07d2 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -4407,6 +4407,120 @@ bool shell_register_protocol(const char *protocol_name, const char *executable) return true; } + +bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable) +{ + const std::wstring extension_wide = utf8_to_wstring(extension); + const std::wstring executable_name_wide = utf8_to_wstring(executable_name); + const std::wstring description_wide = executable_name_wide + L" " + utf8_to_wstring(description); + const std::wstring program_id_wide = executable_name_wide + extension_wide; + const std::wstring executable_wide = utf8_to_wstring(executable); + + // Open registry key for file associations of the current user + HKEY handle_subkey_classes; + const LRESULT result_subkey_classes = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes", 0, KEY_ALL_ACCESS, &handle_subkey_classes); + if(result_subkey_classes != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error opening registry key", result_subkey_classes); + return false; + } + + // Create the program ID key + HKEY handle_subkey_program_id; + const LRESULT result_subkey_program_id = RegCreateKeyExW(handle_subkey_classes, program_id_wide.c_str(), 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_program_id, NULL); + if(result_subkey_program_id != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error creating registry key", result_subkey_program_id); + RegCloseKey(handle_subkey_classes); + return false; + } + + // Set the default value for the key, which specifies the file type description for legacy applications + const LRESULT result_description_default = RegSetValueExW(handle_subkey_program_id, L"", 0, REG_SZ, (BYTE *)description_wide.c_str(), (description_wide.length() + 1) * sizeof(wchar_t)); + if(result_description_default != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error setting registry value", result_description_default); + RegCloseKey(handle_subkey_program_id); + RegCloseKey(handle_subkey_classes); + return false; + } + + // Set the "FriendlyTypeName" value, which specifies the file type description for modern applications + const LRESULT result_description_friendly = RegSetValueExW(handle_subkey_program_id, L"FriendlyTypeName", 0, REG_SZ, (BYTE *)description_wide.c_str(), (description_wide.length() + 1) * sizeof(wchar_t)); + if(result_description_friendly != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error setting registry value", result_description_friendly); + RegCloseKey(handle_subkey_program_id); + RegCloseKey(handle_subkey_classes); + return false; + } + + // Create the "DefaultIcon" subkey + HKEY handle_subkey_icon; + const LRESULT result_subkey_icon = RegCreateKeyExW(handle_subkey_program_id, L"DefaultIcon", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_icon, NULL); + if(result_subkey_icon != ERROR_SUCCESS) + { + windows_print_error("register_protocol", "Error creating registry key", result_subkey_icon); + RegCloseKey(handle_subkey_program_id); + RegCloseKey(handle_subkey_classes); + return false; + } + + // Set the default value for the key, which specifies the icon associated with the program ID + const std::wstring value_icon = L"\"" + executable_wide + L"\",0"; + const LRESULT result_value_icon = RegSetValueExW(handle_subkey_icon, L"", 0, REG_SZ, (BYTE *)value_icon.c_str(), (value_icon.length() + 1) * sizeof(wchar_t)); + RegCloseKey(handle_subkey_icon); + if(result_value_icon != ERROR_SUCCESS) + { + windows_print_error("register_protocol", "Error setting registry value", result_value_icon); + RegCloseKey(handle_subkey_program_id); + RegCloseKey(handle_subkey_classes); + return false; + } + + // Create the "shell\open\command" subkeys + HKEY handle_subkey_shell_open_command; + const LRESULT result_subkey_shell_open_command = RegCreateKeyExW(handle_subkey_program_id, L"shell\\open\\command", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_shell_open_command, NULL); + RegCloseKey(handle_subkey_program_id); + if(result_subkey_shell_open_command != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error creating registry key", result_subkey_shell_open_command); + RegCloseKey(handle_subkey_classes); + return false; + } + + // Set the default value for the key, which specifies the executable command associated with the application + const std::wstring value_executable = L"\"" + executable_wide + L"\" \"%1\""; + const LRESULT result_value_executable = RegSetValueExW(handle_subkey_shell_open_command, L"", 0, REG_SZ, (BYTE *)value_executable.c_str(), (value_executable.length() + 1) * sizeof(wchar_t)); + RegCloseKey(handle_subkey_shell_open_command); + if(result_value_executable != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error setting registry value", result_value_executable); + RegCloseKey(handle_subkey_classes); + return false; + } + + // Create the file extension key + HKEY handle_subkey_extension; + const LRESULT result_subkey_extension = RegCreateKeyExW(handle_subkey_classes, extension_wide.c_str(), 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_extension, NULL); + RegCloseKey(handle_subkey_classes); + if(result_subkey_extension != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error creating registry key", result_subkey_extension); + return false; + } + + // Set the default value for the key, which associates the file extension with the program ID + const LRESULT result_value_application = RegSetValueExW(handle_subkey_extension, L"", 0, REG_SZ, (BYTE *)program_id_wide.c_str(), (program_id_wide.length() + 1) * sizeof(wchar_t)); + RegCloseKey(handle_subkey_extension); + if(result_value_application != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error setting registry value", result_value_application); + return false; + } + + return true; +} #endif size_t std::hash::operator()(const NETADDR &Addr) const noexcept diff --git a/src/base/system.h b/src/base/system.h index 38853c7bf..b9b220f2d 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -2582,6 +2582,20 @@ public: * @return true on success, false on failure. */ bool shell_register_protocol(const char *protocol_name, const char *executable); + +/** + * Registers a file extension. + * + * @ingroup Shell + * + * @param extension The file extension, including the leading dot. + * @param description A readable description for the file extension. + * @param executable_name A unique name that will used to describe the application. + * @param executable The absolute path of the executable that will be associated with the file extension. + * + * @return true on success, false on failure. + */ +bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable); #endif /** From 8a3a8974d042c0698f0f88b1d657d23b704a1f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Thu, 29 Dec 2022 14:08:59 +0100 Subject: [PATCH 3/6] Add `shell_update` to notify system to update shell The `shell_update` function notifies the system when the shell needs to be updated due to a changed protocol or file association. An output parameter is added to the `shell_register_protocol` and `shell_register_extension` functions, to determine whether the shell needs to be updated after calling the functions. We only check whether the application path and program association were changed, instead of checking whether any key or value was changed, as this reduces the amount of necessary checks and we assume that the other values are not externally changed. Because updating the shell is a potentially expensive operation, this should only be done when necessary and only once after registering all protocols and extensions. --- src/base/system.cpp | 79 ++++++++++++++++++++++++++++++++------------- src/base/system.h | 19 +++++++++-- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/src/base/system.cpp b/src/base/system.cpp index 0be3e07d2..bf282fbe4 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -58,7 +58,9 @@ #elif defined(CONF_FAMILY_WINDOWS) #define WIN32_LEAN_AND_MEAN #undef _WIN32_WINNT -#define _WIN32_WINNT 0x0501 /* required for mingw to get getaddrinfo to work */ +// 0x0501 (Windows XP) is required for mingw to get getaddrinfo to work +// 0x0600 (Windows Vista) is required to use RegGetValueW +#define _WIN32_WINNT 0x0600 #include #include #include @@ -70,6 +72,7 @@ #include #include #include +#include // SHChangeNotify #include #include #else @@ -4321,7 +4324,7 @@ static std::wstring utf8_to_wstring(const char *str) return wide_string; } -bool shell_register_protocol(const char *protocol_name, const char *executable) +bool shell_register_protocol(const char *protocol_name, const char *executable, bool *updated) { const std::wstring protocol_name_wide = utf8_to_wstring(protocol_name); const std::wstring executable_wide = utf8_to_wstring(executable); @@ -4395,20 +4398,29 @@ bool shell_register_protocol(const char *protocol_name, const char *executable) return false; } - // Set the default value for the key, which specifies the executable command associated with the protocol + // Get the previous default value for the key, so we can determine if it changed + wchar_t old_value_executable[MAX_PATH + 16]; + DWORD old_size_executable = sizeof(old_value_executable); + const LRESULT result_old_value_executable = RegGetValueW(handle_subkey_shell_open_command, NULL, L"", RRF_RT_REG_SZ, NULL, (BYTE *)old_value_executable, &old_size_executable); const std::wstring value_executable = L"\"" + executable_wide + L"\" \"%1\""; - const LRESULT result_value_executable = RegSetValueExW(handle_subkey_shell_open_command, L"", 0, REG_SZ, (BYTE *)value_executable.c_str(), (value_executable.length() + 1) * sizeof(wchar_t)); - RegCloseKey(handle_subkey_shell_open_command); - if(result_value_executable != ERROR_SUCCESS) + if(result_old_value_executable != ERROR_SUCCESS || wcscmp(old_value_executable, value_executable.c_str()) != 0) { - windows_print_error("shell_register_protocol", "Error setting registry value", result_value_executable); - return false; + // Set the default value for the key, which specifies the executable command associated with the protocol + const LRESULT result_value_executable = RegSetValueExW(handle_subkey_shell_open_command, L"", 0, REG_SZ, (BYTE *)value_executable.c_str(), (value_executable.length() + 1) * sizeof(wchar_t)); + RegCloseKey(handle_subkey_shell_open_command); + if(result_value_executable != ERROR_SUCCESS) + { + windows_print_error("shell_register_protocol", "Error setting registry value", result_value_executable); + return false; + } + + *updated = true; } return true; } -bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable) +bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable, bool *updated) { const std::wstring extension_wide = utf8_to_wstring(extension); const std::wstring executable_name_wide = utf8_to_wstring(executable_name); @@ -4489,15 +4501,24 @@ bool shell_register_extension(const char *extension, const char *description, co return false; } - // Set the default value for the key, which specifies the executable command associated with the application + // Get the previous default value for the key, so we can determine if it changed + wchar_t old_value_executable[MAX_PATH + 16]; + DWORD old_size_executable = sizeof(old_value_executable); + const LRESULT result_old_value_executable = RegGetValueW(handle_subkey_shell_open_command, NULL, L"", RRF_RT_REG_SZ, NULL, (BYTE *)old_value_executable, &old_size_executable); const std::wstring value_executable = L"\"" + executable_wide + L"\" \"%1\""; - const LRESULT result_value_executable = RegSetValueExW(handle_subkey_shell_open_command, L"", 0, REG_SZ, (BYTE *)value_executable.c_str(), (value_executable.length() + 1) * sizeof(wchar_t)); - RegCloseKey(handle_subkey_shell_open_command); - if(result_value_executable != ERROR_SUCCESS) + if(result_old_value_executable != ERROR_SUCCESS || wcscmp(old_value_executable, value_executable.c_str()) != 0) { - windows_print_error("shell_register_extension", "Error setting registry value", result_value_executable); - RegCloseKey(handle_subkey_classes); - return false; + // Set the default value for the key, which specifies the executable command associated with the application + const LRESULT result_value_executable = RegSetValueExW(handle_subkey_shell_open_command, L"", 0, REG_SZ, (BYTE *)value_executable.c_str(), (value_executable.length() + 1) * sizeof(wchar_t)); + RegCloseKey(handle_subkey_shell_open_command); + if(result_value_executable != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error setting registry value", result_value_executable); + RegCloseKey(handle_subkey_classes); + return false; + } + + *updated = true; } // Create the file extension key @@ -4510,17 +4531,31 @@ bool shell_register_extension(const char *extension, const char *description, co return false; } - // Set the default value for the key, which associates the file extension with the program ID - const LRESULT result_value_application = RegSetValueExW(handle_subkey_extension, L"", 0, REG_SZ, (BYTE *)program_id_wide.c_str(), (program_id_wide.length() + 1) * sizeof(wchar_t)); - RegCloseKey(handle_subkey_extension); - if(result_value_application != ERROR_SUCCESS) + // Get the previous default value for the key, so we can determine if it changed + wchar_t old_value_application[128]; + DWORD old_size_application = sizeof(old_value_application); + const LRESULT result_old_value_application = RegGetValueW(handle_subkey_extension, NULL, L"", RRF_RT_REG_SZ, NULL, (BYTE *)old_value_application, &old_size_application); + if(result_old_value_application != ERROR_SUCCESS || wcscmp(old_value_application, program_id_wide.c_str()) != 0) { - windows_print_error("shell_register_extension", "Error setting registry value", result_value_application); - return false; + // Set the default value for the key, which associates the file extension with the program ID + const LRESULT result_value_application = RegSetValueExW(handle_subkey_extension, L"", 0, REG_SZ, (BYTE *)program_id_wide.c_str(), (program_id_wide.length() + 1) * sizeof(wchar_t)); + RegCloseKey(handle_subkey_extension); + if(result_value_application != ERROR_SUCCESS) + { + windows_print_error("shell_register_extension", "Error setting registry value", result_value_application); + return false; + } + + *updated = true; } return true; } + +void shell_update() +{ + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); +} #endif size_t std::hash::operator()(const NETADDR &Addr) const noexcept diff --git a/src/base/system.h b/src/base/system.h index b9b220f2d..27195e22e 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -2578,10 +2578,13 @@ public: * * @param protocol_name The name of the protocol. * @param executable The absolute path of the executable that will be associated with the protocol. + * @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated. * * @return true on success, false on failure. + * + * @remark The caller must later call shell_update, iff the shell needs to be updated. */ -bool shell_register_protocol(const char *protocol_name, const char *executable); +bool shell_register_protocol(const char *protocol_name, const char *executable, bool *updated); /** * Registers a file extension. @@ -2592,10 +2595,22 @@ bool shell_register_protocol(const char *protocol_name, const char *executable); * @param description A readable description for the file extension. * @param executable_name A unique name that will used to describe the application. * @param executable The absolute path of the executable that will be associated with the file extension. + * @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated. * * @return true on success, false on failure. + * + * @remark The caller must later call shell_update, iff the shell needs to be updated. */ -bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable); +bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable, bool *updated); + +/** + * Notifies the system that a protocol or file extension has been changed and the shell needs to be updated. + * + * @ingroup Shell + * + * @remark This is a potentially expensive operation, so it should only be called when necessary. + */ +void shell_update(); #endif /** From 3b73107100dd41e0ae4d518e1956b9a55272be75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Thu, 29 Dec 2022 16:47:17 +0100 Subject: [PATCH 4/6] Add `shell_unregister` to delete protocol/file extension handlers This function removes the registry keys that are created with `shell_register_protocol` and `shell_register_extension`. According to the Microsoft documentation, only the keys for the program IDs should be deleted. The keys that associate the file extensions with the program IDs should be kept, as Windows will automatically ignore the value if the program ID does not exist. See: https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation --- src/base/system.cpp | 29 ++++++++++++++++++++++++++++- src/base/system.h | 16 ++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/base/system.cpp b/src/base/system.cpp index bf282fbe4..c6f9c19e2 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -59,7 +59,7 @@ #define WIN32_LEAN_AND_MEAN #undef _WIN32_WINNT // 0x0501 (Windows XP) is required for mingw to get getaddrinfo to work -// 0x0600 (Windows Vista) is required to use RegGetValueW +// 0x0600 (Windows Vista) is required to use RegGetValueW and RegDeleteTreeW #define _WIN32_WINNT 0x0600 #include #include @@ -4552,6 +4552,33 @@ bool shell_register_extension(const char *extension, const char *description, co return true; } +bool shell_unregister(const char *shell_class, bool *updated) +{ + const std::wstring class_wide = utf8_to_wstring(shell_class); + + // Open registry key for protocol and file associations of the current user + HKEY handle_subkey_classes; + const LRESULT result_subkey_classes = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes", 0, KEY_ALL_ACCESS, &handle_subkey_classes); + if(result_subkey_classes != ERROR_SUCCESS) + { + windows_print_error("shell_unregister", "Error opening registry key", result_subkey_classes); + return false; + } + + // Delete the registry keys for the shell class (protocol or program ID) + LRESULT result_delete = RegDeleteTreeW(handle_subkey_classes, class_wide.c_str()); + RegCloseKey(handle_subkey_classes); + if(result_delete != ERROR_SUCCESS && result_delete != ERROR_FILE_NOT_FOUND) + { + windows_print_error("shell_unregister", "Error deleting registry key", result_delete); + if(result_delete == ERROR_SUCCESS) + *updated = true; + return false; + } + + return true; +} + void shell_update() { SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); diff --git a/src/base/system.h b/src/base/system.h index 27195e22e..17916a3de 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -2603,6 +2603,22 @@ bool shell_register_protocol(const char *protocol_name, const char *executable, */ bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable, bool *updated); +/** + * Unregisters a protocol or file extension handler. + * + * @ingroup Shell + * + * @param shell_class The shell class to delete. + * For protocols this is the name of the protocol. + * For file extensions this is the program ID associated with the file extension. + * @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated. + * + * @return true on success, false on failure. + * + * @remark The caller must later call shell_update, iff the shell needs to be updated. + */ +bool shell_unregister(const char *shell_class, bool *updated); + /** * Notifies the system that a protocol or file extension has been changed and the shell needs to be updated. * From db3d1f19a9d108df703a03892e67802a869d7816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sun, 25 Dec 2022 21:02:35 +0100 Subject: [PATCH 5/6] Register protocol and file extensions on client launch on Windows When launching the client on Windows, associate the protocol `ddnet` and the file extensions `.map` and `.demo` with the client executable. --- src/engine/client.h | 4 ++++ src/engine/client/client.cpp | 42 ++++++++++++++++++++++++++++++++++++ src/engine/client/client.h | 4 ++++ 3 files changed, 50 insertions(+) diff --git a/src/engine/client.h b/src/engine/client.h index b883a50c7..c01b0d877 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -280,6 +280,10 @@ public: virtual CChecksumData *ChecksumData() = 0; virtual bool InfoTaskRunning() = 0; virtual int UdpConnectivity(int NetType) = 0; + +#if defined(CONF_FAMILY_WINDOWS) + virtual void ShellRegister() = 0; +#endif }; class IGameClient : public IInterface diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 9ce7ccfd9..530f3574d 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -4694,6 +4694,11 @@ int main(int argc, const char **argv) } } + // Register protocol and file extensions +#if defined(CONF_FAMILY_WINDOWS) + pClient->ShellRegister(); +#endif + // init SDL if(SDL_Init(0) < 0) { @@ -4887,3 +4892,40 @@ int CClient::UdpConnectivity(int NetType) } return Connectivity; } + +#if defined(CONF_FAMILY_WINDOWS) +void CClient::ShellRegister() +{ + char aBinaryPath[IO_MAX_PATH_LENGTH]; + Storage()->GetBinaryPath(PLAT_CLIENT_EXEC, aBinaryPath, sizeof(aBinaryPath)); + char aFullPath[IO_MAX_PATH_LENGTH]; + if(fs_is_relative_path(aBinaryPath)) + { + if(fs_getcwd(aFullPath, sizeof(aFullPath))) + { + str_append(aFullPath, "/", sizeof(aFullPath)); + str_append(aFullPath, aBinaryPath, sizeof(aFullPath)); + } + else + aFullPath[0] = '\0'; + } + else + str_copy(aFullPath, aBinaryPath); + + if(!aFullPath[0]) + { + dbg_msg("client", "Failed to register protocol and file extensions: could not determine absolute path"); + return; + } + + bool Updated = false; + if(!shell_register_protocol("ddnet", aFullPath, &Updated)) + dbg_msg("client", "Failed to register ddnet protocol"); + if(!shell_register_extension(".map", "Map File", GAME_NAME, aFullPath, &Updated)) + dbg_msg("client", "Failed to register .map file extension"); + if(!shell_register_extension(".demo", "Demo File", GAME_NAME, aFullPath, &Updated)) + dbg_msg("client", "Failed to register .demo file extension"); + if(Updated) + shell_update(); +} +#endif diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 8143e5352..a072915e9 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -546,6 +546,10 @@ public: CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; } bool InfoTaskRunning() override { return m_pDDNetInfoTask != nullptr; } int UdpConnectivity(int NetType) override; + +#if defined(CONF_FAMILY_WINDOWS) + void ShellRegister() override; +#endif }; #endif From a61eec8f1e6504549c633b2e85a06c7ce12ff513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Thu, 29 Dec 2022 17:16:51 +0100 Subject: [PATCH 6/6] Add DDNet settings button to unregister protocol and file extensions Add a button to the Miscellaneous DDNet settings to manually unregister the protocol and file extension handlers. --- src/engine/client.h | 1 + src/engine/client/client.cpp | 13 +++++++++++++ src/engine/client/client.h | 1 + src/game/client/components/menus_settings.cpp | 11 +++++++++++ 4 files changed, 26 insertions(+) diff --git a/src/engine/client.h b/src/engine/client.h index c01b0d877..e9f60e5d9 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -283,6 +283,7 @@ public: #if defined(CONF_FAMILY_WINDOWS) virtual void ShellRegister() = 0; + virtual void ShellUnregister() = 0; #endif }; diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 530f3574d..09dca071a 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -4928,4 +4928,17 @@ void CClient::ShellRegister() if(Updated) shell_update(); } + +void CClient::ShellUnregister() +{ + bool Updated = false; + if(!shell_unregister("ddnet", &Updated)) + dbg_msg("client", "Failed to unregister ddnet protocol"); + if(!shell_unregister(GAME_NAME ".map", &Updated)) + dbg_msg("client", "Failed to unregister .map file extension"); + if(!shell_unregister(GAME_NAME ".demo", &Updated)) + dbg_msg("client", "Failed to unregister .demo file extension"); + if(Updated) + shell_update(); +} #endif diff --git a/src/engine/client/client.h b/src/engine/client/client.h index a072915e9..753054445 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -549,6 +549,7 @@ public: #if defined(CONF_FAMILY_WINDOWS) void ShellRegister() override; + void ShellUnregister() override; #endif }; diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 2128d1cfe..300ed9b8f 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -3433,6 +3433,17 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) SUIExEditBoxProperties EditProps; EditProps.m_pEmptyText = Localize("Chat command (e.g. showall 1)"); UI()->DoEditBox(g_Config.m_ClRunOnJoin, &Button, g_Config.m_ClRunOnJoin, sizeof(g_Config.m_ClRunOnJoin), 14.0f, &s_RunOnJoin, false, IGraphics::CORNER_ALL, EditProps); + +#if defined(CONF_FAMILY_WINDOWS) + static CButtonContainer s_ButtonUnregisterShell; + Right.HSplitTop(10.0f, nullptr, &Right); + Right.HSplitTop(20.0f, &Button, &Right); + if(DoButton_Menu(&s_ButtonUnregisterShell, Localize("Unregister protocol and file extensions"), 0, &Button)) + { + Client()->ShellUnregister(); + } +#endif + // Updater #if defined(CONF_AUTOUPDATE) {