Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.SystemWebAdapters.Features;
public interface IHttpBrowserCapabilityFeature
{
string? this[string key] { get; }

bool IsBrowser(string browserName);
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -652,18 +652,19 @@ private bool UcbrowserProcess(string userAgent, ParsedBrowserResult dictionary)

private sealed class ParsedBrowserResult : Dictionary<string, string?>, IHttpBrowserCapabilityFeature
{
private readonly HashSet<string> _browsers = new(StringComparer.OrdinalIgnoreCase);

public ParsedBrowserResult()
: base(StringComparer.OrdinalIgnoreCase)
{
}

[Conditional("NotNeededYet")]
[Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Retained in case we need the data later")]
public void AddBrowser(string browser)
{
}
public void AddBrowser(string browser) => _browsers.Add(browser);

string? IHttpBrowserCapabilityFeature.this[string key] => TryGetValue(key, out var value) ? value : null;

bool IHttpBrowserCapabilityFeature.IsBrowser(string browserName)
=> !string.IsNullOrEmpty(browserName) && _browsers.Contains(browserName);
}

private readonly struct RegexResult
Expand Down Expand Up @@ -724,5 +725,7 @@ private sealed class EmptyBrowserFeatures : IHttpBrowserCapabilityFeature
public static IHttpBrowserCapabilityFeature Instance { get; } = new EmptyBrowserFeatures();

public string? this[string key] => null;

public bool IsBrowser(string browserName) => false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ private IHttpBrowserCapabilityFeature Capability

public bool IsMobileDevice => GetBoolean("isMobileDevice");

public bool IsBrowser(string browserName) => Capability.IsBrowser(browserName);

private int GetInt(string key) => int.TryParse(Capability[key], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? result : throw new HttpUnhandledException($"Invalid string from browser capabilities '{key}'");

private bool GetBoolean(string key) => bool.TryParse(Capability[key], out var result) && result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public class HttpBrowserCapabilitiesBase
public virtual bool Crawler => throw new NotImplementedException();

public virtual bool IsMobileDevice => throw new NotImplementedException();

public virtual bool IsBrowser(string browserName) => throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ public HttpBrowserCapabilitiesWrapper(HttpBrowserCapabilities capabilities)
public override bool Crawler => _capabilities.Crawler;

public override bool IsMobileDevice => _capabilities.IsMobileDevice;

public override bool IsBrowser(string browserName) => _capabilities.IsBrowser(browserName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,113 @@ public static IEnumerable<object[]> UserAgentTestData
};
}
}

[Theory]
[MemberData(nameof(IsBrowserTestData))]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Used for tests")]
public void IsBrowserMatchesHierarchy(string userAgent, string[] expectedMatches, string[] expectedNonMatches)
{
// Arrange
var browserFactory = new BrowserCapabilitiesFactory();

// Act
var result = browserFactory.Parse(userAgent);

// Assert
foreach (var name in expectedMatches)
{
Assert.True(result.IsBrowser(name), $"Expected IsBrowser(\"{name}\") to be true for '{userAgent}'");
}

foreach (var name in expectedNonMatches)
{
Assert.False(result.IsBrowser(name), $"Expected IsBrowser(\"{name}\") to be false for '{userAgent}'");
}
}

[Theory]
[InlineData("")]
[InlineData(null)]
public void IsBrowserRejectsEmpty(string? browserName)
{
// Arrange
var browserFactory = new BrowserCapabilitiesFactory();

// Act
var result = browserFactory.Parse(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36");

// Assert
Assert.False(result.IsBrowser(browserName!));
}

[Fact]
public void IsBrowserCaseInsensitive()
{
// Arrange
var browserFactory = new BrowserCapabilitiesFactory();

// Act
var result = browserFactory.Parse(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0");

// Assert
Assert.True(result.IsBrowser("firefox"));
Assert.True(result.IsBrowser("FIREFOX"));
Assert.True(result.IsBrowser("Firefox"));
}

public static IEnumerable<object[]> IsBrowserTestData
{
get
{
// Chrome desktop -> Default, Mozilla, WebKit, Chrome
yield return new object[]
{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36",
new[] { "Default", "Mozilla", "WebKit", "Chrome" },
new[] { "Firefox", "IE", "Safari", "Opera" },
};

// Firefox desktop -> Default, Mozilla, Firefox
yield return new object[]
{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
new[] { "Default", "Mozilla", "Firefox" },
new[] { "Chrome", "WebKit", "IE", "Safari" },
};

// MSIE 10 -> Default, Mozilla, IE
yield return new object[]
{
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0; MDDCJS)",
new[] { "Default", "Mozilla", "IE" },
new[] { "Chrome", "Firefox", "WebKit", "Safari" },
};

// iPhone CriOS (no "Chrome/" token in UA, Safari branch wins) -> Default, Mozilla, WebKit, Safari
yield return new object[]
{
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/102.0.5005.87 Mobile/15E148 Safari/604.1",
new[] { "Default", "Mozilla", "WebKit", "Safari" },
new[] { "Chrome", "Firefox", "IE" },
};

// Opera (handled before Mozilla, no Mozilla node added) -> Default, Opera
yield return new object[]
{
"Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14",
new[] { "Default", "Opera" },
new[] { "Mozilla", "Chrome", "Firefox", "IE" },
};

// Empty UA via Parse still runs DefaultProcess -> only Default is added
yield return new object[]
{
string.Empty,
new[] { "Default" },
new[] { "Mozilla", "Chrome", "Firefox", "IE" },
};
}
}
}