diff --git a/.github/skills/find-reviewable-pr/scripts/query-reviewable-prs.ps1 b/.github/skills/find-reviewable-pr/scripts/query-reviewable-prs.ps1 index c9fa57bcb647..dfa6b3d4e4d9 100644 --- a/.github/skills/find-reviewable-pr/scripts/query-reviewable-prs.ps1 +++ b/.github/skills/find-reviewable-pr/scripts/query-reviewable-prs.ps1 @@ -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("") diff --git a/.github/workflows/pr-review-queue.yml b/.github/workflows/pr-review-queue.yml index f41baf1872a5..4f3f500fa614 100644 --- a/.github/workflows/pr-review-queue.yml +++ b/.github/workflows/pr-review-queue.yml @@ -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 @@ -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 diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml index ebce013b91ea..8ee93ed66321 100644 --- a/eng/pipelines/ci-copilot.yml +++ b/eng/pipelines/ci-copilot.yml @@ -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 @@ -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 diff --git a/eng/pipelines/common/variables.yml b/eng/pipelines/common/variables.yml index 8dee742a2525..d878b0ddb12c 100644 --- a/eng/pipelines/common/variables.yml +++ b/eng/pipelines/common/variables.yml @@ -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' diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue23315.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue23315.cs new file mode 100644 index 000000000000..94c4c905a37a --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue23315.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/Resources/Raw/foo/bar/baz/test.html b/src/Controls/tests/TestCases.HostApp/Resources/Raw/foo/bar/baz/test.html new file mode 100644 index 000000000000..a9a7f159cfbb --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Resources/Raw/foo/bar/baz/test.html @@ -0,0 +1,10 @@ + + + + Nested Subdirectory Test File + + +

Nested Subdirectory Test File

+

This is test.html from the foo/bar/baz nested subdirectories.

+ + diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23315.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23315.cs new file mode 100644 index 000000000000..cb4b373a13d4 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23315.cs @@ -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 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)); + var text = statusLabel.GetText(); + Assert.That(text, Is.EqualTo("Success"), $"Expected to load the file from the subdirectory, but got '{text}' instead."); + } + } +} diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.js b/src/Core/src/Handlers/HybridWebView/HybridWebView.js index e4b9d2330c93..a4acb769e63d 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebView.js +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.js @@ -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); }); } diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.ts b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts index 8c72bfee4b78..0ed4a49bd8a7 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebView.ts +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts @@ -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); }); } diff --git a/src/Core/src/Handlers/WebView/WebViewHandler.iOS.cs b/src/Core/src/Handlers/WebView/WebViewHandler.iOS.cs index ddc3c99a0d83..6145159d251a 100644 --- a/src/Core/src/Handlers/WebView/WebViewHandler.iOS.cs +++ b/src/Core/src/Handlers/WebView/WebViewHandler.iOS.cs @@ -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) diff --git a/src/Core/src/Platform/iOS/MauiWKWebView.cs b/src/Core/src/Platform/iOS/MauiWKWebView.cs index 149924163e6b..8dc542188823 100644 --- a/src/Core/src/Platform/iOS/MauiWKWebView.cs +++ b/src/Core/src/Platform/iOS/MauiWKWebView.cs @@ -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)] diff --git a/src/Core/src/Platform/iOS/WebViewExtensions.cs b/src/Core/src/Platform/iOS/WebViewExtensions.cs index 09b0e3c2bf2f..21a295fe0842 100644 --- a/src/Core/src/Platform/iOS/WebViewExtensions.cs +++ b/src/Core/src/Platform/iOS/WebViewExtensions.cs @@ -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 @@ -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) + { + 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); + + return true; + } + catch (Exception ex) + { + logger?.LogWarning($"Could not load {url} as local file: {ex}"); + } + + return false; + } } }