2025-12-28 02:43:38 +00:00
|
|
|
|
using System;
|
2014-05-24 08:29:56 +00:00
|
|
|
|
using System.Collections.Generic;
|
2025-12-28 02:43:38 +00:00
|
|
|
|
using System.Linq;
|
2026-02-25 16:40:00 +00:00
|
|
|
|
using System.Net;
|
2025-10-09 08:36:42 +00:00
|
|
|
|
using System.Windows.Controls;
|
2025-09-04 09:30:20 +00:00
|
|
|
|
using Flow.Launcher.Plugin.SharedCommands;
|
2014-05-24 08:29:56 +00:00
|
|
|
|
|
2020-04-21 09:12:17 +00:00
|
|
|
|
namespace Flow.Launcher.Plugin.Url
|
2014-05-24 08:29:56 +00:00
|
|
|
|
{
|
2025-10-09 08:36:42 +00:00
|
|
|
|
public class Main : IPlugin, IPluginI18n, ISettingProvider
|
2014-05-24 08:29:56 +00:00
|
|
|
|
{
|
2025-09-27 16:18:33 +00:00
|
|
|
|
internal static PluginInitContext Context { get; private set; }
|
2025-09-28 04:02:24 +00:00
|
|
|
|
internal static Settings Settings { get; private set; }
|
2025-09-04 09:24:59 +00:00
|
|
|
|
|
2025-12-28 02:43:38 +00:00
|
|
|
|
private static readonly string[] UrlSchemes = ["http://", "https://", "ftp://"];
|
|
|
|
|
|
|
2015-01-03 07:20:34 +00:00
|
|
|
|
public List<Result> Query(Query query)
|
2014-05-24 08:29:56 +00:00
|
|
|
|
{
|
2015-01-26 09:46:55 +00:00
|
|
|
|
var raw = query.Search;
|
2025-12-28 02:43:38 +00:00
|
|
|
|
if (!IsURL(raw))
|
2014-05-24 08:29:56 +00:00
|
|
|
|
{
|
2025-12-28 02:43:38 +00:00
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 06:05:58 +00:00
|
|
|
|
if (IPEndPoint.TryParse(raw, out var endpoint))
|
|
|
|
|
|
{
|
2026-03-01 06:15:12 +00:00
|
|
|
|
if (endpoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 && raw[0] != '[' && raw[^1] != ']')
|
2026-03-01 06:05:58 +00:00
|
|
|
|
{
|
|
|
|
|
|
// Enclose IPv6 addresses in brackets for URL formatting
|
|
|
|
|
|
raw = $"[{raw}]";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-28 02:43:38 +00:00
|
|
|
|
return
|
2025-09-04 09:26:19 +00:00
|
|
|
|
[
|
|
|
|
|
|
new()
|
2014-05-24 08:29:56 +00:00
|
|
|
|
{
|
|
|
|
|
|
Title = raw,
|
2025-09-27 16:18:33 +00:00
|
|
|
|
SubTitle = Localize.flowlauncher_plugin_url_open_url(raw),
|
2014-07-19 02:12:11 +00:00
|
|
|
|
IcoPath = "Images/url.png",
|
2014-05-24 08:29:56 +00:00
|
|
|
|
Score = 8,
|
|
|
|
|
|
Action = _ =>
|
|
|
|
|
|
{
|
2025-12-28 02:43:38 +00:00
|
|
|
|
// not a recognized scheme, add preferred http scheme
|
|
|
|
|
|
if (!UrlSchemes.Any(scheme => raw.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)))
|
2014-07-19 02:12:11 +00:00
|
|
|
|
{
|
2025-10-09 08:36:42 +00:00
|
|
|
|
raw = GetHttpPreference() + "://" + raw;
|
2014-07-19 02:12:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-09-05 10:39:22 +00:00
|
|
|
|
if (Settings.UseCustomBrowser)
|
2025-09-04 09:30:20 +00:00
|
|
|
|
{
|
2025-09-05 10:39:22 +00:00
|
|
|
|
if (Settings.OpenInNewBrowserWindow)
|
|
|
|
|
|
{
|
|
|
|
|
|
SearchWeb.OpenInBrowserWindow(raw, Settings.BrowserPath, Settings.OpenInPrivateMode, Settings.PrivateModeArgument);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
SearchWeb.OpenInBrowserTab(raw, Settings.BrowserPath, Settings.OpenInPrivateMode, Settings.PrivateModeArgument);
|
|
|
|
|
|
}
|
2025-09-04 09:30:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-09-05 10:39:22 +00:00
|
|
|
|
Context.API.OpenWebUrl(raw);
|
2025-09-04 09:30:20 +00:00
|
|
|
|
}
|
2025-09-28 04:02:24 +00:00
|
|
|
|
|
2014-07-19 02:12:11 +00:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2022-08-08 04:31:38 +00:00
|
|
|
|
catch(Exception)
|
2014-07-19 02:12:11 +00:00
|
|
|
|
{
|
2025-09-27 16:18:33 +00:00
|
|
|
|
Context.API.ShowMsgError(Localize.flowlauncher_plugin_url_cannot_open_url(raw));
|
2014-07-19 02:12:11 +00:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2014-05-24 08:29:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-04 09:26:19 +00:00
|
|
|
|
];
|
2014-05-24 08:29:56 +00:00
|
|
|
|
}
|
2018-12-25 07:11:24 +00:00
|
|
|
|
|
2025-10-12 05:31:24 +00:00
|
|
|
|
private static string GetHttpPreference()
|
2025-10-09 08:36:42 +00:00
|
|
|
|
{
|
2025-10-12 05:31:24 +00:00
|
|
|
|
return Settings.AlwaysOpenWithHttps ? "https" : "http";
|
2025-10-09 08:36:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2014-12-15 16:07:12 +00:00
|
|
|
|
public bool IsURL(string raw)
|
|
|
|
|
|
{
|
2026-02-25 16:40:00 +00:00
|
|
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
|
|
|
|
return false;
|
2025-12-28 02:56:27 +00:00
|
|
|
|
|
2026-02-25 16:40:00 +00:00
|
|
|
|
var input = raw.Trim();
|
2014-12-15 16:07:12 +00:00
|
|
|
|
|
2026-02-25 16:40:00 +00:00
|
|
|
|
// Exclude numbers (e.g. 1.2345)
|
2026-02-27 01:09:33 +00:00
|
|
|
|
if (decimal.TryParse(input, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out _))
|
2026-02-25 16:40:00 +00:00
|
|
|
|
return false;
|
|
|
|
|
|
|
2026-02-26 06:26:20 +00:00
|
|
|
|
// Check if it's a bare IP address with optional port, path, query, or fragment
|
|
|
|
|
|
var ipPart = input.Split('/', '?', '#')[0]; // Remove path, query, and fragment
|
2026-03-02 10:52:51 +00:00
|
|
|
|
if (IPEndPoint.TryParse(ipPart, out var endpoint))
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (endpoint.AddressFamily)
|
|
|
|
|
|
{
|
|
|
|
|
|
case System.Net.Sockets.AddressFamily.InterNetwork:
|
|
|
|
|
|
return !endpoint.Address.Equals(IPAddress.Any);
|
|
|
|
|
|
case System.Net.Sockets.AddressFamily.InterNetworkV6:
|
|
|
|
|
|
if (input.Contains('/') || input.Contains('?') || input.Contains('#'))
|
|
|
|
|
|
{
|
|
|
|
|
|
// Check if IPv6 address is properly bracketed
|
|
|
|
|
|
var bracketStart = input.IndexOf('[');
|
|
|
|
|
|
var bracketEnd = input.IndexOf(']');
|
|
|
|
|
|
if (bracketStart == -1 || bracketEnd == -1 || bracketStart > bracketEnd)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return !endpoint.Address.Equals(IPAddress.IPv6Any);
|
|
|
|
|
|
}
|
2026-02-25 16:40:00 +00:00
|
|
|
|
return true;
|
2026-03-02 10:52:51 +00:00
|
|
|
|
}
|
2026-02-25 16:40:00 +00:00
|
|
|
|
|
2026-02-25 16:57:16 +00:00
|
|
|
|
// Add protocol if missing for Uri validation
|
|
|
|
|
|
var urlToValidate = UrlSchemes.Any(s => input.StartsWith(s, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
? input
|
|
|
|
|
|
: GetHttpPreference() + "://" + input;
|
2026-02-25 16:40:00 +00:00
|
|
|
|
|
|
|
|
|
|
if (!Uri.TryCreate(urlToValidate, UriKind.Absolute, out var uri))
|
|
|
|
|
|
return false;
|
2026-03-02 10:52:51 +00:00
|
|
|
|
|
2026-02-25 16:40:00 +00:00
|
|
|
|
|
|
|
|
|
|
// Validate protocol
|
|
|
|
|
|
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeFtp)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
var host = uri.Host;
|
2026-02-25 16:57:16 +00:00
|
|
|
|
|
|
|
|
|
|
// localhost is valid
|
2026-02-25 16:40:00 +00:00
|
|
|
|
if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
2026-02-25 16:57:16 +00:00
|
|
|
|
// Valid IP address (excluding 0.0.0.0)
|
2026-03-01 06:20:28 +00:00
|
|
|
|
if (IPEndPoint.TryParse(host, out endpoint))
|
|
|
|
|
|
return !endpoint.Address.Equals(IPAddress.Any) && !endpoint.Address.Equals(IPAddress.IPv6Any);
|
2026-02-25 16:40:00 +00:00
|
|
|
|
|
2026-02-25 16:57:16 +00:00
|
|
|
|
// Domain must have valid format with TLD
|
2026-02-25 16:40:00 +00:00
|
|
|
|
var parts = host.Split('.');
|
|
|
|
|
|
if (parts.Length < 2 || parts.Any(string.IsNullOrEmpty))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
2026-02-26 06:26:20 +00:00
|
|
|
|
// TLD must be at least 2 characters, allowing letters and digits
|
2026-02-25 16:40:00 +00:00
|
|
|
|
var tld = parts[^1];
|
2026-02-26 06:26:20 +00:00
|
|
|
|
return tld.Length >= 2 && tld.All(char.IsLetterOrDigit);
|
2014-12-15 16:07:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2015-01-03 07:20:34 +00:00
|
|
|
|
public void Init(PluginInitContext context)
|
2014-07-01 14:19:46 +00:00
|
|
|
|
{
|
2025-09-27 16:18:33 +00:00
|
|
|
|
Context = context;
|
2025-09-28 04:04:32 +00:00
|
|
|
|
|
2025-09-04 09:24:59 +00:00
|
|
|
|
Settings = context.API.LoadSettingJsonStorage<Settings>();
|
2015-02-07 12:17:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string GetTranslatedPluginTitle()
|
|
|
|
|
|
{
|
2025-09-27 16:18:33 +00:00
|
|
|
|
return Localize.flowlauncher_plugin_url_plugin_name();
|
2015-02-07 12:17:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string GetTranslatedPluginDescription()
|
|
|
|
|
|
{
|
2025-09-27 16:18:33 +00:00
|
|
|
|
return Localize.flowlauncher_plugin_url_plugin_description();
|
2014-05-24 08:29:56 +00:00
|
|
|
|
}
|
2025-10-09 08:36:42 +00:00
|
|
|
|
|
|
|
|
|
|
public Control CreateSettingPanel()
|
|
|
|
|
|
{
|
2025-10-12 05:31:24 +00:00
|
|
|
|
return new SettingsControl();
|
2025-10-09 08:36:42 +00:00
|
|
|
|
}
|
2014-05-24 08:29:56 +00:00
|
|
|
|
}
|
2022-08-08 04:31:38 +00:00
|
|
|
|
}
|