Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions Flow.Launcher.Test/Plugins/UrlPluginTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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]")]
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public List<Result> Query(Query query)
Score = BookmarkLoader.MatchProgram(c, param).Score,
Action = _ =>
{
Context.API.OpenUrl(c.Url);
SearchWeb.OpenInBrowserTab(c.Url);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Bookmark actions now bypass Flow's configured browser launch settings and always force tab opening.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs, line 107:

<comment>Bookmark actions now bypass Flow's configured browser launch settings and always force tab opening.</comment>

<file context>
@@ -104,7 +104,7 @@ public List<Result> Query(Query query)
                         Action = _ =>
                         {
-                            Context.API.OpenUrl(c.Url);
+                            SearchWeb.OpenInBrowserTab(c.Url);
 
                             return true;
</file context>
Suggested change
SearchWeb.OpenInBrowserTab(c.Url);
Context.API.OpenUrl(c.Url);


return true;
},
Expand All @@ -128,7 +128,7 @@ public List<Result> Query(Query query)
Score = 5,
Action = _ =>
{
Context.API.OpenUrl(c.Url);
SearchWeb.OpenInBrowserTab(c.Url);
return true;
},
ContextData = new BookmarkAttributes { Url = c.Url }
Expand Down
45 changes: 40 additions & 5 deletions Plugins/Flow.Launcher.Plugin.Url/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result> Query(Query query)
{
Expand Down Expand Up @@ -44,7 +58,12 @@ public List<Result> 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;
}
Expand Down Expand Up @@ -116,21 +135,37 @@ 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Colon-scheme validation incorrectly treats chrome:// as valid because "//" passes the non-empty content check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Plugins/Flow.Launcher.Plugin.Url/Main.cs, line 146:

<comment>Colon-scheme validation incorrectly treats `chrome://` as valid because `"//"` passes the non-empty content check.</comment>

<file context>
@@ -116,21 +135,37 @@ public bool IsURL(string raw)
+                if (ColonOnlySchemes.Any(s => scheme.Equals(s, StringComparison.OrdinalIgnoreCase)))
+                {
+                    var content = input[(colonIndex + 1)..];
+                    return content.Length > 0 && !content.Any(char.IsWhiteSpace);
+                }
+            }
</file context>
Suggested change
return content.Length > 0 && !content.Any(char.IsWhiteSpace);
if (content.Length == 0 || content.Any(char.IsWhiteSpace))
return false;
if (content.StartsWith("//", StringComparison.Ordinal))
return ChromiumSchemes.Any(s => scheme.Equals(s, StringComparison.OrdinalIgnoreCase)) && content.Length > 2;
return true;

}
}
Comment on lines +138 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Bug: chrome:// incorrectly accepted as valid via colon-only path.

For Chromium schemes that support both forms (chrome:settings and chrome://settings), this check accepts chrome:// because content "//" passes the length and whitespace validation. The test at line 111 expects chrome:// to be invalid.

When content starts with //, defer to the double-slash validation path instead.

🐛 Proposed fix
         // 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)..];
+                // Defer double-slash forms to URI-based validation below
+                if (!content.StartsWith("//"))
+                {
                     return content.Length > 0 && !content.Any(char.IsWhiteSpace);
+                }
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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);
}
}
// 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)..];
// Defer double-slash forms to URI-based validation below
if (!content.StartsWith("//"))
{
return content.Length > 0 && !content.Any(char.IsWhiteSpace);
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Plugins/Flow.Launcher.Plugin.Url/Main.cs` around lines 138 - 148, In the
colon-only schemes validation block within the Main.cs file, the return
statement for validating content after the colon does not account for
double-slash patterns. When checking schemes like `chrome` in
`chrome://settings`, the content extracted is `//settings`, which passes the
current validation (length > 0 and no whitespace). Add an additional check to
exclude content that starts with `//` from this colon-only validation path,
allowing it to fall through to the double-slash validation logic instead. Modify
the return statement to ensure that if content begins with `//`, the method
continues to the next validation path rather than returning true.


// 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;

if (!Uri.TryCreate(urlToValidate, UriKind.Absolute, out var uri))
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;
Expand Down
Loading