Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -1219,14 +1219,18 @@ function Format-Markdown-Output {
$title = $rawTitle.Replace('|', '\|')
$link = "[#$($pr.Number)]($($pr.URL))"
$turnCol = "$($pr.TurnIcon) $($pr.TurnDetail)"
# Wrap author handles in backticks so GitHub renders them as inline code
# rather than as @mentions — this issue is for MAUI team triage tracking,
# not a notification firehose to every PR author each day.
$author = "``@$($pr.Author)``"
if ($showMilestone -and $showTurn) {
[void]$md.AppendLine("| $link | $title | @$($pr.Author) | $($pr.Milestone) | $turnCol | $($pr.Platform) | $($pr.Age)d |")
[void]$md.AppendLine("| $link | $title | $author | $($pr.Milestone) | $turnCol | $($pr.Platform) | $($pr.Age)d |")
} elseif ($showMilestone) {
[void]$md.AppendLine("| $link | $title | @$($pr.Author) | $($pr.Milestone) | $($pr.Platform) | $($pr.Age)d | $($pr.Updated)d ago |")
[void]$md.AppendLine("| $link | $title | $author | $($pr.Milestone) | $($pr.Platform) | $($pr.Age)d | $($pr.Updated)d ago |")
} elseif ($showTurn) {
[void]$md.AppendLine("| $link | $title | @$($pr.Author) | $turnCol | $($pr.Platform) | $($pr.Age)d |")
[void]$md.AppendLine("| $link | $title | $author | $turnCol | $($pr.Platform) | $($pr.Age)d |")
} else {
[void]$md.AppendLine("| $link | $title | @$($pr.Author) | $($pr.Platform) | $($pr.Age)d | $($pr.Updated)d ago |")
[void]$md.AppendLine("| $link | $title | $author | $($pr.Platform) | $($pr.Age)d | $($pr.Updated)d ago |")
}
}
[void]$md.AppendLine("")
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/pr-review-queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ concurrency:
jobs:
generate-report:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
# Guard: only run in dotnet/maui — prevents the scheduled run from firing
# in forks and pinging fork owners with duplicate queue issues.
if: github.repository_owner == 'dotnet' && github.event_name != 'pull_request'
permissions:
contents: read
issues: write
Expand Down Expand Up @@ -86,7 +88,7 @@ jobs:
# Dry-run on PRs: validate the script works without creating issues
validate:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
if: github.repository_owner == 'dotnet' && github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: read
Expand Down
10 changes: 8 additions & 2 deletions eng/pipelines/ci-copilot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ stages:
skipAndroidPlatformApis: true
onlyAndroidPlatformDefaultApis: true
skipAndroidEmulatorImages: ${{ ne(parameters.Platform, 'android') }}
skipAndroidCreateAvds: ${{ ne(parameters.Platform, 'android') }}
# AVD is created by the inline 'Create AVD and boot Android Emulator' script below
# with specific config (playstore image variant, partition shrink, ADB key pre-auth)
# that ProvisionAndroidSdkAvdCreateAvds doesn't replicate.
skipAndroidCreateAvds: true
androidEmulatorApiLevel: '30'
skipSimulatorSetup: ${{ or(eq(parameters.Platform, 'android'), eq(parameters.Platform, 'windows'), eq(parameters.Platform, 'catalyst')) }}
skipCertificates: true
Expand Down Expand Up @@ -871,7 +874,10 @@ stages:
skipAndroidPlatformApis: true
onlyAndroidPlatformDefaultApis: true
skipAndroidEmulatorImages: ${{ ne(parameters.Platform, 'android') }}
skipAndroidCreateAvds: ${{ ne(parameters.Platform, 'android') }}
# AVD is created by the inline 'Create AVD and boot Android Emulator' script below
# with specific config (playstore image variant, partition shrink, ADB key pre-auth)
# that ProvisionAndroidSdkAvdCreateAvds doesn't replicate.
skipAndroidCreateAvds: true
androidEmulatorApiLevel: '30'
skipSimulatorSetup: ${{ or(eq(parameters.Platform, 'android'), eq(parameters.Platform, 'windows'), eq(parameters.Platform, 'catalyst')) }}
skipCertificates: true
Expand Down
9 changes: 9 additions & 0 deletions eng/pipelines/common/variables.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ variables:
value: 26.0.1
- name: POWERSHELL_VERSION
value: 7.4.0
# Workaround for PowerShell/PowerShell#20802 (open since Nov 2023):
# pwsh 7.4.x intermittently aborts at startup on macOS with
# 'Call to procargs failed with errno 5'
# because AttemptExecPwshLogin() races on sysctl(KERN_PROCARGS2).
# Setting __PWSH_LOGIN_CHECKED makes pwsh short-circuit the login-shell
# detection that triggers the racy syscall (see Program.cs in PowerShell repo).
# Hit on internal dnceng dotnet-maui build 2990586 (release/11.0.1xx-preview5).
- name: __PWSH_LOGIN_CHECKED
value: '1'
# Localization variables
- name: LocBranchPrefix
value: 'loc-hb'
Expand Down
50 changes: 50 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue23315.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 23315, "LoadFile in src/Core/src/Platform/iOS/MauiWKWebView.cs ignore directories", PlatformAffected.iOS | PlatformAffected.macOS)]
public class Issue23315 : TestContentPage
{
public Issue23315()
{
}

protected override void Init()
{
var statusLabel = new Label
{
AutomationId = "StatusLabel",
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center
};

WebView webView = new WebView
{
Source = "foo/bar/baz/test.html"
};

webView.Navigated += async (sender, e) =>
{
if (e.Result != WebNavigationResult.Success)
{
statusLabel.Text = $"Failed";
}
else if (e.Result == WebNavigationResult.Success)
{
statusLabel.Text = $"Success";
}
};

var grid = new Grid
{
RowDefinitions =
{
new RowDefinition(),
new RowDefinition { Height = 100 }
}
};

grid.Add(webView, 0, 0);
grid.Add(statusLabel, 0, 1);

Content = grid;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Nested Subdirectory Test File</title>
</head>
<body>
<h1>Nested Subdirectory Test File</h1>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[moderate] Regression test coverage — the regression page contains only inline HTML, so it does not catch the current LoadFileUrl(nsUrl, nsUrl) read-access scope problem. Add a sibling resource (CSS/image/script) referenced relatively from this nested page and assert it renders, so the test covers local-file loading beyond the top-level HTML.

<p>This is test.html from the foo/bar/baz nested subdirectories.</p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue23315(TestDevice device) : _IssuesUITest(device)
{
public override string Issue => "LoadFile in src/Core/src/Platform/iOS/MauiWKWebView.cs ignore directories";

[Test]
[Category(UITestCategories.WebView)]
public void WebViewCanLoadFileFromSubdirectory()
{
// The HostApp loads `foo/bar/baz/test.html` whose <title> is "Nested Subdirectory Test File".
// When the bug is present on iOS/MacCatalyst, LoadFile strips the directory part
// and tries to load only `test.html`, so the navigation fails and the label
// never reports the expected title.
var statusLabel = App.WaitForElement("StatusLabel", timeout: TimeSpan.FromSeconds(10));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[moderate] Regression Prevention - Waiting only for the StatusLabel element races the WebView navigation: the label exists as soon as the page is displayed, before Navigated updates it to Success. On slower iOS/MacCatalyst runs this can read the initial empty text and fail despite a correct fix. Wait for the label text to become Success before asserting.

var text = statusLabel.GetText();
Assert.That(text, Is.EqualTo("Success"), $"Expected to load the file from the subdirectory, but got '{text}' instead.");
}
}
}
11 changes: 11 additions & 0 deletions src/Core/src/Handlers/HybridWebView/HybridWebView.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,18 @@
}
else {
// Android WebView
// Native -> JS messages are delivered via WebView.postWebMessage. The resulting
// MessageEvent has source === null (no sending window) -- that's the invariant we
// can rely on across WebView versions. Drop any 'message' event whose source is
// a Window (e.g. a nested iframe calling window.parent.postMessage), which would
// otherwise be mistaken for a native message. NOTE: we intentionally do not check
// arg.origin here because Android WebView delivers postWebMessage events with an
// empty-string origin, not window.location.origin.
window.addEventListener('message', (arg) => {
if (arg.source !== null) {
console.warn(`HybridWebView: ignored 'message' event from unexpected sender (origin: '${arg.origin}').`);
return;
}
dispatchHybridWebViewMessage(arg.data);
});
}
Expand Down
13 changes: 12 additions & 1 deletion src/Core/src/Handlers/HybridWebView/HybridWebView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,18 @@ interface DotNetInvokeResult {
};
} else {
// Android WebView
window.addEventListener('message', (arg: any) => {
// Native -> JS messages are delivered via WebView.postWebMessage. The resulting
// MessageEvent has source === null (no sending window) -- that's the invariant we
// can rely on across WebView versions. Drop any 'message' event whose source is
// a Window (e.g. a nested iframe calling window.parent.postMessage), which would
// otherwise be mistaken for a native message. NOTE: we intentionally do not check
// arg.origin here because Android WebView delivers postWebMessage events with an
// empty-string origin, not window.location.origin.
window.addEventListener('message', (arg: MessageEvent) => {
if (arg.source !== null) {
console.warn(`HybridWebView: ignored 'message' event from unexpected sender (origin: '${arg.origin}').`);
return;
}
dispatchHybridWebViewMessage(arg.data);
});
}
Expand Down
22 changes: 3 additions & 19 deletions src/Core/src/Handlers/WebView/WebViewHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -519,28 +519,12 @@ static string GetCookieString(List<Cookie> existingCookies)

bool LoadFile(string url)
{
try
if (PlatformView is null)
{
var file = Path.GetFileNameWithoutExtension(url);
var ext = Path.GetExtension(url);

var nsUrl = NSBundle.MainBundle.GetUrlForResource(file, ext);

if (nsUrl == null)
{
return false;
}

PlatformView?.LoadFileUrl(nsUrl, nsUrl);

return true;
}
catch (Exception)
{
MauiContext?.CreateLogger<WebViewHandler>()?.LogWarning("Could not load {url} as local file", url);
return false;
}

return false;
return PlatformView.LoadFile(url, MauiContext?.CreateLogger<WebViewHandler>());
}

public static void MapEvaluateJavaScriptAsync(IWebViewHandler handler, IWebView webView, object? arg)
Expand Down
26 changes: 4 additions & 22 deletions src/Core/src/Platform/iOS/MauiWKWebView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,29 +182,11 @@ public static WKWebViewConfiguration CreateConfiguration()

bool LoadFile(string url)
{
try
{
var file = Path.GetFileNameWithoutExtension(url);
var ext = Path.GetExtension(url);

var nsUrl = NSBundle.MainBundle.GetUrlForResource(file, ext);

if (nsUrl == null)
{
return false;
}

LoadFileUrl(nsUrl, nsUrl);

return true;
}
catch (Exception ex)
{
if (_handler.TryGetTarget(out var handler))
handler.MauiContext?.CreateLogger<MauiWKWebView>()?.LogWarning($"Could not load {url} as local file: {ex}");
}
ILogger? logger = null;
if (_handler.TryGetTarget(out var handler))
logger = handler.MauiContext?.CreateLogger<MauiWKWebView>();

return false;
return this.LoadFile(url, logger);
}

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
Expand Down
39 changes: 39 additions & 0 deletions src/Core/src/Platform/iOS/WebViewExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Foundation;
using Microsoft.Extensions.Logging;
using WebKit;

namespace Microsoft.Maui.Platform
Expand Down Expand Up @@ -141,5 +143,42 @@ internal static string HandleWKWebViewResult(NSObject? result)

return result.ToString() ?? "null";
}

/// <summary>
/// Loads a local file URL into the WebView using NSBundle resource loading.
/// </summary>
/// <param name="webView">The WKWebView instance to load the file into</param>
/// <param name="url">The local file URL to load</param>
/// <param name="logger">Optional logger for error reporting</param>
/// <returns>True if the file was successfully loaded, false otherwise</returns>
internal static bool LoadFile(this WKWebView webView, string url, ILogger? logger = null)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Backward Compatibility - This shared helper only fixes the handler path; the registered iOS compatibility WebView renderer still has the duplicated old LoadFile implementation that strips directories before calling NSBundle.MainBundle.GetUrlForResource(file, ext). Concrete scenario: an app using UseMauiCompatibility() or a compatibility WebView renderer with Source = foo/bar/baz/test.html on iOS/MacCatalyst still fails issue #23315. Please either reuse/apply the directory-preserving logic in src/Compatibility/Core/src/iOS/Renderers/WkWebViewRenderer.cs or explicitly scope this PR/test to handler-only behavior.

{
try
{
var file = Path.GetFileNameWithoutExtension(url);
var ext = Path.GetExtension(url);
var directory = Path.GetDirectoryName(url);

// If there's a subdirectory, use the overload that accepts a subdirectory parameter else fallback to the original method if subdirectory method fails or if no subdirectory
NSUrl? nsUrl = string.IsNullOrEmpty(directory)
? NSBundle.MainBundle.GetUrlForResource(file, ext)
: NSBundle.MainBundle.GetUrlForResource(file, ext, directory);

if (nsUrl is null)
{
return false;
}

webView.LoadFileUrl(nsUrl, nsUrl);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] iOS/MacCatalyst WebView - LoadFileUrl(nsUrl, nsUrl) grants WKWebView read access only to the HTML file URL. Nested pages that load relative sibling resources (CSS/JS/images) from the same bundle directory can still render broken even though the top-level HTML loads. Use the containing directory or intended bundle root as allowingReadAccessToURL, and add coverage with a relative subresource under foo/bar/baz/.


return true;
}
catch (Exception ex)
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Compatibility renderer coverage — this helper fixes only the handler path. src/Compatibility/Core/src/iOS/Renderers/WkWebViewRenderer.cs still uses GetUrlForResource(file, ext) and will continue to ignore subdirectories for apps using compatibility renderers; route that renderer through the same directory-aware helper.

logger?.LogWarning($"Could not load {url} as local file: {ex}");
}

return false;
}
}
}
Loading