diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/UriHelper/ExtendedUriParserTests.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/UriHelper/ExtendedUriParserTests.cs index 8da6c03a02..8a960a5f18 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/UriHelper/ExtendedUriParserTests.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/UriHelper/ExtendedUriParserTests.cs @@ -11,62 +11,85 @@ namespace Microsoft.Plugin.Uri.UnitTests.UriHelper public class ExtendedUriParserTests { [DataTestMethod] - [DataRow("google.com", true, "https://google.com/")] - [DataRow("http://google.com", true, "http://google.com/")] - [DataRow("localhost", true, "https://localhost/")] - [DataRow("http://localhost", true, "http://localhost/")] - [DataRow("127.0.0.1", true, "https://127.0.0.1/")] - [DataRow("http://127.0.0.1", true, "http://127.0.0.1/")] - [DataRow("http://127.0.0.1:80", true, "http://127.0.0.1/")] - [DataRow("127", false, null)] - [DataRow("", false, null)] - [DataRow("https://google.com", true, "https://google.com/")] - [DataRow("ftps://google.com", true, "ftps://google.com/")] - [DataRow(null, false, null)] - [DataRow("bing.com/search?q=gmx", true, "https://bing.com/search?q=gmx")] - [DataRow("http://bing.com/search?q=gmx", true, "http://bing.com/search?q=gmx")] - [DataRow("h", true, "https://h/")] - [DataRow("http://h", true, "http://h/")] - [DataRow("ht", true, "https://ht/")] - [DataRow("http://ht", true, "http://ht/")] - [DataRow("htt", true, "https://htt/")] - [DataRow("http://htt", true, "http://htt/")] - [DataRow("http", true, "https://http/")] - [DataRow("http://http", true, "http://http/")] - [DataRow("http:", false, null)] - [DataRow("http:/", false, null)] - [DataRow("http://", false, null)] - [DataRow("http://t", true, "http://t/")] - [DataRow("http://te", true, "http://te/")] - [DataRow("http://tes", true, "http://tes/")] - [DataRow("http://test", true, "http://test/")] - [DataRow("http://test.", false, null)] - [DataRow("http://test.c", true, "http://test.c/")] - [DataRow("http://test.co", true, "http://test.co/")] - [DataRow("http://test.com", true, "http://test.com/")] - [DataRow("http:3", true, "https://http:3/")] - [DataRow("http://http:3", true, "http://http:3/")] - [DataRow("[::]", true, "https://[::]/")] - [DataRow("http://[::]", true, "http://[::]/")] - [DataRow("[2001:0DB8::1]", true, "https://[2001:db8::1]/")] - [DataRow("http://[2001:0DB8::1]", true, "http://[2001:db8::1]/")] - [DataRow("[2001:0DB8::1]:80", true, "https://[2001:db8::1]/")] - [DataRow("http://[2001:0DB8::1]:80", true, "http://[2001:db8::1]/")] - [DataRow("mailto:example@mail.com", true, "mailto:example@mail.com")] - [DataRow("tel:411", true, "tel:411")] - [DataRow("ftp://example.com", true, "ftp://example.com/")] - [DataRow("example.com:443", true, "example.com:443")] + [DataRow("google.com", true, "https://google.com/", true)] + [DataRow("http://google.com", true, "http://google.com/", true)] + [DataRow("localhost", true, "https://localhost/", true)] + [DataRow("http://localhost", true, "http://localhost/", true)] + [DataRow("127.0.0.1", true, "https://127.0.0.1/", true)] + [DataRow("http://127.0.0.1", true, "http://127.0.0.1/", true)] + [DataRow("http://127.0.0.1:80", true, "http://127.0.0.1/", true)] + [DataRow("127", false, null, false)] + [DataRow("", false, null, false)] + [DataRow("https://google.com", true, "https://google.com/", true)] + [DataRow("ftps://google.com", true, "ftps://google.com/", false)] + [DataRow(null, false, null, false)] + [DataRow("bing.com/search?q=gmx", true, "https://bing.com/search?q=gmx", true)] + [DataRow("http://bing.com/search?q=gmx", true, "http://bing.com/search?q=gmx", true)] + [DataRow("h", true, "https://h/", true)] + [DataRow("http://h", true, "http://h/", true)] + [DataRow("ht", true, "https://ht/", true)] + [DataRow("http://ht", true, "http://ht/", true)] + [DataRow("htt", true, "https://htt/", true)] + [DataRow("http://htt", true, "http://htt/", true)] + [DataRow("http", true, "https://http/", true)] + [DataRow("http://http", true, "http://http/", true)] + [DataRow("http:", false, null, false)] + [DataRow("http:/", false, null, false)] + [DataRow("http://", false, null, false)] + [DataRow("http://t", true, "http://t/", true)] + [DataRow("http://te", true, "http://te/", true)] + [DataRow("http://tes", true, "http://tes/", true)] + [DataRow("http://test", true, "http://test/", true)] + [DataRow("http://test.", false, null, false)] + [DataRow("http://test.c", true, "http://test.c/", true)] + [DataRow("http://test.co", true, "http://test.co/", true)] + [DataRow("http://test.com", true, "http://test.com/", true)] + [DataRow("http:3", true, "https://http:3/", true)] + [DataRow("http://http:3", true, "http://http:3/", true)] + [DataRow("[::]", true, "https://[::]/", true)] + [DataRow("http://[::]", true, "http://[::]/", true)] + [DataRow("[2001:0DB8::1]", true, "https://[2001:db8::1]/", true)] + [DataRow("http://[2001:0DB8::1]", true, "http://[2001:db8::1]/", true)] + [DataRow("[2001:0DB8::1]:80", true, "https://[2001:db8::1]/", true)] + [DataRow("http://[2001:0DB8::1]:80", true, "http://[2001:db8::1]/", true)] + [DataRow("mailto:example@mail.com", true, "mailto:example@mail.com", false)] + [DataRow("tel:411", true, "tel:411", false)] + [DataRow("ftp://example.com", true, "ftp://example.com/", false)] - public void TryParseCanParseHostName(string query, bool expectedSuccess, string expectedResult) + // This has been parsed as an application URI. Linked issue: #14260 + [DataRow("example.com:443", true, "example.com:443", false)] + [DataRow("mailto:", true, "mailto:", false)] + [DataRow("mailto:/", false, null, false)] + [DataRow("ms-settings:", true, "ms-settings:", false)] + [DataRow("ms-settings:/", false, null, false)] + [DataRow("ms-settings://", false, null, false)] + [DataRow("ms-settings://privacy", true, "ms-settings://privacy/", false)] + [DataRow("ms-settings://privacy/", true, "ms-settings://privacy/", false)] + [DataRow("ms-settings:privacy", true, "ms-settings:privacy", false)] + [DataRow("ms-settings:powersleep", true, "ms-settings:powersleep", false)] + [DataRow("microsoft-edge:http://google.com", true, "microsoft-edge:http://google.com", false)] + [DataRow("microsoft-edge:https://google.com", true, "microsoft-edge:https://google.com", false)] + [DataRow("microsoft-edge:google.com", true, "microsoft-edge:google.com", false)] + [DataRow("microsoft-edge:google.com/", true, "microsoft-edge:google.com/", false)] + [DataRow("microsoft-edge:https://google.com/", true, "microsoft-edge:https://google.com/", false)] + [DataRow("ftp://user:password@localhost:8080", true, "ftp://user:password@localhost:8080/", false)] + [DataRow("ftp://user:password@localhost:8080/", true, "ftp://user:password@localhost:8080/", false)] + [DataRow("ftp://user:password@google.com", true, "ftp://user:password@google.com/", false)] + [DataRow("ftp://user:password@google.com:2121", true, "ftp://user:password@google.com:2121/", false)] + [DataRow("ftp://user:password@1.1.1.1", true, "ftp://user:password@1.1.1.1/", false)] + [DataRow("ftp://user:password@1.1.1.1:2121", true, "ftp://user:password@1.1.1.1:2121/", false)] + + public void TryParseCanParseHostName(string query, bool expectedSuccess, string expectedResult, bool expectedIsWebUri) { // Arrange var parser = new ExtendedUriParser(); // Act - var success = parser.TryParse(query, out var result); + var success = parser.TryParse(query, out var result, out var isWebUriResult); // Assert Assert.AreEqual(expectedResult, result?.ToString()); + Assert.AreEqual(expectedIsWebUri, isWebUriResult); Assert.AreEqual(expectedSuccess, success); } } diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.dark.png b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.dark.png index 6c34ed1582..e080717309 100644 Binary files a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.dark.png and b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.dark.png differ diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.light.png b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.light.png index bea54d005c..26722696aa 100644 Binary files a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.light.png and b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.light.png differ diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUriParser.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUriParser.cs index aa0154bc35..bb72ae3241 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUriParser.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUriParser.cs @@ -6,6 +6,6 @@ namespace Microsoft.Plugin.Uri.Interfaces { public interface IUriParser { - bool TryParse(string input, out System.Uri result); + bool TryParse(string input, out System.Uri result, out bool isWebUri); } } diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs index a1ea328fa5..bea9768a96 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs @@ -63,17 +63,15 @@ namespace Microsoft.Plugin.Uri { results.Add(new Result { - Title = Properties.Resources.Microsoft_plugin_uri_default_browser, + Title = Properties.Resources.Microsoft_plugin_uri_open, SubTitle = BrowserPath, - IcoPath = _uriSettings.ShowBrowserIcon - ? BrowserIconPath - : DefaultIconPath, + IcoPath = DefaultIconPath, Action = action => { if (!Helper.OpenInShell(BrowserPath)) { var title = $"Plugin: {Properties.Resources.Microsoft_plugin_uri_plugin_name}"; - var message = $"{Properties.Resources.Microsoft_plugin_default_browser_open_failed}: "; + var message = $"{Properties.Resources.Microsoft_plugin_uri_open_failed}: "; Context.API.ShowMsg(title, message); return false; } @@ -85,16 +83,19 @@ namespace Microsoft.Plugin.Uri } if (!string.IsNullOrEmpty(query?.Search) - && _uriParser.TryParse(query.Search, out var uriResult) + && _uriParser.TryParse(query.Search, out var uriResult, out var isWebUri) && _uriResolver.IsValidHost(uriResult)) { var uriResultString = uriResult.ToString(); + var isWebUriBool = isWebUri; results.Add(new Result { Title = uriResultString, - SubTitle = Properties.Resources.Microsoft_plugin_uri_website, - IcoPath = _uriSettings.ShowBrowserIcon + SubTitle = isWebUriBool + ? Properties.Resources.Microsoft_plugin_uri_website + : Properties.Resources.Microsoft_plugin_uri_open, + IcoPath = isWebUriBool ? BrowserIconPath : DefaultIconPath, Action = action => @@ -118,7 +119,7 @@ namespace Microsoft.Plugin.Uri private static bool IsActivationKeyword(Query query) { return !string.IsNullOrEmpty(query?.ActionKeyword) - && query?.ActionKeyword == query?.RawQuery; + && query?.ActionKeyword == query?.RawQuery; } private bool IsDefaultBrowserSet() @@ -155,16 +156,23 @@ namespace Microsoft.Plugin.Uri UpdateBrowserIconPath(newTheme); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to keep the process alive but will log the exception")] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1031:Do not catch general exception types", + Justification = "We want to keep the process alive but will log the exception")] private void UpdateBrowserIconPath(Theme newTheme) { try { - var progId = _registryWrapper.GetRegistryValue("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice", "ProgId"); + var progId = _registryWrapper.GetRegistryValue( + "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice", + "ProgId"); var programLocation = // Resolve App Icon (UWP) - _registryWrapper.GetRegistryValue("HKEY_CLASSES_ROOT\\" + progId + "\\Application", "ApplicationIcon") + _registryWrapper.GetRegistryValue( + "HKEY_CLASSES_ROOT\\" + progId + "\\Application", + "ApplicationIcon") // Resolves default file association icon (UWP + Normal) ?? _registryWrapper.GetRegistryValue("HKEY_CLASSES_ROOT\\" + progId + "\\DefaultIcon", null); @@ -174,14 +182,21 @@ namespace Microsoft.Plugin.Uri if (programLocation.StartsWith("@", StringComparison.Ordinal)) { var directProgramLocationStringBuilder = new StringBuilder(128); - if (NativeMethods.SHLoadIndirectString(programLocation, directProgramLocationStringBuilder, (uint)directProgramLocationStringBuilder.Capacity, IntPtr.Zero) == + if (NativeMethods.SHLoadIndirectString( + programLocation, + directProgramLocationStringBuilder, + (uint)directProgramLocationStringBuilder.Capacity, + IntPtr.Zero) == NativeMethods.Hresult.Ok) { // Check if there's a postfix with contract-white/contrast-black icon is available and use that instead var directProgramLocation = directProgramLocationStringBuilder.ToString(); - var themeIcon = newTheme == Theme.Light || newTheme == Theme.HighContrastWhite ? "contrast-white" : "contrast-black"; + var themeIcon = newTheme == Theme.Light || newTheme == Theme.HighContrastWhite + ? "contrast-white" + : "contrast-black"; var extension = Path.GetExtension(directProgramLocation); - var themedProgLocation = $"{directProgramLocation.Substring(0, directProgramLocation.Length - extension.Length)}_{themeIcon}{extension}"; + var themedProgLocation = + $"{directProgramLocation.Substring(0, directProgramLocation.Length - extension.Length)}_{themeIcon}{extension}"; BrowserIconPath = File.Exists(themedProgLocation) ? themedProgLocation : directProgramLocation; diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Properties/Resources.Designer.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Properties/Resources.Designer.cs index 71c6832a11..0d073767a1 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Properties/Resources.Designer.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Properties/Resources.Designer.cs @@ -70,7 +70,7 @@ namespace Microsoft.Plugin.Uri.Properties { } /// - /// Looks up a localized string similar to Open Default Browser. + /// Looks up a localized string similar to Open default browser. /// public static string Microsoft_plugin_uri_default_browser { get { @@ -79,7 +79,16 @@ namespace Microsoft.Plugin.Uri.Properties { } /// - /// Looks up a localized string similar to Failed to open URL. + /// Looks up a localized string similar to Open URI. + /// + public static string Microsoft_plugin_uri_open { + get { + return ResourceManager.GetString("Microsoft_plugin_uri_open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to open URI. /// public static string Microsoft_plugin_uri_open_failed { get { diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Properties/Resources.resx b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Properties/Resources.resx index 6ce700ddef..19c0a39031 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Properties/Resources.resx +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Properties/Resources.resx @@ -123,8 +123,11 @@ Open default browser + + Open URI + - Failed to open URL + Failed to open URI Opens URLs and UNC network shares. @@ -135,4 +138,4 @@ Open in default browser - + \ No newline at end of file diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs index 322b2a5efa..82d2327502 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs @@ -10,22 +10,37 @@ namespace Microsoft.Plugin.Uri.UriHelper { public class ExtendedUriParser : IUriParser { - public bool TryParse(string input, out System.Uri result) + public bool TryParse(string input, out System.Uri result, out bool isWebUri) { if (string.IsNullOrEmpty(input)) { result = default; + isWebUri = false; return false; } + // Handling URL with only scheme, typically mailto or application uri. + // Do nothing, return the result without urlBuilder + if (input.EndsWith(":", StringComparison.OrdinalIgnoreCase) + && !input.StartsWith("http", StringComparison.OrdinalIgnoreCase) + && !input.Contains("/", StringComparison.OrdinalIgnoreCase) + && !input.All(char.IsDigit)) + { + result = new System.Uri(input); + isWebUri = false; + return true; + } + // Handle common cases UriBuilder does not handle // Using CurrentCulture since this is a user typed string if (input.EndsWith(":", StringComparison.CurrentCulture) || input.EndsWith(".", StringComparison.CurrentCulture) || input.EndsWith(":/", StringComparison.CurrentCulture) + || input.EndsWith("://", StringComparison.CurrentCulture) || input.All(char.IsDigit)) { result = default; + isWebUri = false; return false; } @@ -35,27 +50,31 @@ namespace Microsoft.Plugin.Uri.UriHelper var hadDefaultPort = urlBuilder.Uri.IsDefaultPort; urlBuilder.Port = hadDefaultPort ? -1 : urlBuilder.Port; - if (input.Contains("HTTP://", StringComparison.OrdinalIgnoreCase)) + if (input.StartsWith("HTTP://", StringComparison.OrdinalIgnoreCase)) { urlBuilder.Scheme = System.Uri.UriSchemeHttp; + isWebUri = true; } else if (input.Contains(":", StringComparison.OrdinalIgnoreCase) && - !input.Contains("http", StringComparison.OrdinalIgnoreCase) && + !input.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !input.Contains("[", StringComparison.OrdinalIgnoreCase)) { // Do nothing, leave unchanged + isWebUri = false; } else { urlBuilder.Scheme = System.Uri.UriSchemeHttps; + isWebUri = true; } result = urlBuilder.Uri; return true; } - catch (System.UriFormatException) + catch (UriFormatException) { result = default; + isWebUri = false; return false; } }