diff --git a/src/base/system.cpp b/src/base/system.cpp index 56df4589c..ad94f3cc1 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -4175,6 +4175,7 @@ bool is_process_alive(PROCESS process) #endif } +#if !defined(CONF_PLATFORM_ANDROID) int open_link(const char *link) { #if defined(CONF_FAMILY_WINDOWS) @@ -4236,6 +4237,7 @@ int open_file(const char *path) return open_link(buf); #endif } +#endif // !defined(CONF_PLATFORM_ANDROID) struct SECURE_RANDOM_DATA { diff --git a/src/base/system.h b/src/base/system.h index 704e89356..4ac1bbb61 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -2583,6 +2583,7 @@ int kill_process(PROCESS process); */ bool is_process_alive(PROCESS process); +#if !defined(CONF_PLATFORM_ANDROID) /** * Opens a link in the browser. * @@ -2598,11 +2599,11 @@ bool is_process_alive(PROCESS process); int open_link(const char *link); /** - * Opens a file or directory with default program. + * Opens a file or directory with the default program. * * @ingroup Shell * - * @param path The path to open. + * @param path The file or folder to open with the default program. * * @return `1` on success, `0` on failure. * @@ -2610,6 +2611,7 @@ int open_link(const char *link); * @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. */ int open_file(const char *path); +#endif // !defined(CONF_PLATFORM_ANDROID) /** * @defgroup Secure-Random diff --git a/src/engine/client.h b/src/engine/client.h index 34279ffc3..c07b6e640 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -289,6 +289,27 @@ public: virtual CChecksumData *ChecksumData() = 0; virtual int UdpConnectivity(int NetType) = 0; + /** + * Opens a link in the browser. + * + * @param pLink The link to open in a browser. + * + * @return `true` on success, `false` on failure. + * + * @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. + */ + virtual bool ViewLink(const char *pLink) = 0; + /** + * Opens a file or directory with the default program. + * + * @param pFilename The file or folder to open with the default program. + * + * @return `true` on success, `false` on failure. + * + * @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. + */ + virtual bool ViewFile(const char *pFilename) = 0; + #if defined(CONF_FAMILY_WINDOWS) virtual void ShellRegister() = 0; virtual void ShellUnregister() = 0; diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 08c17e6c9..17a54b256 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -4781,6 +4781,51 @@ int CClient::UdpConnectivity(int NetType) return Connectivity; } +bool CClient::ViewLink(const char *pLink) +{ +#if defined(CONF_PLATFORM_ANDROID) + if(SDL_OpenURL(pLink) == 0) + { + return true; + } + log_error("client", "Failed to open link '%s' (%s)", pLink, SDL_GetError()); + return false; +#else + if(open_link(pLink)) + { + return true; + } + log_error("client", "Failed to open link '%s'", pLink); + return false; +#endif +} + +bool CClient::ViewFile(const char *pFilename) +{ +#if defined(CONF_PLATFORM_MACOS) + return ViewLink(pFilename); +#else + // Create a file link so the path can contain forward and + // backward slashes. But the file link must be absolute. + char aWorkingDir[IO_MAX_PATH_LENGTH]; + if(fs_is_relative_path(pFilename)) + { + if(!fs_getcwd(aWorkingDir, sizeof(aWorkingDir))) + { + log_error("client", "Failed to open file '%s' (failed to get working directory)", pFilename); + return false; + } + str_append(aWorkingDir, "/"); + } + else + aWorkingDir[0] = '\0'; + + char aFileLink[IO_MAX_PATH_LENGTH]; + str_format(aFileLink, sizeof(aFileLink), "file://%s%s", aWorkingDir, pFilename); + return ViewLink(aFileLink); +#endif +} + #if defined(CONF_FAMILY_WINDOWS) void CClient::ShellRegister() { diff --git a/src/engine/client/client.h b/src/engine/client/client.h index bc5877445..0cf3e7b57 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -503,6 +503,9 @@ public: CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; } int UdpConnectivity(int NetType) override; + bool ViewLink(const char *pLink) override; + bool ViewFile(const char *pFilename) override; + #if defined(CONF_FAMILY_WINDOWS) void ShellRegister() override; void ShellUnregister() override; diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index 9fe01f0b0..031ebc9c7 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -1625,10 +1625,7 @@ void CMenus::RenderPopupFullscreen(CUIRect Screen) static CButtonContainer s_ButtonOpenFolder; if(DoButton_Menu(&s_ButtonOpenFolder, Localize("Videos directory"), 0, &OpenFolder)) { - if(!open_file(aSaveFolder)) - { - dbg_msg("menus", "couldn't open file '%s'", aSaveFolder); - } + Client()->ViewFile(aSaveFolder); } static CButtonContainer s_ButtonOk; diff --git a/src/game/client/components/menus_demo.cpp b/src/game/client/components/menus_demo.cpp index 1efead687..4f61a7b83 100644 --- a/src/game/client/components/menus_demo.cpp +++ b/src/game/client/components/menus_demo.cpp @@ -1458,10 +1458,7 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc { char aBuf[IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, aBuf, sizeof(aBuf)); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } GameClient()->m_Tooltips.DoToolTip(&s_DemosDirectoryButton, &DemosDirectoryButton, Localize("Open the directory that contains the demo files")); } diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp index a394ca140..3a5f604a7 100644 --- a/src/game/client/components/menus_ingame.cpp +++ b/src/game/client/components/menus_ingame.cpp @@ -1166,10 +1166,7 @@ void CMenus::RenderGhost(CUIRect MainView) char aBuf[IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(IStorage::TYPE_SAVE, "ghosts", aBuf, sizeof(aBuf)); Storage()->CreateFolder("ghosts", IStorage::TYPE_SAVE); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } Status.VSplitLeft(5.0f, &Button, &Status); diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 00be5dd49..8172b46a5 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -77,8 +77,8 @@ bool CMenusKeyBinder::OnInput(const IInput::CEvent &Event) void CMenus::RenderSettingsGeneral(CUIRect MainView) { char aBuf[128 + IO_MAX_PATH_LENGTH]; - CUIRect Label, Button, Left, Right, Game, Client; - MainView.HSplitTop(150.0f, &Game, &Client); + CUIRect Label, Button, Left, Right, Game, ClientSettings; + MainView.HSplitTop(150.0f, &Game, &ClientSettings); // game { @@ -139,10 +139,10 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) // client { // headline - Client.HSplitTop(30.0f, &Label, &Client); + ClientSettings.HSplitTop(30.0f, &Label, &ClientSettings); Ui()->DoLabel(&Label, Localize("Client"), 20.0f, TEXTALIGN_ML); - Client.HSplitTop(5.0f, nullptr, &Client); - Client.VSplitMid(&Left, &Right, 20.0f); + ClientSettings.HSplitTop(5.0f, nullptr, &ClientSettings); + ClientSettings.VSplitMid(&Left, &Right, 20.0f); // skip main menu Left.HSplitTop(20.0f, &Button, &Left); @@ -165,10 +165,7 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) if(DoButton_Menu(&s_SettingsButtonId, Localize("Settings file"), 0, &SettingsButton)) { Storage()->GetCompletePath(IStorage::TYPE_SAVE, CONFIG_FILE, aBuf, sizeof(aBuf)); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } GameClient()->m_Tooltips.DoToolTip(&s_SettingsButtonId, &SettingsButton, Localize("Open the settings file")); @@ -179,10 +176,7 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) if(DoButton_Menu(&s_ConfigButtonId, Localize("Config directory"), 0, &ConfigButton)) { Storage()->GetCompletePath(IStorage::TYPE_SAVE, "", aBuf, sizeof(aBuf)); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } GameClient()->m_Tooltips.DoToolTip(&s_ConfigButtonId, &ConfigButton, Localize("Open the directory that contains the configuration and user files")); @@ -194,10 +188,7 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) { Storage()->GetCompletePath(IStorage::TYPE_SAVE, "themes", aBuf, sizeof(aBuf)); Storage()->CreateFolder("themes", IStorage::TYPE_SAVE); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } GameClient()->m_Tooltips.DoToolTip(&s_ThemesButtonId, &DirectoryButton, Localize("Open the directory to add custom themes")); @@ -938,11 +929,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) static CButtonContainer s_SkinDatabaseButton; if(DoButton_Menu(&s_SkinDatabaseButton, Localize("Skin Database"), 0, &DatabaseButton)) { - const char *pLink = "https://ddnet.org/skins/"; - if(!open_link(pLink)) - { - dbg_msg("menus", "couldn't open link '%s'", pLink); - } + Client()->ViewLink("https://ddnet.org/skins/"); } static CButtonContainer s_DirectoryButton; @@ -950,10 +937,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) { Storage()->GetCompletePath(IStorage::TYPE_SAVE, "skins", aBuf, sizeof(aBuf)); Storage()->CreateFolder("skins", IStorage::TYPE_SAVE); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } GameClient()->m_Tooltips.DoToolTip(&s_DirectoryButton, &DirectoryButton, Localize("Open the directory to add custom skins")); diff --git a/src/game/client/components/menus_settings_assets.cpp b/src/game/client/components/menus_settings_assets.cpp index c631547e4..a8a0e807e 100644 --- a/src/game/client/components/menus_settings_assets.cpp +++ b/src/game/client/components/menus_settings_assets.cpp @@ -648,10 +648,7 @@ void CMenus::RenderSettingsCustom(CUIRect MainView) Storage()->GetCompletePath(IStorage::TYPE_SAVE, aBufFull, aBuf, sizeof(aBuf)); Storage()->CreateFolder("assets", IStorage::TYPE_SAVE); Storage()->CreateFolder(aBufFull, IStorage::TYPE_SAVE); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } GameClient()->m_Tooltips.DoToolTip(&s_AssetsDirId, &DirectoryButton, Localize("Open the directory to add custom assets")); diff --git a/src/game/client/components/menus_start.cpp b/src/game/client/components/menus_start.cpp index ec505159b..b862c018f 100644 --- a/src/game/client/components/menus_start.cpp +++ b/src/game/client/components/menus_start.cpp @@ -43,11 +43,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) static CButtonContainer s_DiscordButton; if(DoButton_Menu(&s_DiscordButton, Localize("Discord"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) { - const char *pLink = Localize("https://ddnet.org/discord"); - if(!open_link(pLink)) - { - dbg_msg("menus", "couldn't open link '%s'", pLink); - } + Client()->ViewLink(Localize("https://ddnet.org/discord")); } ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space @@ -55,11 +51,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) static CButtonContainer s_LearnButton; if(DoButton_Menu(&s_LearnButton, Localize("Learn"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) { - const char *pLink = Localize("https://wiki.ddnet.org/"); - if(!open_link(pLink)) - { - dbg_msg("menus", "couldn't open link '%s'", pLink); - } + Client()->ViewLink(Localize("https://wiki.ddnet.org/")); } ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space @@ -96,11 +88,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) static CButtonContainer s_WebsiteButton; if(DoButton_Menu(&s_WebsiteButton, Localize("Website"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) { - const char *pLink = "https://ddnet.org/"; - if(!open_link(pLink)) - { - dbg_msg("menus", "couldn't open link '%s'", pLink); - } + Client()->ViewLink("https://ddnet.org/"); } ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 997d38be3..a203a5e37 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -5396,7 +5396,7 @@ void CEditor::RenderFileDialog() { char aOpenPath[IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_pFileDialogPath, aOpenPath, sizeof(aOpenPath)); - if(!open_file(aOpenPath)) + if(!Client()->ViewFile(aOpenPath)) { ShowFileDialogError("Failed to open the directory '%s'.", aOpenPath); } @@ -7712,7 +7712,7 @@ void CEditor::RenderMenubar(CUIRect MenuBar) if(DoButton_Editor(&s_HelpButton, "?", 0, &Help, 0, "[F1] Open the DDNet Wiki page for the Map Editor in a web browser") || (Input()->KeyPress(KEY_F1) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)) { const char *pLink = Localize("https://wiki.ddnet.org/wiki/Mapping"); - if(!open_link(pLink)) + if(!Client()->ViewLink(pLink)) { ShowFileDialogError("Failed to open the link '%s' in the default web browser.", pLink); }