[PT Run] Support for application URI (#14391)
* [RUN] Add support for uri with scheme only * Fix typo * Add full support for application URI * Apply suggestions from code review and add tests * [PT Run] Add support for application uri * Update error message * Adapt the icon if the result is web URI * Update icons for application URI * Update icons for application URI (dark mode) * Update icon
This commit is contained in:
parent
2128b88571
commit
c934127d84
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.8 KiB |
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -70,7 +70,7 @@ namespace Microsoft.Plugin.Uri.Properties {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open Default Browser.
|
||||
/// Looks up a localized string similar to Open default browser.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_uri_default_browser {
|
||||
get {
|
||||
|
@ -79,7 +79,16 @@ namespace Microsoft.Plugin.Uri.Properties {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to open URL.
|
||||
/// Looks up a localized string similar to Open URI.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_uri_open {
|
||||
get {
|
||||
return ResourceManager.GetString("Microsoft_plugin_uri_open", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to open URI.
|
||||
/// </summary>
|
||||
public static string Microsoft_plugin_uri_open_failed {
|
||||
get {
|
||||
|
|
|
@ -123,8 +123,11 @@
|
|||
<data name="Microsoft_plugin_uri_default_browser" xml:space="preserve">
|
||||
<value>Open default browser</value>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_uri_open" xml:space="preserve">
|
||||
<value>Open URI</value>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_uri_open_failed" xml:space="preserve">
|
||||
<value>Failed to open URL</value>
|
||||
<value>Failed to open URI</value>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_uri_plugin_description" xml:space="preserve">
|
||||
<value>Opens URLs and UNC network shares.</value>
|
||||
|
@ -135,4 +138,4 @@
|
|||
<data name="Microsoft_plugin_uri_website" xml:space="preserve">
|
||||
<value>Open in default browser</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue