diff --git a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs index e17070b1284..09fe7d01261 100644 --- a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs @@ -21,7 +21,6 @@ public void OneTimeSetup() [TestCase("http://www.google.com")] [TestCase("https://www.google.com")] [TestCase("http://google.com")] - [TestCase("ftp://google.com")] [TestCase("www.google.com")] [TestCase("google.com")] [TestCase("http://localhost")] @@ -35,7 +34,6 @@ public void OneTimeSetup() [TestCase("110.10.10.10:8080")] [TestCase("192.168.1.1")] [TestCase("192.168.1.1:3000")] - [TestCase("ftp://110.10.10.10")] [TestCase("[2001:db8::1]")] [TestCase("[2001:db8::1]:8080")] [TestCase("http://[2001:db8::1]")] @@ -66,6 +64,23 @@ public void OneTimeSetup() [TestCase("192.168.1.1#fragment")] [TestCase("localhost:8080?test=123")] [TestCase("example.com#fragment")] + // Non-host-validated :// schemes + [TestCase("chrome://settings")] + [TestCase("edge://about")] + [TestCase("brave://settings")] + [TestCase("opera://history")] + [TestCase("vivaldi://bookmarks")] + [TestCase("chrome-extension://abc123def456/")] + [TestCase("moz-extension://abc123def456/")] + [TestCase("file:///C:/path/to/file.txt")] + // Colon-only schemes + [TestCase("about:blank")] + [TestCase("about:config")] + [TestCase("data:text/plain,hello")] + [TestCase("data:,Hello%2C%20World%21")] + // Chromium schemes in colon form + [TestCase("chrome:settings")] + [TestCase("chrome-extension:settings")] public void WhenValidUrlThenIsUrlReturnsTrue(string url) { Assert.That(plugin.IsURL(url), Is.True); @@ -87,6 +102,13 @@ public void WhenValidUrlThenIsUrlReturnsTrue(string url) [TestCase(".com")] [TestCase("http://.com")] [TestCase("2001:db8:::1")] + // Colon scheme with whitespace + [TestCase("about: blank")] + // Empty colon schemes + [TestCase("about:")] + [TestCase("chrome:")] + // Empty non host validated :// + [TestCase("chrome://")] public void WhenInvalidUrlThenIsUrlReturnsFalse(string url) { Assert.That(plugin.IsURL(url), Is.False); diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 07ce510fb3e..65c9ba7236e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -104,7 +104,7 @@ public List Query(Query query) Score = BookmarkLoader.MatchProgram(c, param).Score, Action = _ => { - Context.API.OpenUrl(c.Url); + SearchWeb.OpenInBrowserTab(c.Url); return true; }, @@ -128,7 +128,7 @@ public List Query(Query query) Score = 5, Action = _ => { - Context.API.OpenUrl(c.Url); + SearchWeb.OpenInBrowserTab(c.Url); return true; }, ContextData = new BookmarkAttributes { Url = c.Url } diff --git a/Plugins/Flow.Launcher.Plugin.Url/Main.cs b/Plugins/Flow.Launcher.Plugin.Url/Main.cs index 60a6eec92f1..8997e764916 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Url/Main.cs @@ -12,7 +12,21 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider internal static PluginInitContext Context { get; private set; } internal static Settings Settings { get; private set; } - private static readonly string[] UrlSchemes = ["http", "https", "ftp"]; + // Schemes requiring full host validation: domain with TLD, IP address, or localhost + private static readonly string[] HostValidatedSchemes = ["http", "https"]; + + // Chromium browser schemes accepting both :// and : forms (e.g. chrome://settings, chrome:settings) + private static readonly string[] ChromiumSchemes = ["chrome-extension", "chrome", "brave", "edge", "opera", "vivaldi"]; + + // Schemes using :// that are validated by scheme recognition alone — any valid URI structure is accepted + private static readonly string[] NonHostValidatedDoubleSlashSchemes = [.. ChromiumSchemes, "file", "moz-extension"]; + + // Schemes using colon-only syntax (e.g. about:blank, chrome:settings) + // Chromium schemes also accept the colon form + private static readonly string[] ColonOnlySchemes = [.. ChromiumSchemes, "about", "data"]; + + // All :// schemes + private static readonly string[] AllDoubleSlashSchemes = [.. HostValidatedSchemes, .. NonHostValidatedDoubleSlashSchemes]; public List Query(Query query) { @@ -44,7 +58,12 @@ public List Query(Query query) // if url was accepted without having any of the recognized scheme, // then that means no scheme was specified (e.g. www.google.com) // so we add the preferred http/https scheme - if (!UrlSchemes.Any(scheme => raw.StartsWith(scheme + "://", StringComparison.OrdinalIgnoreCase))) + var hasScheme = ( + AllDoubleSlashSchemes.Any(scheme => raw.StartsWith(scheme + "://", StringComparison.OrdinalIgnoreCase)) + || + ColonOnlySchemes.Any(scheme => raw.StartsWith(scheme + ":", StringComparison.OrdinalIgnoreCase)) + ); + if (!hasScheme) { raw = GetHttpPreference() + "://" + raw; } @@ -116,8 +135,20 @@ public bool IsURL(string raw) return true; } + // Check colon-only schemes (e.g. about:blank, chrome:settings) + var colonIndex = input.IndexOf(':'); + if (colonIndex > 0) + { + var scheme = input[..colonIndex]; + if (ColonOnlySchemes.Any(s => scheme.Equals(s, StringComparison.OrdinalIgnoreCase))) + { + var content = input[(colonIndex + 1)..]; + return content.Length > 0 && !content.Any(char.IsWhiteSpace); + } + } + // Add protocol if missing for Uri validation - var urlToValidate = UrlSchemes.Any(s => input.StartsWith(s + "://", StringComparison.OrdinalIgnoreCase)) + var urlToValidate = AllDoubleSlashSchemes.Any(s => input.StartsWith(s + "://", StringComparison.OrdinalIgnoreCase)) ? input : GetHttpPreference() + "://" + input; @@ -125,12 +156,16 @@ public bool IsURL(string raw) return false; - // Validate protocol against known schemes - if (!UrlSchemes.Any(scheme => uri.Scheme == scheme)) + // Validate protocol against known :// schemes + if (!AllDoubleSlashSchemes.Any(scheme => uri.Scheme == scheme)) return false; var host = uri.Host; + // Scheme-only validation: any valid URI structure is accepted, no host checks + if (NonHostValidatedDoubleSlashSchemes.Any(scheme => uri.Scheme == scheme)) + return true; + // localhost is valid if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) return true;