-
Notifications
You must be signed in to change notification settings - Fork 2k
Consolidate file path resolution for WebView controls #34620
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
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2b1b2b8
Consolidate file path resolution for WebView controls
mattleibow 30fbdf1
Fix path resolution edge cases and add unit tests
mattleibow 98cbf91
Fix CI: revert Tizen changes, use OrdinalIgnoreCase for netstandard c…
mattleibow 7251b4e
Remove HostPage_LoadsSuccessfully test that cannot work in BlazorWebView
mattleibow 656782a
Add positive test for known-good framework asset in BlazorWebView
mattleibow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.ContentRootResolution.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| using System; | ||
| using System.Text.Json.Serialization; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.AspNetCore.Components.WebView.Maui; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Maui.MauiBlazorWebView.DeviceTests.Components; | ||
| using Xunit; | ||
|
|
||
| namespace Microsoft.Maui.MauiBlazorWebView.DeviceTests.Elements; | ||
|
|
||
| /// <summary> | ||
| /// Tests for URL-to-file path resolution in BlazorWebView. | ||
| /// Path.Combine ignores the first argument when the second starts with a path | ||
| /// separator, which can cause incorrect file resolution. These tests verify that | ||
| /// file resolution correctly handles various URL path formats. | ||
| /// </summary> | ||
| public partial class BlazorWebViewTests | ||
| { | ||
| public class UrlResolutionResult | ||
| { | ||
| public int status { get; set; } | ||
| public int bodyLength { get; set; } | ||
| public string bodyPreview { get; set; } = ""; | ||
| public string url { get; set; } = ""; | ||
| } | ||
|
|
||
| [JsonSerializable(typeof(UrlResolutionResult))] | ||
| internal partial class UrlResolutionJsonContext : JsonSerializerContext | ||
| { | ||
| } | ||
|
|
||
| private async Task RunUrlResolutionTest(string path, string mode, Action<UrlResolutionResult> assertion) | ||
| { | ||
| await RunTest(async (blazorWebView, handler) => | ||
| { | ||
| var jsPath = path.Replace("'", "\\'", StringComparison.Ordinal); | ||
| var result = await WebViewHelpers.ExecuteAsyncScriptAndWaitForResult<UrlResolutionResult>( | ||
| handler.PlatformView, | ||
| "try {" + | ||
| " let url;" + | ||
| " if ('" + mode + "' === 'origin') {" + | ||
| " url = window.location.origin + '" + jsPath + "';" + | ||
| " } else {" + | ||
| " url = '" + jsPath + "';" + | ||
| " }" + | ||
| " const response = await fetch(url);" + | ||
| " const body = await response.text();" + | ||
| " return {" + | ||
| " status: response.status," + | ||
| " bodyLength: body.length," + | ||
| " bodyPreview: body.substring(0, 200)," + | ||
| " url: url" + | ||
| " };" + | ||
| "} catch (e) {" + | ||
| " let url = ('" + mode + "' === 'origin') ? window.location.origin + '" + jsPath + "' : '" + jsPath + "';" + | ||
| " return {" + | ||
| " status: -1," + | ||
| " bodyLength: 0," + | ||
| " bodyPreview: e.toString()," + | ||
| " url: url" + | ||
| " };" + | ||
| "}"); | ||
|
|
||
| Assert.NotNull(result); | ||
| assertion(result); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if the response is the host page (SPA fallback) rather than actual file content. | ||
| /// BlazorWebView returns the host page for extensionless paths as part of SPA routing. | ||
| /// </summary> | ||
| static bool IsSpaFallback(UrlResolutionResult result) => | ||
| result.bodyPreview.Contains("blazor.webview.js", StringComparison.Ordinal) || | ||
| result.bodyPreview.Contains("testhtmlloaded", StringComparison.Ordinal) || | ||
| result.bodyPreview.Contains("There is no content at", StringComparison.Ordinal); | ||
|
|
||
| // NOTE: No HostPage_LoadsSuccessfully test here because the Blazor host page | ||
| // is only served for navigation requests (ResourceContext.Document), not for | ||
| // fetch() requests. RunTest already verifies the host page loads by waiting | ||
| // for the Blazor component to render before running the test lambda. | ||
|
|
||
| // ============================================================ | ||
| // Positive test — a known-good asset must still load after the | ||
| // path-hardening changes so we don't accidentally block legit | ||
| // requests | ||
| // ============================================================ | ||
|
|
||
| [Fact] | ||
| public Task Blazor_KnownFrameworkAsset_LoadsSuccessfully() => | ||
| RunUrlResolutionTest("_framework/blazor.webview.js", "relative", result => | ||
| { | ||
| Assert.Equal(200, result.status); | ||
| Assert.True(result.bodyLength > 0, "Framework script should return content"); | ||
| }); | ||
|
|
||
| // ============================================================ | ||
| // Rooted paths — Path.Combine drops the root when the second | ||
| // argument starts with a separator, so these should not resolve | ||
| // ============================================================ | ||
|
|
||
| [Theory] | ||
| [InlineData("//images/logo.png")] | ||
| [InlineData("//data/readme.txt")] | ||
| [InlineData("//content/page.html")] | ||
| public Task Blazor_RootedPath_DoesNotResolve(string path) => | ||
| RunUrlResolutionTest(path, "origin", result => | ||
| { | ||
| Assert.True( | ||
| result.status != 200 || IsSpaFallback(result), | ||
| $"Path '{path}' unexpectedly returned content (status={result.status}, length={result.bodyLength})"); | ||
| }); | ||
|
|
||
| // ============================================================ | ||
| // Dot-dot segments — should not resolve above content root | ||
| // ============================================================ | ||
|
|
||
| [Theory] | ||
| [InlineData("../readme.txt")] | ||
| [InlineData("../../data/config.txt")] | ||
| [InlineData("subfolder/../../readme.txt")] | ||
| public Task Blazor_DotDotSegments_DoNotResolveAboveRoot(string path) => | ||
| RunUrlResolutionTest(path, "relative", result => | ||
| { | ||
| Assert.True( | ||
| result.status != 200 || IsSpaFallback(result), | ||
| $"Path '{path}' unexpectedly returned content (status={result.status}, length={result.bodyLength})"); | ||
| }); | ||
|
|
||
| // ============================================================ | ||
| // Encoded separators — should not affect path resolution | ||
| // ============================================================ | ||
|
|
||
| [Theory] | ||
| [InlineData("%2F%2Fimages%2Flogo.png")] | ||
| [InlineData("%2e%2e/readme.txt")] | ||
| public Task Blazor_EncodedSeparators_DoNotAffectResolution(string path) => | ||
| RunUrlResolutionTest(path, "relative", result => | ||
| { | ||
| Assert.True( | ||
| result.status != 200 || IsSpaFallback(result), | ||
| $"Path '{path}' unexpectedly returned content (status={result.status}, length={result.bodyLength})"); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
...rols/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_ContentRootResolution.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| #nullable enable | ||
| using System; | ||
| using System.Text.Json.Serialization; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Maui.Controls; | ||
| using Xunit; | ||
|
|
||
| namespace Microsoft.Maui.DeviceTests; | ||
|
|
||
| /// <summary> | ||
| /// Tests for URL-to-file path resolution in HybridWebView. | ||
| /// Path.Combine ignores the first argument when the second starts with a path | ||
| /// separator, which can cause incorrect file resolution. These tests verify that | ||
| /// file resolution correctly handles various URL path formats. | ||
| /// </summary> | ||
| [Category(TestCategory.HybridWebView)] | ||
| #if WINDOWS | ||
| [Collection(WebViewsCollection)] | ||
| #endif | ||
| public partial class HybridWebViewTests_ContentRootResolution : HybridWebViewTestsBase | ||
| { | ||
| public class UrlResolutionResult | ||
| { | ||
| public int status { get; set; } | ||
| public int bodyLength { get; set; } | ||
| public string bodyPreview { get; set; } = ""; | ||
| public string url { get; set; } = ""; | ||
| } | ||
|
|
||
| [JsonSourceGenerationOptions(WriteIndented = true)] | ||
| [JsonSerializable(typeof(UrlResolutionResult))] | ||
| [JsonSerializable(typeof(string))] | ||
| internal partial class UrlResolutionJsonContext : JsonSerializerContext | ||
| { | ||
| } | ||
|
|
||
| private Task RunUrlResolutionTest(string path, string mode, Action<UrlResolutionResult> assertion) => | ||
| RunTest("urlresolution.html", async (hybridWebView) => | ||
| { | ||
| var result = await hybridWebView.InvokeJavaScriptAsync<UrlResolutionResult>( | ||
| "TestUrlResolution", | ||
| UrlResolutionJsonContext.Default.UrlResolutionResult, | ||
| [path, mode], | ||
| [UrlResolutionJsonContext.Default.String, UrlResolutionJsonContext.Default.String]); | ||
|
|
||
| Assert.NotNull(result); | ||
| assertion(result); | ||
| }); | ||
|
|
||
| // ============================================================ | ||
| // Relative paths — files inside content root resolve correctly | ||
| // ============================================================ | ||
|
|
||
| [Theory] | ||
| [InlineData("index.html")] | ||
| [InlineData("urlresolution.html")] | ||
| [InlineData("safe-file.txt")] | ||
| public Task RelativePaths_ResolveToContent(string path) => | ||
| RunUrlResolutionTest(path, "relative", result => | ||
| { | ||
| Assert.Equal(200, result.status); | ||
| Assert.True(result.bodyLength > 0, $"Expected content for '{path}' but got empty response."); | ||
| }); | ||
|
|
||
| [Fact] | ||
| public Task KnownFile_ReturnsExpectedContent() => | ||
| RunUrlResolutionTest("safe-file.txt", "relative", result => | ||
| { | ||
| Assert.Equal(200, result.status); | ||
| Assert.Contains("content directory", result.bodyPreview, StringComparison.Ordinal); | ||
| }); | ||
|
|
||
| // ============================================================ | ||
| // Rooted paths — Path.Combine drops the root when the second | ||
| // argument starts with a separator, so these should not resolve | ||
| // ============================================================ | ||
|
|
||
| [Theory] | ||
| [InlineData("//images/logo.png")] | ||
| [InlineData("//data/readme.txt")] | ||
| [InlineData("//content/page.html")] | ||
| public Task RootedPath_DoesNotResolve(string path) => | ||
| RunUrlResolutionTest(path, "origin", result => | ||
| { | ||
| Assert.NotEqual(200, result.status); | ||
| }); | ||
|
|
||
| // ============================================================ | ||
| // Dot-dot segments — should not resolve above content root | ||
| // ============================================================ | ||
|
|
||
| [Theory] | ||
| [InlineData("../readme.txt")] | ||
| [InlineData("../../data/config.txt")] | ||
| [InlineData("subfolder/../../readme.txt")] | ||
| public Task DotDotSegments_DoNotResolveAboveRoot(string path) => | ||
| RunUrlResolutionTest(path, "relative", result => | ||
| { | ||
| Assert.NotEqual(200, result.status); | ||
| }); | ||
|
|
||
| // ============================================================ | ||
| // Encoded separators — should not affect path resolution | ||
| // ============================================================ | ||
|
|
||
| [Theory] | ||
| [InlineData("%2F%2Fimages%2Flogo.png")] | ||
| [InlineData("%2e%2e/readme.txt")] | ||
| public Task EncodedSeparators_DoNotAffectResolution(string path) => | ||
| RunUrlResolutionTest(path, "relative", result => | ||
| { | ||
| Assert.NotEqual(200, result.status); | ||
| }); | ||
| } |
1 change: 1 addition & 0 deletions
1
src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/safe-file.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| This file is inside the HybridRoot content directory. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.