From efa09182d37324b710f320a8d7e8aa8dce214187 Mon Sep 17 00:00:00 2001 From: Koh Jun Dong Date: Fri, 12 Aug 2022 00:04:39 +0800 Subject: [PATCH] [PTRun][URL]Fix web link with ports support (#19809) * [PT Run] Fix web link with ports support (#14260) * URL in the format of `domain:port` now directs to default browser * Add tests to verify web link with ports scenario * Fix test case and scenario where mismatching schema and port for IPv6 does not result in correct output * [PT Run][Tests] Change and add more UriParser Tests * Specifically of note is line 56, where [IPv6]:80 diverts to https instead of http. * [PT Run][Tests] Add UriParser tests * Add more tests targeting port handling * [PT Run] Fix http handling * This also fixes oddity with IPv4 and IPv6 handling * [PT Run] Add second results depending on condition * Test: update all test to reflect updated functions & add a little more tests * Update function to show two results when URI is in the format of `domain:port` (situation where it can also be `schema:path`) * Update regex style to follow previous code * [PT Run] Change tests and filter localhost from certain results * Add tests for 127.0.0.1, localhost, and ::1 * Move test around into more logical arrangement * Filter localhost out from showing double results * [PT Run] Fix spelling on comments * [PT Run] Add some words to expect.txt * [PT Toys] Clarify comment regarding [::] Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com> * [PT Run] Remove tests regarding tel protocol * [PT Run] Clarify UriParser parameter * [PT Run] Add UriParser tests for tel protocol * Current code has a regression bug where tel:xxxx, if xxxx is more than 65536 it will break. Will fix in follow up commit. * [PT Run] Refactor ExtendedUriParser and its tests * Remove `isWebUri` from ExtendedUriParser, keeping only webUri and systemUri * Tel protocol regression bug still exists * [PT Run] Fix wrong icon when webUri result * [PT Run] Fix regression bug for tel protocol * Tel protocol will sometimes bug out when tel:xxxx if xxxxx is more than 65535, as UriBuilder will throw error thinking the port number has been exceeded * [PT Toy] Fix tel test * [PT Run] Changes to tests * Add test for application uri to include ports, for all non-protocol, http and https variants * Rearrange some more test to make more logical sense, and add comments * [PT Run] Simplify code * Move webUri and systemUri to be global, as per htcfreek's recommendation * Add comment to empty catch * Change null to default * [PT Toy] Update test name Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com> * [PT Toy] Change result prompt when empty string * [PT Toy] Fix typo in comment * [PT Toy] Simplify line * [PT Toy] Change result prompt when empty string --- .github/actions/spell-check/expect.txt | 10 +- .../UriHelper/ExtendedUriParserTests.cs | 227 ++++++++++++------ .../Interfaces/IUriParser.cs | 2 +- .../Plugins/Microsoft.Plugin.Uri/Main.cs | 68 ++++-- .../UriHelper/ExtendedUriParser.cs | 56 +++-- 5 files changed, 245 insertions(+), 118 deletions(-) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 2f4034cf1e..0fca1c9093 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -5,7 +5,6 @@ abgr abi ABlocked ABOUTBOX -ABradley Abug accctrl Acceleratorkeys @@ -134,7 +133,6 @@ awakeness awakeversion AWAYMODE AYUV -azchohfi backend backtracer BADD @@ -371,6 +369,7 @@ cxfksword CXSMICON CXVIRTUALSCREEN cxxopts +Cxxx cyberrex Cyrl CYSMICON @@ -420,7 +419,6 @@ Delimarsky dend DENORMAL Deondre -dependabot depersist deprioritized deps @@ -520,8 +518,8 @@ editshortcutswindow EFile ekus elif -eltociear elseif +eltociear Emoji emptyrecyclebin ENABLEDPOPUP @@ -1093,6 +1091,7 @@ LOADSTRING LOBYTE LOCALAPPDATA LOCALDISPLAY +localhost LOCALPACKAGE localport localtime @@ -1298,7 +1297,6 @@ NAMECHANGE nameof namespace Navassa -naveensrinivasan NCACTIVATE ncc NCCALCSIZE @@ -1440,8 +1438,8 @@ openxmlformats OPTIMIZEFORINVOKE ORAW ORPHANEDDIALOGTITLE -oss osfanbuff +oss ostr ostream ostringstream 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 3628f3ca89..cd4a3e0e6f 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,86 +11,171 @@ namespace Microsoft.Plugin.Uri.UnitTests.UriHelper public class ExtendedUriParserTests { [DataTestMethod] - [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)] - // 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)] - [DataRow("^:", false, null, false)] + // Standard web uri + [DataRow("google.com", true, "https://google.com/", null)] + [DataRow("http://google.com", true, "http://google.com/", null)] + [DataRow("https://google.com", true, "https://google.com/", null)] + [DataRow("ftps://google.com", true, null, "ftps://google.com/")] + [DataRow("bing.com/search?q=gmx", true, "https://bing.com/search?q=gmx", null)] + [DataRow("http://bing.com/search?q=gmx", true, "http://bing.com/search?q=gmx", null)] - public void TryParseCanParseHostName(string query, bool expectedSuccess, string expectedResult, bool expectedIsWebUri) + // Edge cases + [DataRow("127", false, null, null)] + [DataRow(null, false, null, null)] + [DataRow("h", true, "https://h/", null)] + [DataRow("ht", true, "https://ht/", null)] + [DataRow("htt", true, "https://htt/", null)] + [DataRow("http", true, "https://http/", null)] + [DataRow("http:", false, null, null)] + [DataRow("http:/", false, null, null)] + [DataRow("http://", false, null, null)] + [DataRow("http://h", true, "http://h/", null)] + [DataRow("http://ht", true, "http://ht/", null)] + [DataRow("http://htt", true, "http://htt/", null)] + [DataRow("http://http", true, "http://http/", null)] + [DataRow("http://t", true, "http://t/", null)] + [DataRow("http://te", true, "http://te/", null)] + [DataRow("http://tes", true, "http://tes/", null)] + [DataRow("http://test", true, "http://test/", null)] + [DataRow("http://test.", false, null, null)] + [DataRow("http://test.c", true, "http://test.c/", null)] + [DataRow("http://test.co", true, "http://test.co/", null)] + [DataRow("http://test.com", true, "http://test.com/", null)] + [DataRow("http:3", true, "https://http:3/", null)] + [DataRow("http://http:3", true, "http://http:3/", null)] + [DataRow("[2001:0DB8::1]", true, "https://[2001:db8::1]/", null)] + [DataRow("[2001:0DB8::1]:80", true, "http://[2001:db8::1]/", null)] + [DataRow("[2001:0DB8::1]:443", true, "https://[2001:db8::1]/", null)] + [DataRow("http://[2001:0DB8::1]", true, "http://[2001:db8::1]/", null)] + [DataRow("http://[2001:0DB8::1]:80", true, "http://[2001:db8::1]/", null)] + [DataRow("http://[2001:0DB8::1]:443", true, "http://[2001:db8::1]:443/", null)] + [DataRow("https://[2001:0DB8::1]:80", true, "https://[2001:db8::1]:80/", null)] + [DataRow("http://test.test.test.test:952", true, "http://test.test.test.test:952/", null)] + [DataRow("https://test.test.test.test:952", true, "https://test.test.test.test:952/", null)] + + // ToDo: Block [::] address results in parser. This Address is unspecified per RFC 4291 and the results make no sense. + [DataRow("[::]", true, "https://[::]/", null)] + [DataRow("http://[::]", true, "http://[::]/", null)] + + // localhost, 127.0.0.1, ::1 tests + [DataRow("localhost", true, "https://localhost/", null)] + [DataRow("localhost:80", true, "http://localhost/", null)] + [DataRow("localhost:443", true, "https://localhost/", null)] + [DataRow("localhost:1234", true, "https://localhost:1234/", null)] + [DataRow("localhost/test", true, "https://localhost/test", null)] + [DataRow("localhost:80/test", true, "http://localhost/test", null)] + [DataRow("localhost:443/test", true, "https://localhost/test", null)] + [DataRow("localhost:1234/test", true, "https://localhost:1234/test", null)] + [DataRow("http://localhost", true, "http://localhost/", null)] + [DataRow("http://localhost:1234", true, "http://localhost:1234/", null)] + [DataRow("https://localhost", true, "https://localhost/", null)] + [DataRow("https://localhost:1234", true, "https://localhost:1234/", null)] + [DataRow("http://localhost/test", true, "http://localhost/test", null)] + [DataRow("http://localhost:1234/test", true, "http://localhost:1234/test", null)] + [DataRow("https://localhost/test", true, "https://localhost/test", null)] + [DataRow("https://localhost:1234/test", true, "https://localhost:1234/test", null)] + [DataRow("127.0.0.1", true, "https://127.0.0.1/", null)] + [DataRow("127.0.0.1:80", true, "http://127.0.0.1/", null)] + [DataRow("127.0.0.1:443", true, "https://127.0.0.1/", null)] + [DataRow("127.0.0.1:1234", true, "https://127.0.0.1:1234/", null)] + [DataRow("127.0.0.1/test", true, "https://127.0.0.1/test", null)] + [DataRow("127.0.0.1:80/test", true, "http://127.0.0.1/test", null)] + [DataRow("127.0.0.1:443/test", true, "https://127.0.0.1/test", null)] + [DataRow("127.0.0.1:1234/test", true, "https://127.0.0.1:1234/test", null)] + [DataRow("http://127.0.0.1", true, "http://127.0.0.1/", null)] + [DataRow("http://127.0.0.1:1234", true, "http://127.0.0.1:1234/", null)] + [DataRow("https://127.0.0.1", true, "https://127.0.0.1/", null)] + [DataRow("https://127.0.0.1:1234", true, "https://127.0.0.1:1234/", null)] + [DataRow("http://127.0.0.1/test", true, "http://127.0.0.1/test", null)] + [DataRow("http://127.0.0.1:1234/test", true, "http://127.0.0.1:1234/test", null)] + [DataRow("https://127.0.0.1/test", true, "https://127.0.0.1/test", null)] + [DataRow("https://127.0.0.1:1234/test", true, "https://127.0.0.1:1234/test", null)] + [DataRow("[::1]", true, "https://[::1]/", null)] + [DataRow("[::1]:80", true, "http://[::1]/", null)] + [DataRow("[::1]:443", true, "https://[::1]/", null)] + [DataRow("[::1]:1234", true, "https://[::1]:1234/", null)] + [DataRow("[::1]/test", true, "https://[::1]/test", null)] + [DataRow("[::1]:80/test", true, "http://[::1]/test", null)] + [DataRow("[::1]:443/test", true, "https://[::1]/test", null)] + [DataRow("[::1]:1234/test", true, "https://[::1]:1234/test", null)] + [DataRow("http://[::1]", true, "http://[::1]/", null)] + [DataRow("http://[::1]:1234", true, "http://[::1]:1234/", null)] + [DataRow("https://[::1]", true, "https://[::1]/", null)] + [DataRow("https://[::1]:1234", true, "https://[::1]:1234/", null)] + [DataRow("http://[::1]/test", true, "http://[::1]/test", null)] + [DataRow("http://[::1]:1234/test", true, "http://[::1]:1234/test", null)] + [DataRow("https://[::1]/test", true, "https://[::1]/test", null)] + [DataRow("https://[::1]:1234/test", true, "https://[::1]:1234/test", null)] + + // Case where `domain:port`, as specified in issue #14260 + // Assumption: Only domain with dot is accepted as sole webUri + [DataRow("example.com:80", true, "http://example.com/", null)] + [DataRow("example.com:80/test", true, "http://example.com/test", null)] + [DataRow("example.com:80/126", true, "http://example.com/126", null)] + [DataRow("example.com:443", true, "https://example.com/", null)] + [DataRow("example.com:443/test", true, "https://example.com/test", null)] + [DataRow("example.com:443/126", true, "https://example.com/126", null)] + [DataRow("google.com:91", true, "https://google.com:91/", null)] + [DataRow("google.com:91/test", true, "https://google.com:91/test", null)] + [DataRow("google.com:91/126", true, "https://google.com:91/126", null)] + [DataRow("test.test.test.test:952", true, "https://test.test.test.test:952/", null)] + [DataRow("test.test.test.test:952/test", true, "https://test.test.test.test:952/test", null)] + [DataRow("test.test.test.test:952/126", true, "https://test.test.test.test:952/126", null)] + + // Following cases can be both interpreted as schema:path and domain:port + [DataRow("tel:411", true, "https://tel:411/", "tel:411")] + [DataRow("tel:70421567", true, null, "tel:70421567")] + [DataRow("tel:863-1234", true, null, "tel:863-1234")] + [DataRow("tel:+1-201-555-0123", true, null, "tel:+1-201-555-0123")] + + // All following cases should be parsed as application URI + [DataRow("mailto:", true, null, "mailto:")] + [DataRow("mailto:/", false, null, null)] + [DataRow("mailto:example@mail.com", true, null, "mailto:example@mail.com")] + [DataRow("ms-settings:", true, null, "ms-settings:")] + [DataRow("ms-settings:/", false, null, null)] + [DataRow("ms-settings://", false, null, null)] + [DataRow("ms-settings://privacy", true, null, "ms-settings://privacy/")] + [DataRow("ms-settings://privacy/", true, null, "ms-settings://privacy/")] + [DataRow("ms-settings:privacy", true, null, "ms-settings:privacy")] + [DataRow("ms-settings:powersleep", true, null, "ms-settings:powersleep")] + [DataRow("microsoft-edge:google.com", true, null, "microsoft-edge:google.com")] + [DataRow("microsoft-edge:google.com/", true, null, "microsoft-edge:google.com/")] + [DataRow("microsoft-edge:google.com:80/", true, null, "microsoft-edge:google.com:80/")] + [DataRow("microsoft-edge:google.com:443/", true, null, "microsoft-edge:google.com:443/")] + [DataRow("microsoft-edge:google.com:1234/", true, null, "microsoft-edge:google.com:1234/")] + [DataRow("microsoft-edge:http://google.com", true, null, "microsoft-edge:http://google.com")] + [DataRow("microsoft-edge:http://google.com:80", true, null, "microsoft-edge:http://google.com:80")] + [DataRow("microsoft-edge:http://google.com:443", true, null, "microsoft-edge:http://google.com:443")] + [DataRow("microsoft-edge:http://google.com:1234", true, null, "microsoft-edge:http://google.com:1234")] + [DataRow("microsoft-edge:https://google.com", true, null, "microsoft-edge:https://google.com")] + [DataRow("microsoft-edge:https://google.com:80", true, null, "microsoft-edge:https://google.com:80")] + [DataRow("microsoft-edge:https://google.com:443", true, null, "microsoft-edge:https://google.com:443")] + [DataRow("microsoft-edge:https://google.com:1234", true, null, "microsoft-edge:https://google.com:1234")] + [DataRow("ftp://user:password@localhost:8080", true, null, "ftp://user:password@localhost:8080/")] + [DataRow("ftp://user:password@localhost:8080/", true, null, "ftp://user:password@localhost:8080/")] + [DataRow("ftp://user:password@google.com", true, null, "ftp://user:password@google.com/")] + [DataRow("ftp://user:password@google.com:2121", true, null, "ftp://user:password@google.com:2121/")] + [DataRow("ftp://user:password@1.1.1.1", true, null, "ftp://user:password@1.1.1.1/")] + [DataRow("ftp://user:password@1.1.1.1:2121", true, null, "ftp://user:password@1.1.1.1:2121/")] + [DataRow("ftp://example.com", true, null, "ftp://example.com/")] + [DataRow("ftp://example.com/test", true, null, "ftp://example.com/test")] + [DataRow("ftp://example.com:123/test", true, null, "ftp://example.com:123/test")] + [DataRow("ftp://example.com/126", true, null, "ftp://example.com/126")] + [DataRow("^:", false, null, null)] + + public void ParserReturnsExpectedResults(string query, bool expectedSuccess, string expectedWebUri, string expectedSystemUri) { // Arrange var parser = new ExtendedUriParser(); // Act - var success = parser.TryParse(query, out var result, out var isWebUriResult); + var success = parser.TryParse(query, out var webUriResult, out var systemUriResult); // Assert - Assert.AreEqual(expectedResult, result?.ToString()); - Assert.AreEqual(expectedIsWebUri, isWebUriResult); + Assert.AreEqual(expectedWebUri, webUriResult?.ToString()); + Assert.AreEqual(expectedSystemUri, systemUriResult?.ToString()); Assert.AreEqual(expectedSuccess, success); } } 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 bb72ae3241..90767175c6 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, out bool isWebUri); + bool TryParse(string input, out System.Uri webUri, out System.Uri systemUri); } } diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs index efb68a867c..c073520924 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs @@ -50,7 +50,7 @@ namespace Microsoft.Plugin.Uri { results.Add(new Result { - Title = Properties.Resources.Microsoft_plugin_uri_open, + Title = Properties.Resources.Microsoft_plugin_uri_default_browser, SubTitle = BrowserInfo.Path, IcoPath = DefaultIconPath, Action = action => @@ -70,34 +70,54 @@ namespace Microsoft.Plugin.Uri } if (!string.IsNullOrEmpty(query?.Search) - && _uriParser.TryParse(query.Search, out var uriResult, out var isWebUri) - && _uriResolver.IsValidHost(uriResult)) + && _uriParser.TryParse(query.Search, out var webUriResult, out var systemUriResult) + && _uriResolver.IsValidHost(webUriResult)) { - var uriResultString = uriResult.ToString(); - var isWebUriBool = isWebUri; - - results.Add(new Result + if (webUriResult is not null) { - Title = uriResultString, - SubTitle = isWebUriBool - ? Properties.Resources.Microsoft_plugin_uri_website - : Properties.Resources.Microsoft_plugin_uri_open, - IcoPath = isWebUriBool && BrowserInfo.IconPath != null - ? BrowserInfo.IconPath - : DefaultIconPath, - Action = action => + var resultString = webUriResult.ToString(); + results.Add(new Result { - if (!Helper.OpenInShell(uriResultString)) + Title = resultString, + SubTitle = Properties.Resources.Microsoft_plugin_uri_website, + IcoPath = BrowserInfo.IconPath, + Action = action => { - var title = $"Plugin: {Properties.Resources.Microsoft_plugin_uri_plugin_name}"; - var message = $"{Properties.Resources.Microsoft_plugin_uri_open_failed}: {uriResultString}"; - Context.API.ShowMsg(title, message); - return false; - } + if (!Helper.OpenInShell(resultString)) + { + var title = $"Plugin: {Properties.Resources.Microsoft_plugin_uri_plugin_name}"; + var message = $"{Properties.Resources.Microsoft_plugin_uri_open_failed}: {resultString}"; + Context.API.ShowMsg(title, message); + return false; + } - return true; - }, - }); + return true; + }, + }); + } + + if (systemUriResult is not null) + { + var resultString = systemUriResult.ToString(); + results.Add(new Result + { + Title = resultString, + SubTitle = Properties.Resources.Microsoft_plugin_uri_open, + IcoPath = DefaultIconPath, + Action = action => + { + if (!Helper.OpenInShell(resultString)) + { + var title = $"Plugin: {Properties.Resources.Microsoft_plugin_uri_plugin_name}"; + var message = $"{Properties.Resources.Microsoft_plugin_uri_open_failed}: {resultString}"; + Context.API.ShowMsg(title, message); + return false; + } + + return true; + }, + }); + } } return results; 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 b30aece9ed..8040e612e9 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs @@ -12,18 +12,19 @@ namespace Microsoft.Plugin.Uri.UriHelper public class ExtendedUriParser : IUriParser { // When updating this method, also update the local method IsUri() in Community.PowerToys.Run.Plugin.WebSearch.Main.Query - public bool TryParse(string input, out System.Uri result, out bool isWebUri) + public bool TryParse(string input, out System.Uri webUri, out System.Uri systemUri) { + webUri = default; + systemUri = default; + 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 - // And check if scheme match REC3986 (issue #15035) + // And check if scheme match RFC3986 (issue #15035) const string schemeRegex = @"^([a-z][a-z0-9+\-.]*):"; if (input.EndsWith(":", StringComparison.OrdinalIgnoreCase) && !input.StartsWith("http", StringComparison.OrdinalIgnoreCase) @@ -31,8 +32,7 @@ namespace Microsoft.Plugin.Uri.UriHelper && !input.All(char.IsDigit) && Regex.IsMatch(input, schemeRegex)) { - result = new System.Uri(input); - isWebUri = false; + systemUri = new System.Uri(input); return true; } @@ -44,42 +44,66 @@ namespace Microsoft.Plugin.Uri.UriHelper || input.EndsWith("://", StringComparison.CurrentCulture) || input.All(char.IsDigit)) { - result = default; - isWebUri = false; return false; } try { + string isDomainPortRegex = @"^[\w\.]+:\d+"; + string isIPv6PortRegex = @"^\[([\w:]+:+)+[\w]+\]:\d+"; var urlBuilder = new UriBuilder(input); - var hadDefaultPort = urlBuilder.Uri.IsDefaultPort; - urlBuilder.Port = hadDefaultPort ? -1 : urlBuilder.Port; + urlBuilder.Port = urlBuilder.Uri.IsDefaultPort ? -1 : urlBuilder.Port; if (input.StartsWith("HTTP://", StringComparison.OrdinalIgnoreCase)) { urlBuilder.Scheme = System.Uri.UriSchemeHttp; - isWebUri = true; + } + else if (Regex.IsMatch(input, isDomainPortRegex) || + Regex.IsMatch(input, isIPv6PortRegex)) + { + var secondUrlBuilder = urlBuilder; + + try + { + urlBuilder = new UriBuilder("https://" + input); + + if (urlBuilder.Port == 80) + { + urlBuilder.Scheme = System.Uri.UriSchemeHttp; + } + } + catch (UriFormatException) + { + // This handles the situation in tel:xxxx and others + // When xxxx > 65535, it will throw UriFormatException + // The catch ensures it will at least still try to return a systemUri + } + + string singleLabelRegex = @"[\.:]+|^http$|^https$|^localhost$"; + systemUri = Regex.IsMatch(urlBuilder.Host, singleLabelRegex) ? null : secondUrlBuilder.Uri; } else if (input.Contains(':', StringComparison.OrdinalIgnoreCase) && !input.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !input.Contains('[', StringComparison.OrdinalIgnoreCase)) { // Do nothing, leave unchanged - isWebUri = false; + systemUri = urlBuilder.Uri; } else { urlBuilder.Scheme = System.Uri.UriSchemeHttps; - isWebUri = true; } - result = urlBuilder.Uri; + if (urlBuilder.Scheme.Equals(System.Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + urlBuilder.Scheme.Equals(System.Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + webUri = urlBuilder.Uri; + } + return true; } catch (UriFormatException) { - result = default; - isWebUri = false; return false; } }