CVE-2025-46282
Overview
Background
- Web Archive (.webarchive)
- A saved snapshot of a page (content under some origin) that can be opened later; an attacker can craft one presenting a chosen origin.
- Permission caching
- WebKit remembers granted/denied capture permissions per origin to avoid re-prompting; reusing that cache for archive content is unsafe.
- didLoadWebArchive
- A page flag indicating the current content came from a web archive, used to force prompting.
Root Cause Analysis
This fixes a permission bypass where content loaded from a saved Web Archive could silently reuse cached camera/microphone/geolocation grants. WebKit caches media-capture permission decisions per origin so a site does not re-prompt: grantRequest appends to m_grantedRequests, denyRequest appends to m_deniedRequests, and hasGrantedRequest consults m_grantedRequests to auto-approve a matching origin without a prompt. A .webarchive file bundles content under an arbitrary origin; if a user opens a malicious web archive whose origin matches a site the user previously granted capture to (or if the archive can otherwise ride the cache), the archive’s content could obtain camera/microphone/geolocation access WITHOUT a fresh user prompt — accessing sensitive user data.
The fix makes web-archive-loaded pages never populate or reuse the permission cache: grantRequest and denyRequest set shouldCacheResult = !page->didLoadWebArchive() and skip caching when a web archive was loaded; hasGrantedRequest returns false immediately when page->didLoadWebArchive(); and a new WebPageProxy::shouldAlwaysPromptForPermission returns true for Camera/Microphone/Geolocation when the page loaded a web archive.
The restored invariant is that a page loaded from a web archive always prompts for capture/geolocation permissions and cannot inherit or seed cached grants. The impact — reachable by getting a user to open a crafted .webarchive — is unauthorized access to sensitive sensors, not memory corruption.
Attack Path
- Craft a malicious web archive Build a .webarchive presenting content under an origin the user has previously granted (or otherwise able to hit the permission cache).
- Get the user to open it The user opens the web archive, loading its content into WebKit as a web-archive page.
- Reuse the cached grant Pre-patch, hasGrantedRequest matches the cached grant for the origin and auto-approves capture without prompting.
- Access sensitive data The archive content activates camera/microphone/geolocation and exfiltrates sensitive user data without consent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forSource/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp |
modified | |
switchSource/WebKit/UIProcess/WebPageProxy.cpp |
modified | |
ifTools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm |
modified |
Files Changed
Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cppSource/WebKit/UIProcess/WebPageProxy.cppSource/WebKit/UIProcess/WebPageProxy.hTools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxprojTools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mmTools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mmTools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mmTools/TestWebKitAPI/Tests/WebKitCocoa/example.webarchive
Audit Directions
- Other cached permissions vs web archiveAudit permission/state caches (notifications, storage access, geolocation tokens) for reuse on didLoadWebArchive() pages without an always-prompt guard.
- Origin trust for archivesGrep UIProcess for permission/grant lookups keyed only on security origin that do not distinguish web-archive-loaded content.
Patch
diff --git a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
index 286af00b7156..27279c6e0c33 100644
--- a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
+++ b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
@@ -267,7 +267,12 @@ void UserMediaPermissionRequestManagerProxy::denyRequest(UserMediaPermissionRequ
ALWAYS_LOG(LOGIDENTIFIER, request.userMediaID() ? request.userMediaID()->toUInt64() : 0, ", reason: ", reason);
- if (reason == UserMediaPermissionRequestProxy::UserMediaAccessDenialReason::PermissionDenied)
+ bool shouldCacheResult = true;
+#if ENABLE(WEB_ARCHIVE)
+ shouldCacheResult = !page->didLoadWebArchive();
+#endif
+
+ if (reason == UserMediaPermissionRequestProxy::UserMediaAccessDenialReason::PermissionDenied && shouldCacheResult)
m_deniedRequests.append(DeniedRequest { request.mainFrameID(), request.userMediaDocumentSecurityOrigin(), request.topLevelDocumentSecurityOrigin(), request.requiresAudioCapture(), request.requiresVideoCapture(), request.requiresDisplayCapture() });
if (auto callback = request.decisionCompletionHandler()) {
@@ -299,7 +304,12 @@ void UserMediaPermissionRequestManagerProxy::grantRequest(UserMediaPermissionReq
page->willStartCapture(request, [callback = WTFMove(callback)]() mutable {
callback(true);
});
- m_grantedRequests.append(request);
+ bool shouldCacheResult = true;
+#if ENABLE(WEB_ARCHIVE)
+ shouldCacheResult = !page->didLoadWebArchive();
+#endif
+ if (shouldCacheResult)
+ m_grantedRequests.append(request);
if (request.requiresAudioCapture())
m_grantedAudioFrames.add(request.frameID());
if (request.requiresVideoCapture())
@@ -442,6 +452,11 @@ bool UserMediaPermissionRequestManagerProxy::hasGrantedRequest(std::optional<Fra
if (!isUserGesturePriviledged && inactiveMediaCaptureStreamDuration().minutes() > page->protectedPreferences()->inactiveMediaCaptureStreamRepromptWithoutUserGestureIntervalInMinutes())
return false;
+#if ENABLE(WEB_ARCHIVE)
+ if (page->didLoadWebArchive())
+ return false;
+#endif
+
bool checkForAudio = needsAudio;
bool checkForVideo = needsVideo;
for (Ref grantedRequest : m_grantedRequests) {
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index f07d5b338236..7ec2f1fa8245 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -12814,6 +12814,48 @@ void WebPageProxy::revokeGeolocationAuthorizationToken(const String& authorizati
internals().protectedGeolocationPermissionRequestManager()->revokeAuthorizationToken(authorizationToken);
}
+#if ENABLE(WEB_ARCHIVE)
+bool WebPageProxy::shouldAlwaysPromptForPermission(PermissionName permissionName) const
+{
+ if (!didLoadWebArchive())
+ return false;
+
+ switch (permissionName) {
+ // All cached permissions that are supported by platforms that support
+ // WEB_ARCHIVE should always prompt.
+ case PermissionName::Camera:
+ case PermissionName::Geolocation:
+ case PermissionName::Microphone:
+ break;
+
+ // Notifications are not available in ephemeral sessions.
+ case PermissionName::Notifications:
+ case PermissionName::Push:
+
+ // Orientation and motion data are not emitted.
+ case PermissionName::Accelerometer:
+ case PermissionName::Gyroscope:
+ case PermissionName::Magnetometer:
+
+ // These are not persistent permissions.
+ case PermissionName::BackgroundFetch:
+ case PermissionName::DisplayCapture:
+ case PermissionName::ScreenWakeLock:
+ case PermissionName::SpeakerSelection:
+ case PermissionName::StorageAccess:
+
+ // Not supported.
+ case PermissionName::Bluetooth:
+ case PermissionName::Midi:
+ case PermissionName::Nfc:
+ ASSERT_NOT_REACHED();
+ break;
+ }
+
+ return true;
+}
+#endif
+
void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const PermissionDescriptor& descriptor, CompletionHandler<void(std::optional<PermissionState>)>&& completionHandler)
{
bool canAPISucceed = true;
@@ -12822,8 +12864,8 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi
String name;
#if ENABLE(WEB_ARCHIVE)
- if (didLoadWebArchive()) {
- completionHandler(PermissionState::Denied);
+ if (shouldAlwaysPromptForPermission(descriptor.name)) {
+ completionHandler(PermissionState::Prompt);
return;
}
#endif
diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h
index 490eab241948..20770b71e251 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.h
+++ b/Source/WebKit/UIProcess/WebPageProxy.h
@@ -3107,6 +3107,10 @@ private:
void requestNotificationPermission(const String& originString, CompletionHandler<void(bool allowed)>&&);
+#if ENABLE(WEB_ARCHIVE)
+ bool shouldAlwaysPromptForPermission(WebCore::PermissionName) const;
+#endif
+
void didChangeContentSize(const WebCore::IntSize&);
void didChangeIntrinsicContentSize(const WebCore::IntSize&);
diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
index 51d51c95fb59..cb89c353447d 100644
--- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
+++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
@@ -763,6 +763,7 @@
A17991881E1C994E00A505ED /* SharedBuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = A17991861E1C994E00A505ED /* SharedBuffer.mm */; };
A179918B1E1CA24100A505ED /* SharedBufferTest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A17991891E1CA24100A505ED /* SharedBufferTest.cpp */; };
A17C46592C98E4A80023F3C7 /* download.example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414A /* download.example.webarchive */; };
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414B /* example.webarchive */; };
A17C465A2C98E4BB0023F3C7 /* active-touch-events.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4A7CE792662D83E00228685 /* active-touch-events.html */; };
A17C465B2C98E4BB0023F3C7 /* animated-red-green-blue-repeat-infinite.gif in Copy Resources */ = {isa = PBXBuildFile; fileRef = 48354DD129F08036003E566D /* animated-red-green-blue-repeat-infinite.gif */; };
A17C465C2C98E4BB0023F3C7 /* composited.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 0F16BED72304A1D100B4A167 /* composited.html */; };
@@ -2002,6 +2003,7 @@
A17C475F2C98E5C20023F3C7 /* enormous.svg in Copy Resources */,
A17C46AF2C98E54B0023F3C7 /* enumerateMediaDevices.html in Copy Resources */,
A17C47602C98E5C20023F3C7 /* event.ics in Copy Resources */,
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */,
A17C46B02C98E54B0023F3C7 /* execCopy.html in Copy Resources */,
A17C47612C98E5C20023F3C7 /* exif-orientation-8-llo.jpg in Copy Resources */,
A17C47622C98E5C20023F3C7 /* ExitFullscreenOnEnterPiP.html in Copy Resources */,
@@ -4104,6 +4106,7 @@
CEDA12402437C9EA00C28A9E /* editable-region-composited-and-non-composited-overlap.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "editable-region-composited-and-non-composited-overlap.html"; path = "ios/editable-region-composited-and-non-composited-overlap.html"; sourceTree = SOURCE_ROOT; };
D04CF93E285C77C9005D6337 /* MachSendRight.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MachSendRight.cpp; sourceTree = "<group>"; };
D20C03AE2B704A26004C414A /* download.example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = download.example.webarchive; path = Tests/WebKitCocoa/download.example.webarchive; sourceTree = "<group>"; };
+ D20C03AE2B704A26004C414B /* example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = example.webarchive; path = Tests/WebKitCocoa/example.webarchive; sourceTree = "<group>"; };
D2CC7CF22D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = TestCocoaImageAndCocoaColor.h; path = cocoa/TestCocoaImageAndCocoaColor.h; sourceTree = "<group>"; };
D2CC7CF32D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; name = TestCocoaImageAndCocoaColor.mm; path = cocoa/TestCocoaImageAndCocoaColor.mm; sourceTree = "<group>"; };
D2D9A6BD2D96B539004A2C92 /* icon-with-subresource.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "icon-with-subresource.svg"; sourceTree = "<group>"; };
@@ -4594,6 +4597,7 @@
isa = PBXGroup;
children = (
D20C03AE2B704A26004C414A /* download.example.webarchive */,
+ D20C03AE2B704A26004C414B /* example.webarchive */,
3AA7856029B6C8AA00C4374A /* TestWebKitAPIPrefix.h */,
DDF3A82928930475005920CF /* gtest.xcodeproj */,
49AEEF682407276F00C87E4C /* Info.plist */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
index 8f258d6448ea..24fe09c7016e 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
@@ -2138,6 +2138,87 @@ TEST(WebKit, GetUserMediaWithWebThread)
}
#endif // PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST)
+#if ENABLE(WEB_ARCHIVE)
+TEST(GetUserMedia, OriginAndWebArchivePermission)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/"_s, { "body"_s } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ initializeMediaCaptureConfiguration(configuration.get());
+
+ auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setProxyConfiguration:@{
+ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
+ (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port())
+ }];
+ auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+ [configuration setWebsiteDataStore:dataStore.get()];
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
+ auto delegate = adoptNS([[UserMediaCaptureUIDelegate alloc] init]);
+
+ RetainPtr navDelegate = adoptNS([TestNavigationDelegate new]);
+
+ auto observer = adoptNS([[MediaCaptureObserver alloc] init]);
+ [webView addObserver:observer.get() forKeyPath:@"microphoneCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+ [webView addObserver:observer.get() forKeyPath:@"cameraCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+
+ [navDelegate allowAnyTLSCertificate];
+ [webView setUIDelegate:delegate.get()];
+ [webView setNavigationDelegate:navDelegate.get()];
+
+ [delegate setAudioDecision:WKPermissionDecisionGrant];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/"]]];
+ [navDelegate waitForDidFinishNavigation];
+
+ cameraCaptureStateChange = false;
+ EXPECT_EQ([webView cameraCaptureState], WKMediaCaptureStateNone);
+ [webView evaluateJavaScript:@"navigator.mediaDevices.enumerateDevices().then(() => { return navigator.mediaDevices.getUserMedia({ audio: false, video: true }) });" completionHandler:nil];
+ EXPECT_TRUE(waitUntilCameraState(webView.get(), WKMediaCaptureStateActive));
+ EXPECT_EQ([webView cameraCaptureState], WKMediaCaptureStateActive);
+
+ [delegate waitUntilPrompted];
+ [delegate resetWasPrompted];
+
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+ [navDelegate waitForDidFinishNavigation];
+ __block bool doneEvaluatingJavaScript { false };
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"camera\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ doneEvaluatingJavaScript = false;
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"microphone\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ doneEvaluatingJavaScript = false;
+ [webView callAsyncJavaScript:@"try { await navigator.mediaDevices.getUserMedia({ audio: false, video: true }); return null; } catch (err) { return err.name; }" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"TypeError", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ EXPECT_EQ([delegate numberOfPrompts], 1);
+ [webView removeObserver:observer.get() forKeyPath:@"microphoneCaptureState"];
+ [webView removeObserver:observer.get() forKeyPath:@"cameraCaptureState"];
+}
+#endif
+
} // namespace TestWebKitAPI
#endif // ENABLE(MEDIA_STREAM)
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
index 2b1e3b3cb949..9ca993324891 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
@@ -28,6 +28,7 @@
#if ENABLE(NOTIFICATIONS) && !(PLATFORM(IOS) || PLATFORM(VISION))
#import "DeprecatedGlobalValues.h"
#import "PlatformUtilities.h"
+#import "TestNavigationDelegate.h"
#import "TestWKWebView.h"
#import <WebKit/WKUIDelegatePrivate.h>
#import <wtf/Function.h>
@@ -169,6 +170,40 @@ TEST(Notification, ParallelPermissionRequestsGranted)
runParallelPermissionRequestsTest(ShouldGrantPermission::Yes);
}
+#if ENABLE(WEB_ARCHIVE)
+TEST(Notification, WebArchiveNotificationNotSupported)
+{
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ auto messageHandler = adoptNS([[NotificationPermissionMessageHandler alloc] init]);
+ [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"testHandler"];
+
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+ RetainPtr uiDelegate = adoptNS([[NotificationPermissionUIDelegate alloc] initWithHandler:[] {
+ return true;
+ }]);
+ [webView setUIDelegate:uiDelegate.get()];
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [webView setNavigationDelegate:navigationDelegate.get()];
+ [navigationDelegate allowAnyTLSCertificate];
+
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+
+ [navigationDelegate waitForDidFinishNavigation];
+
+ didReceiveMessage = false;
+ clientPermissionRequestCount = 0;
+ [receivedMessages removeAllObjects];
+
+ [webView evaluateJavaScript:@"Notification.requestPermission((permission) => { webkit.messageHandlers.testHandler.postMessage(permission) });" completionHandler:nil];
+ TestWebKitAPI::Util::run(&didReceiveMessage);
+
+ EXPECT_EQ(clientPermissionRequestCount, 0U);
+ EXPECT_WK_STREQ(@"denied", receivedMessages.get()[0]);
+}
+#endif
+
} // namespace TestWebKitAPI
#endif // ENABLE(NOTIFICATIONS) && !(PLATFORM(IOS) || PLATFORM(VISION))
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
index c64d263224b9..384ca0dee99b 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
@@ -63,8 +63,10 @@
@end
#endif
+#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/_WKFeature.h>
#import <WebKit/_WKHitTestResult.h>
+#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
#import <wtf/BlockPtr.h>
#import <wtf/RetainPtr.h>
#import <wtf/Vector.h>
@@ -343,6 +345,115 @@ TEST(WebKit, GeolocationPermissionInIFrame)
EXPECT_TRUE(done);
}
+TEST(WebKit, GeolocationPermissionInIFrameExampleWebArchive)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/"_s, { mainFrameText } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto pool = adoptNS([[WKProcessPool alloc] init]);
+
+ WKGeolocationProviderV1 providerCallback;
+ zeroBytes(providerCallback);
+ providerCallback.base.version = 1;
+ providerCallback.startUpdating = [] (WKGeolocationManagerRef manager, const void*) {
+ WKGeolocationManagerProviderDidChangePosition(manager, adoptWK(WKGeolocationPositionCreate(0, 50.644358, 3.345453, 2.53)).get());
+ };
+ WKGeolocationManagerSetProvider(WKContextGetGeolocationManager((WKContextRef)pool.get()), &providerCallback.base);
+
+ auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setProxyConfiguration:@{
+ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
+ (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port())
+ }];
+ auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ configuration.get().processPool = pool.get();
+ [configuration setWebsiteDataStore:dataStore.get()];
+
+ auto messageHandler = adoptNS([[GeolocationPermissionMessageHandler alloc] init]);
+ [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"testHandler"];
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]);
+
+ auto permissionDelegate = adoptNS([[GeolocationDelegateNew alloc] init]);
+ [webView setUIDelegate:permissionDelegate.get()];
+
+ auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+
+ webView.get().navigationDelegate = navigationDelegate.get();
+
+ [permissionDelegate setValidationHandler:[&webView](WKSecurityOrigin *origin, WKFrameInfo *frame) {
+ if ([origin.protocol isEqualToString:@"https"]) {
+ EXPECT_WK_STREQ(origin.protocol, @"https");
+ EXPECT_WK_STREQ(origin.host, @"example.com");
+ } else {
+ EXPECT_WK_STREQ(origin.protocol, @"file");
+ EXPECT_WK_STREQ(origin.host, @"");
+ }
+ EXPECT_EQ(origin.port, 0);
+
+ EXPECT_WK_STREQ(frame.securityOrigin.protocol, @"https");
+ EXPECT_WK_STREQ(frame.securityOrigin.host, @"example.com");
+ EXPECT_EQ(frame.securityOrigin.port, 0);
+ EXPECT_TRUE(frame.isMainFrame);
+ EXPECT_TRUE(frame.webView == webView);
+ }];
+
+ NSString *getCurrentPosition = @"navigator.geolocation.getCurrentPosition(() => { webkit.messageHandlers.testHandler.postMessage(\"ok\") }, () => { webkit.messageHandlers.testHandler.postMessage(\"ko\") });";
+
+ done = false;
+ didReceiveMessage = false;
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+ [webView evaluateJavaScript:getCurrentPosition completionHandler:nil];
+ TestWebKitAPI::Util::run(&didReceiveMessage);
+ TestWebKitAPI::Util::run(&done);
+
+#if ENABLE(WEB_ARCHIVE)
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+ [navigationDelegate waitForDidFinishNavigation];
+ __block bool doneEvaluatingJavaScript { false };
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"geolocation\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
... (file diff truncated)
Regression Test / PoC
diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
index 51d51c95fb59..cb89c353447d 100644
--- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
+++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
@@ -763,6 +763,7 @@
A17991881E1C994E00A505ED /* SharedBuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = A17991861E1C994E00A505ED /* SharedBuffer.mm */; };
A179918B1E1CA24100A505ED /* SharedBufferTest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A17991891E1CA24100A505ED /* SharedBufferTest.cpp */; };
A17C46592C98E4A80023F3C7 /* download.example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414A /* download.example.webarchive */; };
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414B /* example.webarchive */; };
A17C465A2C98E4BB0023F3C7 /* active-touch-events.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4A7CE792662D83E00228685 /* active-touch-events.html */; };
A17C465B2C98E4BB0023F3C7 /* animated-red-green-blue-repeat-infinite.gif in Copy Resources */ = {isa = PBXBuildFile; fileRef = 48354DD129F08036003E566D /* animated-red-green-blue-repeat-infinite.gif */; };
A17C465C2C98E4BB0023F3C7 /* composited.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 0F16BED72304A1D100B4A167 /* composited.html */; };
@@ -2002,6 +2003,7 @@
A17C475F2C98E5C20023F3C7 /* enormous.svg in Copy Resources */,
A17C46AF2C98E54B0023F3C7 /* enumerateMediaDevices.html in Copy Resources */,
A17C47602C98E5C20023F3C7 /* event.ics in Copy Resources */,
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */,
A17C46B02C98E54B0023F3C7 /* execCopy.html in Copy Resources */,
A17C47612C98E5C20023F3C7 /* exif-orientation-8-llo.jpg in Copy Resources */,
A17C47622C98E5C20023F3C7 /* ExitFullscreenOnEnterPiP.html in Copy Resources */,
@@ -4104,6 +4106,7 @@
CEDA12402437C9EA00C28A9E /* editable-region-composited-and-non-composited-overlap.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "editable-region-composited-and-non-composited-overlap.html"; path = "ios/editable-region-composited-and-non-composited-overlap.html"; sourceTree = SOURCE_ROOT; };
D04CF93E285C77C9005D6337 /* MachSendRight.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MachSendRight.cpp; sourceTree = "<group>"; };
D20C03AE2B704A26004C414A /* download.example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = download.example.webarchive; path = Tests/WebKitCocoa/download.example.webarchive; sourceTree = "<group>"; };
+ D20C03AE2B704A26004C414B /* example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = example.webarchive; path = Tests/WebKitCocoa/example.webarchive; sourceTree = "<group>"; };
D2CC7CF22D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = TestCocoaImageAndCocoaColor.h; path = cocoa/TestCocoaImageAndCocoaColor.h; sourceTree = "<group>"; };
D2CC7CF32D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; name = TestCocoaImageAndCocoaColor.mm; path = cocoa/TestCocoaImageAndCocoaColor.mm; sourceTree = "<group>"; };
D2D9A6BD2D96B539004A2C92 /* icon-with-subresource.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "icon-with-subresource.svg"; sourceTree = "<group>"; };
@@ -4594,6 +4597,7 @@
isa = PBXGroup;
children = (
D20C03AE2B704A26004C414A /* download.example.webarchive */,
+ D20C03AE2B704A26004C414B /* example.webarchive */,
3AA7856029B6C8AA00C4374A /* TestWebKitAPIPrefix.h */,
DDF3A82928930475005920CF /* gtest.xcodeproj */,
49AEEF682407276F00C87E4C /* Info.plist */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
index 8f258d6448ea..24fe09c7016e 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
@@ -2138,6 +2138,87 @@ TEST(WebKit, GetUserMediaWithWebThread)
}
#endif // PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST)
+#if ENABLE(WEB_ARCHIVE)
+TEST(GetUserMedia, OriginAndWebArchivePermission)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/"_s, { "body"_s } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ initializeMediaCaptureConfiguration(configuration.get());
+
+ auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setProxyConfiguration:@{
+ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
+ (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port())
+ }];
+ auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+ [configuration setWebsiteDataStore:dataStore.get()];
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
+ auto delegate = adoptNS([[UserMediaCaptureUIDelegate alloc] init]);
+
+ RetainPtr navDelegate = adoptNS([TestNavigationDelegate new]);
+
+ auto observer = adoptNS([[MediaCaptureObserver alloc] init]);
+ [webView addObserver:observer.get() forKeyPath:@"microphoneCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+ [webView addObserver:observer.get() forKeyPath:@"cameraCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+
+ [navDelegate allowAnyTLSCertificate];
+ [webView setUIDelegate:delegate.get()];
+ [webView setNavigationDelegate:navDelegate.get()];
+
+ [delegate setAudioDecision:WKPermissionDecisionGrant];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/"]]];
+ [navDelegate waitForDidFinishNavigation];
+
+ cameraCaptureStateChange = false;
+ EXPECT_EQ([webView cameraCaptureState], WKMediaCaptureStateNone);
+ [webView evaluateJavaScript:@"navigator.mediaDevices.enumerateDevices().then(() => { return navigator.mediaDevices.getUserMedia({ audio: false, video: true }) });" completionHandler:nil];
+ EXPECT_TRUE(waitUntilCameraState(webView.get(), WKMediaCaptureStateActive));
+ EXPECT_EQ([webView cameraCaptureState], WKMediaCaptureStateActive);
+
+ [delegate waitUntilPrompted];
+ [delegate resetWasPrompted];
+
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+ [navDelegate waitForDidFinishNavigation];
+ __block bool doneEvaluatingJavaScript { false };
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"camera\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ doneEvaluatingJavaScript = false;
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"microphone\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ doneEvaluatingJavaScript = false;
+ [webView callAsyncJavaScript:@"try { await navigator.mediaDevices.getUserMedia({ audio: false, video: true }); return null; } catch (err) { return err.name; }" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"TypeError", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ EXPECT_EQ([delegate numberOfPrompts], 1);
+ [webView removeObserver:observer.get() forKeyPath:@"microphoneCaptureState"];
+ [webView removeObserver:observer.get() forKeyPath:@"cameraCaptureState"];
+}
+#endif
+
} // namespace TestWebKitAPI
#endif // ENABLE(MEDIA_STREAM)
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
index 2b1e3b3cb949..9ca993324891 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
@@ -28,6 +28,7 @@
#if ENABLE(NOTIFICATIONS) && !(PLATFORM(IOS) || PLATFORM(VISION))
#import "DeprecatedGlobalValues.h"
#import "PlatformUtilities.h"
+#import "TestNavigationDelegate.h"
#import "TestWKWebView.h"
#import <WebKit/WKUIDelegatePrivate.h>
#import <wtf/Function.h>
@@ -169,6 +170,40 @@ TEST(Notification, ParallelPermissionRequestsGranted)
runParallelPermissionRequestsTest(ShouldGrantPermission::Yes);
}
+#if ENABLE(WEB_ARCHIVE)
+TEST(Notification, WebArchiveNotificationNotSupported)
+{
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ auto messageHandler = adoptNS([[NotificationPermissionMessageHandler alloc] init]);
+ [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"testHandler"];
+
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+ RetainPtr uiDelegate = adoptNS([[NotificationPermissionUIDelegate alloc] initWithHandler:[] {
+ return true;
+ }]);
+ [webView setUIDelegate:uiDelegate.get()];
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [webView setNavigationDelegate:navigationDelegate.get()];
+ [navigationDelegate allowAnyTLSCertificate];
+
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+
+ [navigationDelegate waitForDidFinishNavigation];
+
+ didReceiveMessage = false;
+ clientPermissionRequestCount = 0;
+ [receivedMessages removeAllObjects];
+
+ [webView evaluateJavaScript:@"Notification.requestPermission((permission) => { webkit.messageHandlers.testHandler.postMessage(permission) });" completionHandler:nil];
+ TestWebKitAPI::Util::run(&didReceiveMessage);
+
+ EXPECT_EQ(clientPermissionRequestCount, 0U);
+ EXPECT_WK_STREQ(@"denied", receivedMessages.get()[0]);
+}
+#endif
+
} // namespace TestWebKitAPI
#endif // ENABLE(NOTIFICATIONS) && !(PLATFORM(IOS) || PLATFORM(VISION))
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
index c64d263224b9..384ca0dee99b 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
@@ -63,8 +63,10 @@
@end
#endif
+#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/_WKFeature.h>
#import <WebKit/_WKHitTestResult.h>
+#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
#import <wtf/BlockPtr.h>
#import <wtf/RetainPtr.h>
#import <wtf/Vector.h>
@@ -343,6 +345,115 @@ TEST(WebKit, GeolocationPermissionInIFrame)
EXPECT_TRUE(done);
}
+TEST(WebKit, GeolocationPermissionInIFrameExampleWebArchive)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/"_s, { mainFrameText } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto pool = adoptNS([[WKProcessPool alloc] init]);
+
+ WKGeolocationProviderV1 providerCallback;
+ zeroBytes(providerCallback);
+ providerCallback.base.version = 1;
+ providerCallback.startUpdating = [] (WKGeolocationManagerRef manager, const void*) {
+ WKGeolocationManagerProviderDidChangePosition(manager, adoptWK(WKGeolocationPositionCreate(0, 50.644358, 3.345453, 2.53)).get());
+ };
+ WKGeolocationManagerSetProvider(WKContextGetGeolocationManager((WKContextRef)pool.get()), &providerCallback.base);
+
+ auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setProxyConfiguration:@{
+ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
+ (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port())
+ }];
+ auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ configuration.get().processPool = pool.get();
+ [configuration setWebsiteDataStore:dataStore.get()];
+
+ auto messageHandler = adoptNS([[GeolocationPermissionMessageHandler alloc] init]);
+ [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"testHandler"];
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]);
+
+ auto permissionDelegate = adoptNS([[GeolocationDelegateNew alloc] init]);
+ [webView setUIDelegate:permissionDelegate.get()];
+
+ auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+
+ webView.get().navigationDelegate = navigationDelegate.get();
+
+ [permissionDelegate setValidationHandler:[&webView](WKSecurityOrigin *origin, WKFrameInfo *frame) {
+ if ([origin.protocol isEqualToString:@"https"]) {
+ EXPECT_WK_STREQ(origin.protocol, @"https");
+ EXPECT_WK_STREQ(origin.host, @"example.com");
+ } else {
+ EXPECT_WK_STREQ(origin.protocol, @"file");
+ EXPECT_WK_STREQ(origin.host, @"");
+ }
+ EXPECT_EQ(origin.port, 0);
+
+ EXPECT_WK_STREQ(frame.securityOrigin.protocol, @"https");
+ EXPECT_WK_STREQ(frame.securityOrigin.host, @"example.com");
+ EXPECT_EQ(frame.securityOrigin.port, 0);
+ EXPECT_TRUE(frame.isMainFrame);
+ EXPECT_TRUE(frame.webView == webView);
+ }];
+
+ NSString *getCurrentPosition = @"navigator.geolocation.getCurrentPosition(() => { webkit.messageHandlers.testHandler.postMessage(\"ok\") }, () => { webkit.messageHandlers.testHandler.postMessage(\"ko\") });";
+
+ done = false;
+ didReceiveMessage = false;
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+ [webView evaluateJavaScript:getCurrentPosition completionHandler:nil];
+ TestWebKitAPI::Util::run(&didReceiveMessage);
+ TestWebKitAPI::Util::run(&done);
+
+#if ENABLE(WEB_ARCHIVE)
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+ [navigationDelegate waitForDidFinishNavigation];
+ __block bool doneEvaluatingJavaScript { false };
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"geolocation\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ done = false;
+ didReceiveMessage = false;
+ [webView evaluateJavaScript:getCurrentPosition completionHandler:nil];
+ TestWebKitAPI::Util::run(&didReceiveMessage);
+ TestWebKitAPI::Util::run(&done);
+
+ // Reset web process state.
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ done = false;
+ didReceiveMessage = false;
+ doneEvaluatingJavaScript = false;
+ [webView callAsyncJavaScript:@"return (await new Promise(function (resolve, reject) {"
+ "navigator.geolocation.getCurrentPosition((position) => { resolve(JSON.stringify(position.toJSON())) }, (error) => { reject(error.message) });"
+ "}));" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
... (truncated)