diff --git a/src/base/system.cpp b/src/base/system.cpp index a133d704f..6e2311591 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 and RegDeleteTreeW +#define _WIN32_WINNT 0x0600 #include #include #include @@ -70,6 +72,7 @@ #include #include #include +#include // SHChangeNotify #include #include #else @@ -1436,7 +1439,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; @@ -4302,6 +4305,282 @@ 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, bool *updated) +{ + 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; + } + + // 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\""; + if(result_old_value_executable != ERROR_SUCCESS || wcscmp(old_value_executable, value_executable.c_str()) != 0) + { + // 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 *updated) +{ + 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; + } + + // 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\""; + if(result_old_value_executable != ERROR_SUCCESS || wcscmp(old_value_executable, value_executable.c_str()) != 0) + { + // 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 + 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; + } + + // 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) + { + // 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; +} + +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); +} #endif size_t std::hash::operator()(const NETADDR &Addr) const noexcept diff --git a/src/base/system.h b/src/base/system.h index aaa4ef1de..9d4b97ecf 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -2581,6 +2581,63 @@ 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. + * @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 *updated); + +/** + * 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. + * @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 *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. + * + * @ingroup Shell + * + * @remark This is a potentially expensive operation, so it should only be called when necessary. + */ +void shell_update(); #endif /** diff --git a/src/engine/client.h b/src/engine/client.h index b883a50c7..e9f60e5d9 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -280,6 +280,11 @@ public: virtual CChecksumData *ChecksumData() = 0; virtual bool InfoTaskRunning() = 0; virtual int UdpConnectivity(int NetType) = 0; + +#if defined(CONF_FAMILY_WINDOWS) + virtual void ShellRegister() = 0; + virtual void ShellUnregister() = 0; +#endif }; class IGameClient : public IInterface diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 67eb5ca3f..eb037fb7d 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -4704,6 +4704,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) { @@ -4897,3 +4902,53 @@ 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(); +} + +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 8143e5352..753054445 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -546,6 +546,11 @@ 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; + void ShellUnregister() override; +#endif }; #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) {