Skip to content

chore: update URLMatch to follow upstream #3150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 23, 2025
Merged
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
106 changes: 80 additions & 26 deletions src/Playwright.Tests/InterceptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/

using System.Net;
using System.Text.RegularExpressions;
using Microsoft.Playwright.Helpers;

namespace Microsoft.Playwright.Tests;
Expand All @@ -32,32 +33,85 @@ public class InterceptionTests : PageTestEx
[PlaywrightTest("interception.spec.ts", "should work with glob")]
public void ShouldWorkWithGlob()
{
Assert.That("https://localhost:8080/foo.js", Does.Match(StringExtensions.GlobToRegex("**/*.js")));
Assert.That("https://localhost:8080/foo.js", Does.Not.Match(StringExtensions.GlobToRegex("**/*.css")));
Assert.That("https://localhost:8080/foo.js", Does.Not.Match(StringExtensions.GlobToRegex("*.js")));
Assert.That("https://localhost:8080/foo.js", Does.Match(StringExtensions.GlobToRegex("https://**/*.js")));
Assert.That("http://localhost:8080/simple/path.js", Does.Match(StringExtensions.GlobToRegex("http://localhost:8080/simple/path.js")));
Assert.That("http://localhost:8080/Simple/path.js", Does.Match(StringExtensions.GlobToRegex("http://localhost:8080/?imple/path.js")));
Assert.That("https://localhost:8080/a.js", Does.Match(StringExtensions.GlobToRegex("**/{a,b}.js")));
Assert.That("https://localhost:8080/b.js", Does.Match(StringExtensions.GlobToRegex("**/{a,b}.js")));
Assert.That("https://localhost:8080/c.js", Does.Not.Match(StringExtensions.GlobToRegex("**/{a,b}.js")));
Assert.That("https://localhost:8080/c.jpg", Does.Match(StringExtensions.GlobToRegex("**/*.{png,jpg,jpeg}")));
Assert.That("https://localhost:8080/c.jpeg", Does.Match(StringExtensions.GlobToRegex("**/*.{png,jpg,jpeg}")));
Assert.That("https://localhost:8080/c.png", Does.Match(StringExtensions.GlobToRegex("**/*.{png,jpg,jpeg}")));
Assert.That("https://localhost:8080/c.css", Does.Not.Match(StringExtensions.GlobToRegex("**/*.{png,jpg,jpeg}")));
Assert.That("foo.js", Does.Match(StringExtensions.GlobToRegex("foo*")));
Assert.That("foo/bar.js", Does.Not.Match(StringExtensions.GlobToRegex("foo*")));
Assert.That("http://localhost:3000/signin-oidc/foo", Does.Not.Match(StringExtensions.GlobToRegex("http://localhost:3000/signin-oidc*")));
Assert.That("http://localhost:3000/signin-oidcnice", Does.Match(StringExtensions.GlobToRegex("http://localhost:3000/signin-oidc*")));

Assert.That("http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah", Does.Match(StringExtensions.GlobToRegex("**/three-columns/settings.html?**id=[a-z]**")));

Assert.AreEqual("^\\?$", StringExtensions.GlobToRegex("\\?"));
Assert.AreEqual("^\\\\$", StringExtensions.GlobToRegex("\\"));
Assert.AreEqual("^\\\\$", StringExtensions.GlobToRegex("\\\\"));
Assert.AreEqual("^\\[$", StringExtensions.GlobToRegex("\\["));
Assert.AreEqual("^[a-z]$", StringExtensions.GlobToRegex("[a-z]"));
Assert.AreEqual(@"^\$\^\+\.\*\(\)\|\?\{\}\[\]$", StringExtensions.GlobToRegex("$^+.\\*()|\\?\\{\\}\\[\\]"));
Regex GlobToRegex(string glob)
{
return new Regex(URLMatch.GlobToRegexPattern(glob));
}

bool URLMatches(string baseURL, string url, string glob)
{
return new URLMatch()
{
baseURL = baseURL,
glob = glob,
}.Match(url);
}

Assert.That("https://localhost:8080/foo.js", Does.Match(GlobToRegex("**/*.js")));
Assert.That("https://localhost:8080/foo.js", Does.Not.Match(GlobToRegex("**/*.css")));
Assert.That("https://localhost:8080/foo.js", Does.Not.Match(GlobToRegex("*.js")));
Assert.That("https://localhost:8080/foo.js", Does.Match(GlobToRegex("https://**/*.js")));
Assert.That("http://localhost:8080/simple/path.js", Does.Match(GlobToRegex("http://localhost:8080/simple/path.js")));
Assert.That("https://localhost:8080/a.js", Does.Match(GlobToRegex("**/{a,b}.js")));
Assert.That("https://localhost:8080/b.js", Does.Match(GlobToRegex("**/{a,b}.js")));
Assert.That("https://localhost:8080/c.js", Does.Not.Match(GlobToRegex("**/{a,b}.js")));
Assert.That("https://localhost:8080/c.jpg", Does.Match(GlobToRegex("**/*.{png,jpg,jpeg}")));
Assert.That("https://localhost:8080/c.jpeg", Does.Match(GlobToRegex("**/*.{png,jpg,jpeg}")));
Assert.That("https://localhost:8080/c.png", Does.Match(GlobToRegex("**/*.{png,jpg,jpeg}")));
Assert.That("https://localhost:8080/c.css", Does.Not.Match(GlobToRegex("**/*.{png,jpg,jpeg}")));
Assert.That("foo.js", Does.Match(GlobToRegex("foo*")));
Assert.That("foo/bar.js", Does.Not.Match(GlobToRegex("foo*")));
Assert.That("http://localhost:3000/signin-oidc/foo", Does.Not.Match(GlobToRegex("http://localhost:3000/signin-oidc*")));
Assert.That("http://localhost:3000/signin-oidcnice", Does.Match(GlobToRegex("http://localhost:3000/signin-oidc*")));

// range [] is NOT supported
Assert.That("http://example.com/api/v[0-9]", Does.Match(GlobToRegex("**/api/v[0-9]")));
Assert.That("http://example.com/api/version", Does.Not.Match(GlobToRegex("**/api/v[0-9]")));

// query params
Assert.That("http://example.com/api?param", Does.Match(GlobToRegex("**/api\\?param")));
Assert.That("http://example.com/api-param", Does.Not.Match(GlobToRegex("**/api\\?param")));
Assert.That("http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah", Does.Match(GlobToRegex("**/three-columns/settings.html\\?**id=settings-**")));

Assert.AreEqual("^\\?$", URLMatch.GlobToRegexPattern("\\?"));
Assert.AreEqual("^\\\\$", URLMatch.GlobToRegexPattern("\\"));
Assert.AreEqual("^\\\\$", URLMatch.GlobToRegexPattern("\\\\"));
Assert.AreEqual("^\\[$", URLMatch.GlobToRegexPattern("\\["));
Assert.AreEqual("^\\[a-z\\]$", URLMatch.GlobToRegexPattern("[a-z]"));
Assert.AreEqual(@"^\$\^\+\.\*\(\)\|\?\{\}\[\]$", URLMatch.GlobToRegexPattern("$^+.\\*()|\\?\\{\\}\\[\\]"));

Assert.True(URLMatches(null, "http://playwright.dev/", "http://playwright.dev"));
Assert.True(URLMatches(null, "http://playwright.dev/?a=b", "http://playwright.dev?a=b"));
Assert.True(URLMatches(null, "http://playwright.dev/", "h*://playwright.dev"));
Assert.True(URLMatches(null, "http://api.playwright.dev/?x=y", "http://*.playwright.dev?x=y"));
Assert.True(URLMatches(null, "http://playwright.dev/foo/bar", "**/foo/**"));
Assert.True(URLMatches("http://playwright.dev", "http://playwright.dev/?x=y", "?x=y"));
Assert.True(URLMatches("http://playwright.dev/foo/", "http://playwright.dev/foo/bar?x=y", "./bar?x=y"));

// This is not supported, we treat ? as a query separator.
Assert.That("http://localhost:8080/Simple/path.js", Does.Not.Match(GlobToRegex("http://localhost:8080/?imple/path.js")));
Assert.False(URLMatches(null, "http://playwright.dev/", "http://playwright.?ev"));
Assert.True(URLMatches(null, "http://playwright./?ev", "http://playwright.?ev"));
Assert.False(URLMatches(null, "http://playwright.dev/foo", "http://playwright.dev/f??"));
Assert.True(URLMatches(null, "http://playwright.dev/f??", "http://playwright.dev/f??"));
Assert.True(URLMatches(null, "http://playwright.dev/?x=y", "http://playwright.dev\\?x=y"));
Assert.True(URLMatches(null, "http://playwright.dev/?x=y", "http://playwright.dev/\\?x=y"));
Assert.True(URLMatches("http://playwright.dev/foo", "http://playwright.dev/foo?bar", "?bar"));
Assert.True(URLMatches("http://playwright.dev/foo", "http://playwright.dev/foo?bar", "\\\\?bar"));
Assert.True(URLMatches("http://first.host/", "http://second.host/foo", "**/foo"));
Assert.True(URLMatches("http://playwright.dev/", "http://localhost/", "*//localhost/"));
}

[PlaywrightTest("interception.spec.ts", "should intercept by glob")]
public async Task ShouldInterceptByGlob()
{
await Page.GotoAsync(Server.EmptyPage);
await Page.RouteAsync("http://localhos**?*oo", (route) =>
{
return route.FulfillAsync(new() { Status = (int)HttpStatusCode.OK, Body = "intercepted" });
});
var result = await Page.EvaluateAsync<string>("url => fetch(url).then(r => r.text())", Server.Prefix + "/?foo");
Assert.AreEqual("intercepted", result);
}

[PlaywrightTest("interception.spec.ts", "should work with ignoreHTTPSErrors")]
Expand Down
3 changes: 2 additions & 1 deletion src/Playwright/Core/BrowserContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ private async Task UnrouteAsync(string globMatch, Regex reMatch, Func<string, bo
var remaining = new List<RouteHandler>();
foreach (var routeHandler in _routes)
{
if (routeHandler.urlMatcher.Equals(globMatch, reMatch, funcMatch, Options.BaseURL) && (handler == null || routeHandler.Handler == handler))
if (routeHandler.urlMatcher.Equals(globMatch, reMatch, funcMatch, Options.BaseURL, false) && (handler == null || routeHandler.Handler == handler))
{
removed.Add(routeHandler);
}
Expand Down Expand Up @@ -934,6 +934,7 @@ private Task RouteWebSocketAsync(string globMatch, Regex reMatch, Func<string, b
glob = globMatch,
re = reMatch,
func = funcMatch,
isWebSocketUrl = true,
},
Handler = handler,
});
Expand Down
3 changes: 2 additions & 1 deletion src/Playwright/Core/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1262,7 +1262,7 @@ private async Task UnrouteAsync(string globMatch, Regex reMatch, Func<string, bo
var remaining = new List<RouteHandler>();
foreach (var routeHandler in _routes)
{
if (routeHandler.urlMatcher.Equals(globMatch, reMatch, funcMatch, Context.Options.BaseURL) && (handler == null || routeHandler.Handler == handler))
if (routeHandler.urlMatcher.Equals(globMatch, reMatch, funcMatch, Context.Options.BaseURL, false) && (handler == null || routeHandler.Handler == handler))
{
removed.Add(routeHandler);
}
Expand Down Expand Up @@ -1610,6 +1610,7 @@ private Task RouteWebSocketAsync(string globMatch, Regex urlRegex, Func<string,
glob = globMatch,
re = urlRegex,
func = urlFunc,
isWebSocketUrl = true,
},
Handler = handler,
});
Expand Down
91 changes: 0 additions & 91 deletions src/Playwright/Helpers/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ namespace Microsoft.Playwright.Helpers;
/// </summary>
internal static class StringExtensions
{
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
private static readonly char[] _escapeGlobChars = new[] { '$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']' };

private static readonly Dictionary<string, string> _mappings = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
{
{ ".323", "text/h323" },
Expand Down Expand Up @@ -629,94 +626,6 @@ public static Dictionary<string, string> ParseQueryString(this string query)
return result;
}

/// <summary>
/// Converts an url glob expression to a regex.
/// </summary>
/// <param name="glob">Input url.</param>
/// <returns>A Regex with the glob expression.</returns>
public static string GlobToRegex(this string glob)
{
if (string.IsNullOrEmpty(glob))
{
return null;
}

List<string> tokens = new() { "^" };
bool inGroup = false;

for (int i = 0; i < glob.Length; ++i)
{
var c = glob[i];
if (c == '\\' && i + 1 < glob.Length)
{
var @char = glob[++i];
tokens.Add(_escapeGlobChars.Contains(@char) ? "\\" + @char : @char.ToString());
continue;
}
if (c == '*')
{
char? beforeDeep = i == 0 ? null : glob[i - 1];
int starCount = 1;
while (i < glob.Length - 1 && glob[i + 1] == '*')
{
starCount++;
i++;
}

char? afterDeep = i >= glob.Length - 1 ? null : glob[i + 1];
var isDeep = starCount > 1 &&
(beforeDeep == '/' || beforeDeep == null) &&
(afterDeep == '/' || afterDeep == null);
if (isDeep)
{
tokens.Add("((?:[^/]*(?:\\/|$))*)");
i++;
}
else
{
tokens.Add("([^/]*)");
}
continue;
}

switch (c)
{
case '?':
tokens.Add(".");
break;
case '[':
tokens.Add("[");
break;
case ']':
tokens.Add("]");
break;
case '{':
inGroup = true;
tokens.Add("(");
break;
case '}':
inGroup = false;
tokens.Add(")");
break;
case ',':
if (inGroup)
{
tokens.Add("|");
break;
}

tokens.Add("\\" + c);
break;
default:
tokens.Add(_escapeGlobChars.Contains(c) ? "\\" + c : c.ToString());
break;
}
}

tokens.Add("$");
return string.Concat(tokens.ToArray());
}

internal static string GetContentType(this string path)
{
const string defaultContentType = "application/octet-stream";
Expand Down
Loading