diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index e2a841d79a..240421b360 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Screens.Menu; using osuTK.Input; @@ -15,8 +16,14 @@ namespace osu.Game.Tests.Visual.Menus { private OnlineMenuBanner onlineMenuBanner => Game.ChildrenOfType().Single(); + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("don't fetch online content", () => onlineMenuBanner.FetchOnlineContent = false); + } + [Test] - public void TestOnlineMenuBanner() + public void TestOnlineMenuBannerTrusted() { AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent { @@ -25,13 +32,51 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); AddStep("enter menu", () => InputManager.Key(Key.Enter)); AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("image loaded", () => onlineMenuBanner.ChildrenOfType().FirstOrDefault()?.IsLoaded, () => Is.True); + + AddStep("click banner", () => + { + InputManager.MoveMouseTo(onlineMenuBanner); + InputManager.Click(MouseButton.Left); + }); + + // Might not catch every occurrence due to async nature, but works in manual testing and saves annoying test setup. + AddAssert("no dialog", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog == null); + } + + [Test] + public void TestOnlineMenuBannerUntrustedDomain() + { + AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://google.com", + } + } + }); + AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("image loaded", () => onlineMenuBanner.ChildrenOfType().FirstOrDefault()?.IsLoaded, () => Is.True); + + AddStep("click banner", () => + { + InputManager.MoveMouseTo(onlineMenuBanner); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for dialog", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog != null); } } } diff --git a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs b/osu.Game/Localisation/DialogStrings.cs similarity index 80% rename from osu.Game/Localisation/DeleteConfirmationDialogStrings.cs rename to osu.Game/Localisation/DialogStrings.cs index 25997eadd3..043a3f5b4c 100644 --- a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs +++ b/osu.Game/Localisation/DialogStrings.cs @@ -5,14 +5,14 @@ using osu.Framework.Localisation; namespace osu.Game.Localisation { - public static class DeleteConfirmationDialogStrings + public static class DialogStrings { - private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog"; + private const string prefix = @"osu.Game.Resources.Localisation.Dialog"; /// /// "Caution" /// - public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Caution"); + public static LocalisableString Caution => new TranslatableString(getKey(@"header_text"), @"Caution"); /// /// "Yes. Go for it." diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 56d24e35bb..82ad4215c2 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -8,8 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; +using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Online.Chat { @@ -44,8 +46,8 @@ namespace osu.Game.Online.Chat { public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { - HeaderText = "Just checking..."; - BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}"; + HeaderText = DialogStrings.Caution; + BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; @@ -53,17 +55,17 @@ namespace osu.Game.Online.Chat { new PopupDialogOkButton { - Text = @"Yes. Go for it.", + Text = @"Open in browser", Action = openExternalLinkAction }, new PopupDialogCancelButton { - Text = @"Copy URL to the clipboard instead.", + Text = @"Copy URL to the clipboard", Action = copyExternalLinkAction }, new PopupDialogCancelButton { - Text = @"No! Abort mission!" + Text = CommonStrings.ButtonsCancel, }, }; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index af01a1b1ac..0833f52b1e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -485,10 +485,19 @@ namespace osu.Game } }); - public void OpenUrlExternally(string url, bool bypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => + public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => { + bool isTrustedDomain; + if (url.StartsWith('/')) - url = $"{API.APIEndpointUrl}{url}"; + { + url = $"{API.WebsiteRootUrl}{url}"; + isTrustedDomain = true; + } + else + { + isTrustedDomain = url.StartsWith(API.WebsiteRootUrl, StringComparison.Ordinal); + } if (!url.CheckIsValidUrl()) { @@ -500,7 +509,7 @@ namespace osu.Game return; } - externalLinkOpener.OpenUrlExternally(url, bypassExternalUrlWarning); + externalLinkOpener.OpenUrlExternally(url, forceBypassExternalUrlWarning || isTrustedDomain); }); /// diff --git a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs index 42a3ff827c..31160d1832 100644 --- a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Dialog protected DangerousActionDialog() { - HeaderText = DeleteConfirmationDialogStrings.HeaderText; + HeaderText = DialogStrings.Caution; Icon = FontAwesome.Regular.TrashAlt; @@ -38,12 +38,12 @@ namespace osu.Game.Overlays.Dialog { new PopupDialogDangerousButton { - Text = DeleteConfirmationDialogStrings.Confirm, + Text = DialogStrings.Confirm, Action = () => DangerousAction?.Invoke() }, new PopupDialogCancelButton { - Text = DeleteConfirmationDialogStrings.Cancel, + Text = DialogStrings.Cancel, Action = () => CancelAction?.Invoke() } }; diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index b9d269c82a..49fc89c171 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Menu Task.Run(() => request.Perform()) .ContinueWith(r => { + if (!FetchOnlineContent) + return; + if (r.IsCompletedSuccessfully) Schedule(() => Current.Value = request.ResponseObject); @@ -170,6 +173,11 @@ namespace osu.Game.Screens.Menu private Sprite flash = null!; + /// + /// Overridden as a safety for functioning correctly. + /// + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + private ScheduledDelegate? openUrlAction; public MenuImage(APIMenuImage image)