|
1 | 1 | using System; |
2 | 2 | using System.Collections.Generic; |
3 | 3 | using System.Linq; |
4 | | -using System.Text.RegularExpressions; |
| 4 | +using System.Net; |
5 | 5 | using System.Windows.Controls; |
6 | 6 | using Flow.Launcher.Plugin.SharedCommands; |
7 | 7 |
|
8 | 8 | namespace Flow.Launcher.Plugin.Url |
9 | 9 | { |
10 | 10 | public class Main : IPlugin, IPluginI18n, ISettingProvider |
11 | 11 | { |
12 | | - //based on https://gist.github.com/dperini/729294 |
13 | | - private const string UrlPattern = "^" + |
14 | | - // protocol identifier |
15 | | - "(?:(?:https?|ftp)://|)" + |
16 | | - // user:pass authentication |
17 | | - "(?:\\S+(?::\\S*)?@)?" + |
18 | | - "(?:" + |
19 | | - // IPv6 address with optional brackets (brackets required if followed by port) |
20 | | - // IPv6 with brackets |
21 | | - "(?:\\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\]|" + // standard IPv6 |
22 | | - "\\[(?:[0-9a-fA-F]{1,4}:){1,7}:\\]|" + // IPv6 with trailing :: |
23 | | - "\\[(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\\]|" + // IPv6 compressed |
24 | | - "\\[::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}\\]|" + // IPv6 with leading :: |
25 | | - "\\[(?:(?:[0-9a-fA-F]{1,4}:){1,6}|:):(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}\\]|" + // IPv6 with :: in the middle |
26 | | - "\\[::1\\])" + // IPv6 loopback |
27 | | - "|" + |
28 | | - // IPv6 without brackets (only when no port follows) |
29 | | - "(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|" + // standard IPv6 |
30 | | - "(?:[0-9a-fA-F]{1,4}:){1,7}:|" + // IPv6 with trailing :: |
31 | | - "(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + // IPv6 compressed |
32 | | - "::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}|" + // IPv6 with leading :: |
33 | | - "(?:(?:[0-9a-fA-F]{1,4}:){1,6}|:):(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}|" + // IPv6 with :: in the middle |
34 | | - "::1)(?!:[0-9])" + // IPv6 loopback (not followed by port) |
35 | | - "|" + |
36 | | - // IPv4 address - all valid addresses including private networks (excluding 0.0.0.0) |
37 | | - "(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|[1-9])\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))" + |
38 | | - "|" + |
39 | | - // localhost |
40 | | - "localhost" + |
41 | | - "|" + |
42 | | - // host name |
43 | | - "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" + |
44 | | - // domain name |
45 | | - "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" + |
46 | | - // TLD identifier |
47 | | - "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" + |
48 | | - ")" + |
49 | | - // port number |
50 | | - "(?::\\d{1,5})?" + |
51 | | - // resource path |
52 | | - "(?:/\\S*)?" + |
53 | | - "$"; |
54 | | - private readonly Regex UrlRegex = new(UrlPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); |
55 | 12 | internal static PluginInitContext Context { get; private set; } |
56 | 13 | internal static Settings Settings { get; private set; } |
57 | 14 |
|
@@ -117,11 +74,68 @@ private static string GetHttpPreference() |
117 | 74 |
|
118 | 75 | public bool IsURL(string raw) |
119 | 76 | { |
120 | | - raw = raw.ToLower(); |
| 77 | + if (string.IsNullOrWhiteSpace(raw)) |
| 78 | + return false; |
121 | 79 |
|
122 | | - if (UrlRegex.Match(raw).Value == raw) return true; |
| 80 | + var input = raw.Trim(); |
123 | 81 |
|
124 | | - return false; |
| 82 | + // Exclude numbers (e.g. 1.2345) |
| 83 | + if (decimal.TryParse(input, out _)) |
| 84 | + return false; |
| 85 | + |
| 86 | + // Check if it's a bare IP address (without protocol) |
| 87 | + var inputHost = Uri.TryCreate(input, UriKind.Absolute, out var tempUri) ? tempUri.Host : input.Split(['/', ':'])[0].Trim('[', ']'); |
| 88 | + if (IPAddress.TryParse(inputHost, out var ip)) |
| 89 | + { |
| 90 | + // Exclude invalid address 0.0.0.0 |
| 91 | + if (ip.Equals(IPAddress.Any)) |
| 92 | + return false; |
| 93 | + |
| 94 | + return true; |
| 95 | + } |
| 96 | + |
| 97 | + // Check if it's a bare IPv6 address (contains multiple colons but no protocol) |
| 98 | + if (input.Count(c => c == ':') > 1 && !input.Contains("://")) |
| 99 | + { |
| 100 | + var ipv6Part = input.Split('/')[0].Trim('[', ']'); |
| 101 | + if (IPAddress.TryParse(ipv6Part, out _)) |
| 102 | + return true; |
| 103 | + } |
| 104 | + |
| 105 | + // Validate using Uri after adding protocol |
| 106 | + var urlToValidate = input; |
| 107 | + if (!UrlSchemes.Any(s => input.StartsWith(s, StringComparison.OrdinalIgnoreCase))) |
| 108 | + { |
| 109 | + urlToValidate = GetHttpPreference() + "://" + input; |
| 110 | + } |
| 111 | + |
| 112 | + if (!Uri.TryCreate(urlToValidate, UriKind.Absolute, out var uri)) |
| 113 | + return false; |
| 114 | + |
| 115 | + // Validate protocol |
| 116 | + if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeFtp) |
| 117 | + return false; |
| 118 | + |
| 119 | + // Validate host: must contain a dot (domain), be localhost, or be a valid IP |
| 120 | + var host = uri.Host; |
| 121 | + if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) |
| 122 | + return true; |
| 123 | + |
| 124 | + if (IPAddress.TryParse(host, out var hostIp)) |
| 125 | + return !hostIp.Equals(IPAddress.Any); |
| 126 | + |
| 127 | + // Domain must contain at least one dot, and dot cannot be at the start or end |
| 128 | + if (!host.Contains('.')) |
| 129 | + return false; |
| 130 | + |
| 131 | + // Ensure valid domain format (not starting or ending with dot, TLD at least 2 characters) |
| 132 | + var parts = host.Split('.'); |
| 133 | + if (parts.Length < 2 || parts.Any(string.IsNullOrEmpty)) |
| 134 | + return false; |
| 135 | + |
| 136 | + // TLD must be at least 2 characters |
| 137 | + var tld = parts[^1]; |
| 138 | + return tld.Length >= 2 && tld.All(char.IsLetter); |
125 | 139 | } |
126 | 140 |
|
127 | 141 | public void Init(PluginInitContext context) |
|
0 commit comments