blob: 668cc2d57ed0b0cfd440367137636addb5aa8fc1 [file] [log] [blame] [edit]
/*
* Copyright (C) 2022 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "DragAndDropSimulator.h"
#import "FrameTreeChecks.h"
#import "HTTPServer.h"
#import "PlatformUtilities.h"
#import "TestNavigationDelegate.h"
#import "TestUIDelegate.h"
#import "TestURLSchemeHandler.h"
#import "TestWKWebView.h"
#import "Utilities.h"
#import "WKWebViewFindStringFindDelegate.h"
#import <WebKit/WKFrameInfoPrivate.h>
#import <WebKit/WKNavigationDelegatePrivate.h>
#import <WebKit/WKNavigationPrivate.h>
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKProcessPoolPrivate.h>
#import <WebKit/WKURLSchemeTaskPrivate.h>
#import <WebKit/WKWebViewPrivateForTesting.h>
#import <WebKit/WKWebpagePreferencesPrivate.h>
#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/_WKFeature.h>
#import <WebKit/_WKFrameTreeNode.h>
#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
#import <wtf/BlockPtr.h>
#import <wtf/text/MakeString.h>
namespace TestWebKitAPI {
static void enableSiteIsolation(WKWebViewConfiguration *configuration)
{
auto preferences = [configuration preferences];
for (_WKFeature *feature in [WKPreferences _features]) {
if ([feature.key isEqualToString:@"SiteIsolationEnabled"]) {
[preferences _setEnabled:YES forFeature:feature];
break;
}
}
}
static std::pair<RetainPtr<TestWKWebView>, RetainPtr<TestNavigationDelegate>> siteIsolatedViewAndDelegate(RetainPtr<WKWebViewConfiguration> configuration, CGRect rect = CGRectZero)
{
auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
[navigationDelegate allowAnyTLSCertificate];
enableSiteIsolation(configuration.get());
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:rect configuration:configuration.get()]);
webView.get().navigationDelegate = navigationDelegate.get();
return { WTFMove(webView), WTFMove(navigationDelegate) };
}
static std::pair<RetainPtr<TestWKWebView>, RetainPtr<TestNavigationDelegate>> siteIsolatedViewAndDelegate(const HTTPServer& server, CGRect rect = CGRectZero)
{
return siteIsolatedViewAndDelegate(server.httpsProxyConfiguration(), rect);
}
static bool processStillRunning(pid_t pid)
{
return !kill(pid, 0);
}
static bool frameTreesMatch(_WKFrameTreeNode *actualRoot, ExpectedFrameTree&& expectedRoot)
{
WKFrameInfo *info = actualRoot.info;
if (info._isLocalFrame != std::holds_alternative<String>(expectedRoot.remoteOrOrigin))
return false;
if (auto* expectedOrigin = std::get_if<String>(&expectedRoot.remoteOrOrigin)) {
WKSecurityOrigin *origin = info.securityOrigin;
auto actualOrigin = makeString(String(origin.protocol), "://"_s, String(origin.host), origin.port ? makeString(':', origin.port) : String());
if (actualOrigin != *expectedOrigin)
return false;
}
if (actualRoot.childFrames.count != expectedRoot.children.size())
return false;
for (_WKFrameTreeNode *actualChild in actualRoot.childFrames) {
auto index = expectedRoot.children.findIf([&] (auto& expectedFrameTree) {
return frameTreesMatch(actualChild, ExpectedFrameTree { expectedFrameTree });
});
if (index == WTF::notFound)
return false;
expectedRoot.children.remove(index);
}
return expectedRoot.children.isEmpty();
}
static bool frameTreesMatch(NSSet<_WKFrameTreeNode *> *actualFrameTrees, Vector<ExpectedFrameTree>&& expectedFrameTrees)
{
if (actualFrameTrees.count != expectedFrameTrees.size())
return false;
for (_WKFrameTreeNode *root in actualFrameTrees) {
auto index = expectedFrameTrees.findIf([&] (auto& expectedFrameTree) {
return frameTreesMatch(root, ExpectedFrameTree { expectedFrameTree });
});
if (index == WTF::notFound)
return false;
expectedFrameTrees.remove(index);
}
return expectedFrameTrees.isEmpty();
}
static RetainPtr<NSSet> frameTrees(WKWebView *webView)
{
__block RetainPtr<NSSet> result;
[webView _frameTrees:^(NSSet<_WKFrameTreeNode *> *frameTrees) {
result = frameTrees;
}];
while (!result)
Util::spinRunLoop();
return result;
}
static Vector<char> indentation(size_t count)
{
Vector<char> result;
for (size_t i = 0; i < count; i++)
result.append(' ');
result.append(0);
return result;
}
static void printTree(_WKFrameTreeNode *n, size_t indent = 0)
{
if (n.info._isLocalFrame)
WTFLogAlways("%s%@://%@ (pid %d)", indentation(indent).data(), n.info.securityOrigin.protocol, n.info.securityOrigin.host, n.info._processIdentifier);
else
WTFLogAlways("%s(remote) (pid %d)", indentation(indent).data(), n.info._processIdentifier);
for (_WKFrameTreeNode *c in n.childFrames)
printTree(c, indent + 1);
}
static void printTree(const ExpectedFrameTree& n, size_t indent = 0)
{
if (auto* s = std::get_if<String>(&n.remoteOrOrigin))
WTFLogAlways("%s%s", indentation(indent).data(), s->utf8().data());
else
WTFLogAlways("%s(remote)", indentation(indent).data());
for (const auto& c : n.children)
printTree(c, indent + 1);
}
static void checkFrameTreesInProcesses(NSSet<_WKFrameTreeNode *> *actualTrees, const Vector<ExpectedFrameTree>& expectedFrameTrees)
{
bool result = frameTreesMatch(actualTrees, Vector<ExpectedFrameTree> { expectedFrameTrees });
if (!result) {
WTFLogAlways("ACTUAL");
for (_WKFrameTreeNode *n in actualTrees)
printTree(n);
WTFLogAlways("EXPECTED");
for (const auto& e : expectedFrameTrees)
printTree(e);
WTFLogAlways("END");
}
EXPECT_TRUE(result);
}
void checkFrameTreesInProcesses(WKWebView *webView, Vector<ExpectedFrameTree>&& expectedFrameTrees)
{
checkFrameTreesInProcesses(frameTrees(webView).get(), WTFMove(expectedFrameTrees));
}
enum class FrameType : bool { Local, Remote };
static pid_t findFramePID(NSSet<_WKFrameTreeNode *> *set, FrameType local)
{
for (_WKFrameTreeNode *node in set) {
if (node.info._isLocalFrame == (local == FrameType::Local))
return node.info._processIdentifier;
}
EXPECT_FALSE(true);
return 0;
}
TEST(SiteIsolation, LoadingCallbacksAndPostMessage)
{
auto exampleHTML = "<script>"
" window.addEventListener('message', (event) => {"
" alert('parent frame received ' + event.data)"
" }, false);"
" onload = () => {"
" document.getElementById('webkit_frame').contentWindow.postMessage('ping', '*');"
" }"
"</script>"
"<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s;
auto webkitHTML = "<script>"
" window.addEventListener('message', (event) => {"
" parent.window.postMessage(event.data + 'pong', { 'targetOrigin' : '*' });"
" }, false)"
"</script>"_s;
bool finishedLoading { false };
size_t framesCommitted { 0 };
HTTPServer server(HTTPServer::UseCoroutines::Yes, [&](Connection connection) -> Task {
while (1) {
auto request = co_await connection.awaitableReceiveHTTPRequest();
auto path = HTTPServer::parsePath(request);
if (path == "/example"_s) {
co_await connection.awaitableSend(HTTPResponse(exampleHTML).serialize());
continue;
}
if (path == "/webkit"_s) {
size_t contentLength = 2000000 + webkitHTML.length();
co_await connection.awaitableSend(makeString("HTTP/1.1 200 OK\r\nContent-Length: "_s, contentLength, "\r\n\r\n"_s));
co_await connection.awaitableSend(webkitHTML);
co_await connection.awaitableSend(Vector<uint8_t>(1000000, ' '));
while (framesCommitted < 2)
Util::spinRunLoop();
Util::runFor(Seconds(0.5));
EXPECT_EQ(framesCommitted, 2u);
EXPECT_FALSE(finishedLoading);
co_await connection.awaitableSend(Vector<uint8_t>(1000000, ' '));
continue;
}
EXPECT_FALSE(true);
}
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
navigationDelegate.get().didCommitLoadWithRequestInFrame = makeBlockPtr([&](WKWebView *, NSURLRequest *, WKFrameInfo *frameInfo) {
NSString *url = frameInfo.request.URL.absoluteString;
switch (++framesCommitted) {
case 1:
EXPECT_WK_STREQ(url, "https://example.com/example");
EXPECT_TRUE(frameInfo.isMainFrame);
break;
case 2:
EXPECT_WK_STREQ(url, "https://webkit.org/webkit");
EXPECT_FALSE(frameInfo.isMainFrame);
break;
default:
EXPECT_FALSE(true);
break;
}
}).get();
navigationDelegate.get().didFinishNavigation = makeBlockPtr([&](WKWebView *, WKNavigation *navigation) {
if (navigation._request) {
EXPECT_WK_STREQ(navigation._request.URL.absoluteString, "https://example.com/example");
finishedLoading = true;
}
}).get();
__block RetainPtr<NSString> alert;
auto uiDelegate = adoptNS([TestUIDelegate new]);
uiDelegate.get().runJavaScriptAlertPanelWithMessage = ^(WKWebView *, NSString *message, WKFrameInfo *, void (^completionHandler)(void)) {
alert = message;
completionHandler();
};
webView.get().UIDelegate = uiDelegate.get();
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
Util::run(&finishedLoading);
while (!alert)
Util::spinRunLoop();
EXPECT_WK_STREQ(alert.get(), "parent frame received pingpong");
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://webkit.org"_s } }
},
});
}
TEST(SiteIsolation, BasicPostMessageWindowOpen)
{
auto exampleHTML = "<script>"
" window.addEventListener('message', (event) => {"
" w.postMessage('pong', '*');"
" }, false);"
"</script>"_s;
auto webkitHTML = "<script>"
" window.addEventListener('message', (event) => {"
" alert('opened page received ' + event.data);"
" }, false);"
"</script>"_s;
__block bool openerFinishedLoading { false };
__block bool openedFinishedLoading { false };
HTTPServer server({
{ "/example"_s, { exampleHTML } },
{ "/webkit"_s, { webkitHTML } }
}, HTTPServer::Protocol::HttpsProxy);
__block RetainPtr<WKWebView> openerWebView;
__block RetainPtr<WKWebView> openedWebView;
auto openerNavigationDelegate = adoptNS([TestNavigationDelegate new]);
[openerNavigationDelegate allowAnyTLSCertificate];
openerNavigationDelegate.get().didFinishNavigation = ^(WKWebView *opener, WKNavigation *navigation) {
EXPECT_WK_STREQ(navigation._request.URL.absoluteString, "https://example.com/example");
checkFrameTreesInProcesses(opener, { { "https://example.com"_s } });
openerFinishedLoading = true;
};
__block auto openedNavigationDelegate = adoptNS([TestNavigationDelegate new]);
[openedNavigationDelegate allowAnyTLSCertificate];
openedNavigationDelegate.get().didFinishNavigation = ^(WKWebView *, WKNavigation *navigation) {
EXPECT_WK_STREQ(navigation._request.URL.absoluteString, "https://webkit.org/webkit");
checkFrameTreesInProcesses(openerWebView.get(), { { "https://example.com"_s }, { RemoteFrame } });
checkFrameTreesInProcesses(openedWebView.get(), { { "https://webkit.org"_s }, { RemoteFrame } });
auto openerFrames = frameTrees(openerWebView.get());
auto openedFrames = frameTrees(openedWebView.get());
EXPECT_NE([openerWebView _webProcessIdentifier], [openedWebView _webProcessIdentifier]);
EXPECT_EQ(findFramePID(openerFrames.get(), FrameType::Remote), [openedWebView _webProcessIdentifier]);
EXPECT_EQ(findFramePID(openedFrames.get(), FrameType::Remote), [openerWebView _webProcessIdentifier]);
openedFinishedLoading = true;
};
openedNavigationDelegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^completionHandler)(WKNavigationResponsePolicy)) {
auto openerFrames = frameTrees(openerWebView.get());
checkFrameTreesInProcesses(openerFrames.get(), { { "https://example.com"_s }, { RemoteFrame } });
checkFrameTreesInProcesses(openedWebView.get(), { { "https://example.com"_s } });
EXPECT_EQ([openerWebView _webProcessIdentifier], [openedWebView _webProcessIdentifier]);
EXPECT_NE([openedWebView _webProcessIdentifier], [openedWebView _provisionalWebProcessIdentifier]);
EXPECT_EQ(findFramePID(openerFrames.get(), FrameType::Remote), [openedWebView _provisionalWebProcessIdentifier]);
EXPECT_EQ(findFramePID(openerFrames.get(), FrameType::Local), [openerWebView _webProcessIdentifier]);
completionHandler(WKNavigationResponsePolicyAllow);
};
auto configuration = server.httpsProxyConfiguration();
enableSiteIsolation(configuration);
__block RetainPtr<NSString> alert;
auto uiDelegate = adoptNS([TestUIDelegate new]);
uiDelegate.get().runJavaScriptAlertPanelWithMessage = ^(WKWebView *, NSString *message, WKFrameInfo *, void (^completionHandler)(void)) {
alert = message;
completionHandler();
};
uiDelegate.get().createWebViewWithConfiguration = ^(WKWebViewConfiguration *configuration, WKNavigationAction *action, WKWindowFeatures *windowFeatures) {
openedWebView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
openedWebView.get().UIDelegate = uiDelegate.get();
openedWebView.get().navigationDelegate = openedNavigationDelegate.get();
return openedWebView.get();
};
openerWebView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
openerWebView.get().navigationDelegate = openerNavigationDelegate.get();
openerWebView.get().UIDelegate = uiDelegate.get();
openerWebView.get().configuration.preferences.javaScriptCanOpenWindowsAutomatically = YES;
[openerWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
Util::run(&openerFinishedLoading);
[openerWebView evaluateJavaScript:@"w = window.open('https://webkit.org/webkit')" completionHandler:nil];
Util::run(&openedFinishedLoading);
[openedWebView evaluateJavaScript:@"try { window.opener.postMessage('ping', '*'); } catch(e) { alert('error ' + e) }" completionHandler:nil];
while (!alert)
Util::spinRunLoop();
EXPECT_WK_STREQ(alert.get(), "opened page received pong");
}
struct WebViewAndDelegates {
RetainPtr<TestWKWebView> webView;
RetainPtr<TestNavigationDelegate> navigationDelegate;
RetainPtr<TestUIDelegate> uiDelegate;
};
static std::pair<WebViewAndDelegates, WebViewAndDelegates> openerAndOpenedViews(const HTTPServer& server, NSString *url = @"https://example.com/example", bool waitForOpenedNavigation = true)
{
__block WebViewAndDelegates opener;
__block WebViewAndDelegates opened;
opener.navigationDelegate = adoptNS([TestNavigationDelegate new]);
[opener.navigationDelegate allowAnyTLSCertificate];
auto configuration = server.httpsProxyConfiguration();
enableSiteIsolation(configuration);
opener.webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration]);
opener.webView.get().navigationDelegate = opener.navigationDelegate.get();
opener.uiDelegate = adoptNS([TestUIDelegate new]);
opener.uiDelegate.get().createWebViewWithConfiguration = ^(WKWebViewConfiguration *configuration, WKNavigationAction *action, WKWindowFeatures *windowFeatures) {
enableSiteIsolation(configuration);
opened.webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
opened.navigationDelegate = adoptNS([TestNavigationDelegate new]);
[opened.navigationDelegate allowAnyTLSCertificate];
opened.uiDelegate = adoptNS([TestUIDelegate new]);
opened.webView.get().navigationDelegate = opened.navigationDelegate.get();
opened.webView.get().UIDelegate = opened.uiDelegate.get();
return opened.webView.get();
};
[opener.webView setUIDelegate:opener.uiDelegate.get()];
opener.webView.get().configuration.preferences.javaScriptCanOpenWindowsAutomatically = YES;
[opener.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
while (!opened.webView)
Util::spinRunLoop();
if (waitForOpenedNavigation)
[opened.navigationDelegate waitForDidFinishNavigation];
return { WTFMove(opener), WTFMove(opened) };
}
TEST(SiteIsolation, NavigationAfterWindowOpen)
{
HTTPServer server({
{ "/example"_s, { "<script>w = window.open('https://webkit.org/webkit')</script>"_s } },
{ "/webkit"_s, { "hi"_s } },
{ "/example_opened_after_navigation"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [opener, opened] = openerAndOpenedViews(server);
checkFrameTreesInProcesses(opener.webView.get(), { { "https://example.com"_s }, { RemoteFrame } });
checkFrameTreesInProcesses(opened.webView.get(), { { RemoteFrame }, { "https://webkit.org"_s } });
pid_t webKitPid = findFramePID(frameTrees(opener.webView.get()).get(), FrameType::Remote);
[opened.webView evaluateJavaScript:@"window.location = 'https://example.com/example_opened_after_navigation'" completionHandler:nil];
[opened.navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(opener.webView.get(), { { "https://example.com"_s } });
checkFrameTreesInProcesses(opened.webView.get(), { { "https://example.com"_s } });
while (processStillRunning(webKitPid))
Util::spinRunLoop();
}
TEST(SiteIsolation, PreferencesUpdatesToAllProcesses)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://apple.com/apple'></iframe>"_s } },
{ "/apple"_s, { "hi"_s } },
{ "/opened"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
webView.get().configuration.preferences.javaScriptCanOpenWindowsAutomatically = NO;
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
webView.get().configuration.preferences.javaScriptCanOpenWindowsAutomatically = YES;
auto uiDelegate = adoptNS([TestUIDelegate new]);
__block bool opened { false };
uiDelegate.get().createWebViewWithConfiguration = ^WKWebView *(WKWebViewConfiguration *configuration, WKNavigationAction *action, WKWindowFeatures *windowFeatures)
{
opened = true;
return nil;
};
[webView setUIDelegate:uiDelegate.get()];
[webView evaluateJavaScript:@"window.open('https://example.com/opened')" inFrame:[[webView firstChildFrame] info] completionHandler:nil];
Util::run(&opened);
}
TEST(SiteIsolation, ParentOpener)
{
HTTPServer server({
{ "/example"_s, { "<script>w = window.open('https://webkit.org/webkit')</script>"_s } },
{ "/webkit"_s, { "<iframe src='https://apple.com/apple'></iframe>"_s } },
{ "/apple"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [opener, opened] = openerAndOpenedViews(server);
[opened.webView evaluateJavaScript:@"try { opener.postMessage('test1', '*'); alert('posted message 1') } catch(e) { alert(e) }" completionHandler:nil];
EXPECT_WK_STREQ([opened.uiDelegate waitForAlert], "posted message 1");
[opened.webView evaluateJavaScript:@"try { top.opener.postMessage('test2', '*'); alert('posted message 2') } catch(e) { alert(e) }" inFrame:[[opened.webView firstChildFrame] info] completionHandler:nil];
EXPECT_WK_STREQ([opened.uiDelegate waitForAlert], "posted message 2");
}
TEST(SiteIsolation, WindowOpenRedirect)
{
HTTPServer server({
{ "/example1"_s, { "<script>w = window.open('https://webkit.org/webkit1')</script>"_s } },
{ "/webkit1"_s, { 302, { { "Location"_s, "/webkit2"_s } }, "redirecting..."_s } },
{ "/webkit2"_s, { "loaded!"_s } },
{ "/example2"_s, { "<script>w = window.open('https://webkit.org/webkit3')</script>"_s } },
{ "/webkit3"_s, { 302, { { "Location"_s, "https://example.com/example3"_s } }, "redirecting..."_s } },
{ "/example3"_s, { "loaded!"_s } },
{ "/example4"_s, { "<script>w = window.open('https://webkit.org/webkit4')</script>"_s } },
{ "/webkit4"_s, { 302, { { "Location"_s, "https://apple.com/apple"_s } }, "redirecting..."_s } },
{ "/apple"_s, { "loaded!"_s } },
}, HTTPServer::Protocol::HttpsProxy);
{
auto [opener, opened] = openerAndOpenedViews(server, @"https://example.com/example1");
EXPECT_WK_STREQ(opened.webView.get().URL.absoluteString, "https://webkit.org/webkit2");
}
{
auto [opener, opened] = openerAndOpenedViews(server, @"https://example.com/example2");
EXPECT_WK_STREQ(opened.webView.get().URL.absoluteString, "https://example.com/example3");
}
{
auto [opener, opened] = openerAndOpenedViews(server, @"https://example.com/example4");
EXPECT_WK_STREQ(opened.webView.get().URL.absoluteString, "https://apple.com/apple");
}
}
TEST(SiteIsolation, CloseAfterWindowOpen)
{
HTTPServer server({
{ "/example"_s, { "<script>w = window.open('https://webkit.org/webkit')</script>"_s } },
{ "/webkit"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [opener, opened] = openerAndOpenedViews(server);
pid_t webKitPid = findFramePID(frameTrees(opener.webView.get()).get(), FrameType::Remote);
EXPECT_FALSE([[opener.webView objectByEvaluatingJavaScript:@"w.closed"] boolValue]);
[opener.webView evaluateJavaScript:@"w.close()" completionHandler:nil];
[opened.uiDelegate waitForDidClose];
[opened.webView _close];
while (processStillRunning(webKitPid))
Util::spinRunLoop();
checkFrameTreesInProcesses(opener.webView.get(), { { "https://example.com"_s } });
EXPECT_TRUE([[opener.webView objectByEvaluatingJavaScript:@"w.closed"] boolValue]);
}
// FIXME: <rdar://117383420> Add a test that deallocates the opened WKWebView without being asked to by JS.
// Check state using native *and* JS APIs. Make sure processes are torn down as expected.
// Same with the opener WKWebView. We would probably need to set remotePageProxyInOpenerProcess
// to null manually to make the process terminate.
//
// Also test when the opener frame (if it's an iframe) is removed from the tree and garbage collected.
// That should probably do some teardown that should be visible from the API.
TEST(SiteIsolation, PostMessageWithMessagePorts)
{
auto exampleHTML = "<script>"
" const channel = new MessageChannel();"
" channel.port1.onmessage = function() {"
" alert('parent frame received ' + event.data)"
" };"
" onload = () => {"
" document.getElementById('webkit_frame').contentWindow.postMessage('ping', '*', [channel.port2]);"
" }"
"</script>"
"<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s;
auto webkitHTML = "<script>"
" window.addEventListener('message', (event) => {"
" event.ports[0].postMessage('got port and message ' + event.data);"
" }, false)"
"</script>"_s;
bool finishedLoading { false };
HTTPServer server({
{ "/example"_s, { exampleHTML } },
{ "/webkit"_s, { webkitHTML } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
navigationDelegate.get().didFinishNavigation = makeBlockPtr([&](WKWebView *, WKNavigation *navigation) {
if (navigation._request) {
EXPECT_WK_STREQ(navigation._request.URL.absoluteString, "https://example.com/example");
finishedLoading = true;
}
}).get();
__block RetainPtr<NSString> alert;
auto uiDelegate = adoptNS([TestUIDelegate new]);
uiDelegate.get().runJavaScriptAlertPanelWithMessage = ^(WKWebView *, NSString *message, WKFrameInfo *, void (^completionHandler)(void)) {
alert = message;
completionHandler();
};
webView.get().UIDelegate = uiDelegate.get();
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
Util::run(&finishedLoading);
while (!alert)
Util::spinRunLoop();
EXPECT_WK_STREQ(alert.get(), "parent frame received got port and message ping");
auto mainFrame = [webView mainFrame];
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = mainFrame.childFrames.firstObject.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
}
TEST(SiteIsolation, PostMessageWithNotAllowedTargetOrigin)
{
auto exampleHTML = "<script>"
" onload = () => {"
" document.getElementById('webkit_frame').contentWindow.postMessage('ping', 'https://foo.org');"
" }"
"</script>"
"<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s;
auto webkitHTML = "<script>"
" window.addEventListener('message', (event) => {"
" alert('child frame received ' + event.data)"
" }, false);"
" setTimeout(() => { alert('child did not receive message'); }, 1000);"
"</script>"_s;
bool finishedLoading { false };
HTTPServer server(HTTPServer::UseCoroutines::Yes, [&](Connection connection) -> Task {
while (1) {
auto request = co_await connection.awaitableReceiveHTTPRequest();
auto path = HTTPServer::parsePath(request);
if (path == "/example"_s) {
co_await connection.awaitableSend(HTTPResponse(exampleHTML).serialize());
continue;
}
if (path == "/webkit"_s) {
co_await connection.awaitableSend(HTTPResponse(webkitHTML).serialize());
continue;
}
EXPECT_FALSE(true);
}
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
navigationDelegate.get().didFinishNavigation = makeBlockPtr([&](WKWebView *, WKNavigation *navigation) {
if (navigation._request) {
EXPECT_WK_STREQ(navigation._request.URL.absoluteString, "https://example.com/example");
finishedLoading = true;
}
}).get();
__block RetainPtr<NSString> alert;
auto uiDelegate = adoptNS([TestUIDelegate new]);
uiDelegate.get().runJavaScriptAlertPanelWithMessage = ^(WKWebView *, NSString *message, WKFrameInfo *, void (^completionHandler)(void)) {
alert = message;
completionHandler();
};
webView.get().UIDelegate = uiDelegate.get();
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
Util::run(&finishedLoading);
while (!alert)
Util::spinRunLoop();
EXPECT_WK_STREQ(alert.get(), "child did not receive message");
auto mainFrame = [webView mainFrame];
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = mainFrame.childFrames.firstObject.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
}
TEST(SiteIsolation, PostMessageToIFrameWithOpaqueOrigin)
{
auto exampleHTML = "<script>"
" onload = () => {"
" try {"
" document.getElementById('webkit_frame').contentWindow.postMessage('ping', 'data:');"
" } catch (error) {"
" alert(error);"
" }"
" }"
"</script>"
"<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s;
auto webkitHTML = "<script>"
" window.addEventListener('message', (event) => {"
" alert('child frame received ' + event.data)"
" }, false);"
"</script>"_s;
bool finishedLoading { false };
HTTPServer server({
{ "/example"_s, { exampleHTML } },
{ "/webkit"_s, { webkitHTML } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
navigationDelegate.get().didFinishNavigation = makeBlockPtr([&](WKWebView *, WKNavigation *navigation) {
if (navigation._request) {
EXPECT_WK_STREQ(navigation._request.URL.absoluteString, "https://example.com/example");
finishedLoading = true;
}
}).get();
__block RetainPtr<NSString> alert;
auto uiDelegate = adoptNS([TestUIDelegate new]);
uiDelegate.get().runJavaScriptAlertPanelWithMessage = ^(WKWebView *, NSString *message, WKFrameInfo *, void (^completionHandler)(void)) {
alert = message;
completionHandler();
};
webView.get().UIDelegate = uiDelegate.get();
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
Util::run(&finishedLoading);
while (!alert)
Util::spinRunLoop();
EXPECT_WK_STREQ(alert.get(), "SyntaxError: The string did not match the expected pattern.");
auto mainFrame = [webView mainFrame];
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = mainFrame.childFrames.firstObject.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
}
TEST(SiteIsolation, QueryFramesStateAfterNavigating)
{
HTTPServer server({
{ "/page1.html"_s, { "<iframe src='subframe1.html'></iframe><iframe src='subframe2.html'></iframe><iframe src='subframe3.html'></iframe>"_s } },
{ "/page2.html"_s, { "<iframe src='subframe4.html'></iframe>"_s } },
{ "/subframe1.html"_s, { "SubFrame1"_s } },
{ "/subframe2.html"_s, { "SubFrame2"_s } },
{ "/subframe3.html"_s, { "SubFrame3"_s } },
{ "/subframe4.html"_s, { "SubFrame4"_s } }
}, HTTPServer::Protocol::Http);
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero]);
[webView synchronouslyLoadRequest:server.request("/page1.html"_s)];
EXPECT_EQ(3u, [webView mainFrame].childFrames.count);
[webView synchronouslyLoadRequest:server.request("/page2.html"_s)];
EXPECT_EQ(1u, [webView mainFrame].childFrames.count);
}
TEST(SiteIsolation, NavigatingCrossOriginIframeToSameOrigin)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/example_subframe"_s, { "<script>alert('done')</script>"_s } },
{ "/webkit"_s, { "<script>window.location='https://example.com/example_subframe'</script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_EQ(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "example.com");
}
TEST(SiteIsolation, ParentNavigatingCrossOriginIframeToSameOrigin)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe><script>onload = () => { document.getElementById('webkit_frame').src = 'https://example.com/example_subframe' }</script>"_s } },
{ "/example_subframe"_s, { "<script>onload = ()=>{ alert('done') }</script>"_s } },
{ "/webkit"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_EQ(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "example.com");
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { "https://example.com"_s } }
}
});
}
TEST(SiteIsolation, IframeNavigatesSelfWithoutChangingOrigin)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "<script>window.location='/webkit_second'</script>"_s } },
{ "/webkit_second"_s, { "<script>alert('done')</script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "webkit.org");
}
TEST(SiteIsolation, IframeWithConfirm)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "<script>confirm('confirm message')</script>"_s } },
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForConfirm], "confirm message");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "webkit.org");
}
TEST(SiteIsolation, IframeWithPrompt)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "<script>prompt('prompt message', 'default input')</script>"_s } },
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForPromptWithReply:@"default input"], "prompt message");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "webkit.org");
}
TEST(SiteIsolation, GrandchildIframe)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "<iframe onload='alert(\"grandchild loaded successfully\")' srcdoc=\"<script>window.location='https://apple.com/apple'</script>\">"_s } },
{ "/apple"_s, { ""_s } },
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "grandchild loaded successfully");
}
TEST(SiteIsolation, GrandchildIframeSameOriginAsGrandparent)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "<iframe src='https://example.com/example_grandchild'></iframe>\">"_s } },
{ "/example_grandchild"_s, { "<script>alert('grandchild loaded successfully')</script>"_s } },
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "grandchild loaded successfully");
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s, { { RemoteFrame, { { "https://example.com"_s } } } } },
{ RemoteFrame, { { "https://webkit.org"_s, { { RemoteFrame } } } } }
});
}
TEST(SiteIsolation, ChildNavigatingToNewDomain)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/example_subframe"_s, { "<script>alert('done')</script>"_s } },
{ "/webkit"_s, { "<script>window.location='https://foo.com/example_subframe'</script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "foo.com");
}
TEST(SiteIsolation, ChildNavigatingToMainFrameDomain)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/example_subframe"_s, { "<script>alert('done')</script>"_s } },
{ "/webkit"_s, { "<script>window.location='https://example.com/example_subframe'</script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_EQ(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "example.com");
}
TEST(SiteIsolation, ChildNavigatingToSameDomain)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/example_subframe"_s, { "<script>alert('done')</script>"_s } },
{ "/webkit"_s, { "<script>window.location='https://webkit.org/example_subframe'</script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "webkit.org");
}
TEST(SiteIsolation, ChildNavigatingToDomainLoadedOnADifferentPage)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "<script>alert('done')</script>"_s } },
{ "/foo"_s, { "<iframe id='foo'><html><body><p>Hello world.</p></body></html></iframe>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [firstWebView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[firstWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://webkit.org/foo"]]];
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:firstWebView.get().configuration]);
webView.get().navigationDelegate = navigationDelegate.get();
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
auto firstWebViewMainFrame = [firstWebView mainFrame];
EXPECT_NE(firstWebViewMainFrame.info._processIdentifier, 0);
pid_t firstFramePID = firstWebViewMainFrame.info._processIdentifier;
EXPECT_WK_STREQ(firstWebViewMainFrame.info.securityOrigin.host, "webkit.org");
auto mainFrame = [webView mainFrame];
auto childFrame = mainFrame.childFrames.firstObject;
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
EXPECT_EQ(firstFramePID, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "webkit.org");
}
TEST(SiteIsolation, MainFrameWithTwoIFramesInTheSameProcess)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame_1' src='https://webkit.org/a'></iframe><iframe id='webkit_frame_2' src='https://webkit.org/b'></iframe>"_s } },
{ "/a"_s, { "<script>alert('donea')</script>"_s } },
{ "/b"_s, { "<script>alert('doneb')</script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
NSString* alert1 = [webView _test_waitForAlert];
NSString* alert2 = [webView _test_waitForAlert];
if ([alert1 isEqualToString:@"donea"])
EXPECT_WK_STREQ(alert2, "doneb");
else if ([alert1 isEqualToString:@"doneb"])
EXPECT_WK_STREQ(alert2, "donea");
else
EXPECT_TRUE(false);
auto mainFrame = [webView mainFrame];
EXPECT_EQ(mainFrame.childFrames.count, 2u);
_WKFrameTreeNode *childFrame = mainFrame.childFrames.firstObject;
_WKFrameTreeNode *otherChildFrame = mainFrame.childFrames[1];
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
pid_t otherChildFramePid = otherChildFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(otherChildFramePid, 0);
EXPECT_EQ(childFramePid, otherChildFramePid);
EXPECT_NE(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "webkit.org");
EXPECT_WK_STREQ(otherChildFrame.info.securityOrigin.host, "webkit.org");
}
TEST(SiteIsolation, ChildBeingNavigatedToMainFrameDomainByParent)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/example_subframe"_s, { "<script>alert('done')</script>"_s } },
{ "/webkit"_s, { "<html></html>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
auto mainFrame = [webView mainFrame];
auto childFrame = [webView firstChildFrame];
EXPECT_NE(mainFrame.info._processIdentifier, childFrame.info._processIdentifier);
[webView evaluateJavaScript:@"document.getElementById('webkit_frame').src = 'https://example.com/example_subframe'" completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { "https://example.com"_s } }
}
});
while (processStillRunning(childFrame.info._processIdentifier))
Util::spinRunLoop();
}
TEST(SiteIsolation, ChildBeingNavigatedToSameDomainByParent)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe><script>onload = () => { document.getElementById('webkit_frame').src = 'https://webkit.org/example_subframe' }</script>"_s } },
{ "/example_subframe"_s, { "<script>alert('done')</script>"_s } },
{ "/webkit"_s, { "<html></html>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
auto mainFrame = [webView mainFrame];
auto childFrame = [webView firstChildFrame];
pid_t mainFramePid = mainFrame.info._processIdentifier;
pid_t childFramePid = childFrame.info._processIdentifier;
EXPECT_NE(mainFramePid, 0);
EXPECT_NE(childFramePid, 0);
EXPECT_NE(mainFramePid, childFramePid);
EXPECT_WK_STREQ(mainFrame.info.securityOrigin.host, "example.com");
EXPECT_WK_STREQ(childFrame.info.securityOrigin.host, "webkit.org");
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://webkit.org"_s } }
}
});
}
TEST(SiteIsolation, ChildBeingNavigatedToNewDomainByParent)
{
auto appleHTML = "<script>"
"window.addEventListener('message', (event) => {"
" parent.window.postMessage(event.data + 'pong', { 'targetOrigin' : '*' });"
"}, false);"
"alert('apple iframe loaded')"
"</script>"_s;
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe><script>onload = () => { document.getElementById('webkit_frame').src = 'https://apple.com/apple' }</script>"_s } },
{ "/webkit"_s, { "<html></html>"_s } },
{ "/apple"_s, { appleHTML } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "apple iframe loaded");
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://apple.com"_s } }
}
});
NSString *jsCheckingPostMessageRoundTripAfterIframeProcessChange = @""
"window.addEventListener('message', (event) => {"
" alert('parent frame received ' + event.data)"
"}, false);"
"document.getElementById('webkit_frame').contentWindow.postMessage('ping', '*');";
[webView evaluateJavaScript:jsCheckingPostMessageRoundTripAfterIframeProcessChange completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "parent frame received pingpong");
}
TEST(SiteIsolation, IframeRedirectSameSite)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { 302, { { "Location"_s, "https://www.webkit.org/www_webkit"_s } }, "redirecting..."_s } },
{ "/www_webkit"_s, { "arrived!"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://www.webkit.org"_s } }
}
});
}
TEST(SiteIsolation, IframeRedirectCrossSite)
{
HTTPServer server({
{ "/example1"_s, { "<iframe src='https://webkit.org/webkit1'></iframe>"_s } },
{ "/webkit1"_s, { 302, { { "Location"_s, "https://apple.com/apple1"_s } }, "redirecting..."_s } },
{ "/apple1"_s, { "arrived!"_s } },
{ "/example2"_s, { "<iframe src='https://webkit.org/webkit2'></iframe>"_s } },
{ "/webkit2"_s, { 302, { { "Location"_s, "https://webkit.org/webkit3"_s } }, "redirecting..."_s } },
{ "/webkit3"_s, { 302, { { "Location"_s, "https://example.com/example3"_s } }, "redirecting..."_s } },
{ "/example3"_s, { "arrived!"_s } },
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example1"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://apple.com"_s } }
}
});
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example2"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { "https://example.com"_s } }
}
});
}
TEST(SiteIsolation, CrossOriginOpenerPolicy)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { { { "Content-Type"_s, "text/html"_s }, { "Cross-Origin-Opener-Policy"_s, "same-origin"_s } }, "iframe content"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s, { { RemoteFrame } } },
{ RemoteFrame, { { "https://webkit.org"_s } } }
});
[webView waitForNextPresentationUpdate];
}
TEST(SiteIsolation, NavigationWithIFrames)
{
HTTPServer server({
{ "/1"_s, { "<iframe src='https://domain2.com/2'></iframe>"_s } },
{ "/2"_s, { "hi!"_s } },
{ "/3"_s, { "<iframe src='https://domain4.com/4'></iframe>"_s } },
{ "/4"_s, { "<iframe src='https://domain5.com/5'></iframe>"_s } },
{ "/5"_s, { "hi!"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/1"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://domain1.com"_s, { { RemoteFrame } } },
{ RemoteFrame, { { "https://domain2.com"_s } } }
});
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain3.com/3"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://domain3.com"_s, { { RemoteFrame, { { RemoteFrame } } } } },
{ RemoteFrame, { { "https://domain4.com"_s, { { RemoteFrame } } } } },
{ RemoteFrame, { { RemoteFrame, { { "https://domain5.com"_s } } } } }
});
[webView goBack];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://domain1.com"_s, { { RemoteFrame } } },
{ RemoteFrame, { { "https://domain2.com"_s } } }
});
}
TEST(SiteIsolation, RemoveFrames)
{
HTTPServer server({
{ "/webkit_main"_s, { "<iframe src='https://webkit.org/webkit_iframe' id='wk'></iframe><iframe src='https://example.com/example_iframe' id='ex'></iframe>"_s } },
{ "/webkit_iframe"_s, { "hi!"_s } },
{ "/example_iframe"_s, { "<iframe src='example_grandchild_frame'></iframe>"_s } },
{ "/example_grandchild_frame"_s, { "hi!"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://webkit.org/webkit_main"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://webkit.org"_s,
{ { "https://webkit.org"_s }, { RemoteFrame, { { RemoteFrame } } } }
}, { RemoteFrame,
{ { RemoteFrame }, { "https://example.com"_s, { { "https://example.com"_s } } } }
}
});
__block bool removedLocalFrame { false };
[webView evaluateJavaScript:@"var frame = document.getElementById('wk');frame.parentNode.removeChild(frame);" completionHandler:^(id, NSError *error) {
removedLocalFrame = true;
}];
Util::run(&removedLocalFrame);
checkFrameTreesInProcesses(webView.get(), {
{ "https://webkit.org"_s,
{ { RemoteFrame, { { RemoteFrame } } } }
}, { RemoteFrame,
{ { "https://example.com"_s, { { "https://example.com"_s } } } }
}
});
__block bool removedRemoteFrame { false };
[webView evaluateJavaScript:@"var frame = document.getElementById('ex');frame.parentNode.removeChild(frame);" completionHandler:^(id, NSError *error) {
removedRemoteFrame = true;
}];
Util::run(&removedRemoteFrame);
checkFrameTreesInProcesses(webView.get(), {
{ "https://webkit.org"_s }
});
}
TEST(SiteIsolation, ProvisionalLoadFailure)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { HTTPResponse::Behavior::TerminateConnectionAfterReceivingResponse } },
{ "/apple"_s, { "hello"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s, { { "https://example.com"_s } } }
});
[webView evaluateJavaScript:@"var iframe = document.createElement('iframe');document.body.appendChild(iframe);iframe.src = 'https://apple.com/apple'" completionHandler:nil];
Vector<ExpectedFrameTree> expectedFrameTreesAfterAddingApple { {
"https://example.com"_s, { { "https://example.com"_s }, { RemoteFrame } }
}, {
RemoteFrame, { { RemoteFrame }, { "https://apple.com"_s } }
} };
while (!frameTreesMatch(frameTrees(webView.get()).get(), Vector<ExpectedFrameTree> { expectedFrameTreesAfterAddingApple }))
Util::spinRunLoop();
[webView evaluateJavaScript:@"iframe.onload = alert('done');iframe.src = 'https://webkit.org/webkit'" completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
checkFrameTreesInProcesses(webView.get(), WTFMove(expectedFrameTreesAfterAddingApple));
}
TEST(SiteIsolation, MultipleReloads)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "hello"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s, { { RemoteFrame } } },
{ RemoteFrame, { { "https://webkit.org"_s } } }
});
[webView reload];
Util::runFor(0.1_s);
[webView reload];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s, { { RemoteFrame } } },
{ RemoteFrame, { { "https://webkit.org"_s } } }
});
}
#if PLATFORM(MAC)
TEST(SiteIsolation, PropagateMouseEventsToSubframe)
{
auto mainframeHTML = "<script>"
" window.eventTypes = [];"
" window.addEventListener('message', function(event) {"
" window.eventTypes.push(event.data);"
" });"
"</script>"
"<iframe src='https://domain2.com/subframe'></iframe>"_s;
auto subframeHTML = "<script>"
" addEventListener('mousemove', (event) => { window.parent.postMessage('mousemove', '*') });"
" addEventListener('mousedown', (event) => { window.parent.postMessage('mousedown,' + event.pageX + ',' + event.pageY, '*') });"
" addEventListener('mouseup', (event) => { window.parent.postMessage('mouseup,' + event.pageX + ',' + event.pageY, '*') });"
"</script>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { subframeHTML } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server, CGRectMake(0, 0, 800, 600));
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
CGPoint eventLocationInWindow = [webView convertPoint:CGPointMake(50, 50) toView:nil];
[webView mouseEnterAtPoint:eventLocationInWindow];
[webView mouseMoveToPoint:eventLocationInWindow withFlags:0];
[webView mouseDownAtPoint:eventLocationInWindow simulatePressure:NO];
[webView mouseUpAtPoint:eventLocationInWindow];
[webView waitForPendingMouseEvents];
NSArray<NSString *> *eventTypes = [webView objectByEvaluatingJavaScript:@"window.eventTypes"];
EXPECT_EQ(3U, eventTypes.count);
EXPECT_WK_STREQ("mousemove", eventTypes[0]);
EXPECT_WK_STREQ("mousedown,40,40", eventTypes[1]);
EXPECT_WK_STREQ("mouseup,40,40", eventTypes[2]);
}
TEST(SiteIsolation, RunOpenPanel)
{
HTTPServer server({
{ "/mainframe"_s, { "<iframe src='https://b.com/subframe'></iframe>"_s } },
{ "/subframe"_s, { "<!DOCTYPE html><input style='width: 100vw; height: 100vh;' type='file'>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server, CGRectMake(0, 0, 800, 600));
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://a.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto uiDelegate = adoptNS([TestUIDelegate new]);
[webView setUIDelegate:uiDelegate.get()];
__block bool fileSelected = false;
[uiDelegate setRunOpenPanelWithParameters:^(WKWebView *, WKOpenPanelParameters *, WKFrameInfo *, void (^completionHandler)(NSArray<NSURL *> *)) {
fileSelected = true;
completionHandler(@[ [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"test"]] ]);
}];
CGPoint eventLocationInWindow = [webView convertPoint:CGPointMake(100, 100) toView:nil];
[webView mouseDownAtPoint:eventLocationInWindow simulatePressure:NO];
[webView mouseUpAtPoint:eventLocationInWindow];
[webView waitForPendingMouseEvents];
Util::run(&fileSelected);
EXPECT_WK_STREQ("test", [[webView objectByEvaluatingJavaScript:@"document.getElementsByTagName('input')[0].files[0].name" inFrame:[[webView firstChildFrame] info]] stringValue]);
}
TEST(SiteIsolation, CancelOpenPanel)
{
auto subframeHTML = "<!DOCTYPE html><input style='width: 100vw; height: 100vh;' id='file' type='file'>"
"<script>"
"document.getElementById('file').addEventListener('cancel', () => { alert('cancel'); });"
"</script>"_s;
HTTPServer server({
{ "/mainframe"_s, { "<iframe src='https://b.com/subframe'></iframe>"_s } },
{ "/subframe"_s, { subframeHTML } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server, CGRectMake(0, 0, 800, 600));
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://a.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto uiDelegate = adoptNS([TestUIDelegate new]);
[webView setUIDelegate:uiDelegate.get()];
[uiDelegate setRunOpenPanelWithParameters:^(WKWebView *, WKOpenPanelParameters *, WKFrameInfo *, void (^completionHandler)(NSArray<NSURL *> *)) {
completionHandler(nil);
}];
CGPoint eventLocationInWindow = [webView convertPoint:CGPointMake(100, 100) toView:nil];
[webView mouseDownAtPoint:eventLocationInWindow simulatePressure:NO];
[webView mouseUpAtPoint:eventLocationInWindow];
[webView waitForPendingMouseEvents];
EXPECT_WK_STREQ([uiDelegate waitForAlert], "cancel");
}
TEST(SiteIsolation, DragEvents)
{
auto mainframeHTML = "<script>"
" window.events = [];"
" addEventListener('message', function(event) {"
" window.events.push(event.data);"
" });"
"</script>"
"<iframe width='300' height='300' src='https://domain2.com/subframe'></iframe>"_s;
auto subframeHTML = "<body>"
"<div id='draggable' draggable='true' style='width: 100px; height: 100px; background-color: blue;'></div>"
"<script>"
" draggable.addEventListener('dragstart', (event) => { window.parent.postMessage('dragstart', '*') });"
" draggable.addEventListener('dragend', (event) => { window.parent.postMessage('dragend', '*') });"
" draggable.addEventListener('dragenter', (event) => { window.parent.postMessage('dragenter', '*') });"
" draggable.addEventListener('dragleave', (event) => { window.parent.postMessage('dragleave', '*') });"
"</script>"
"</body>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { subframeHTML } }
}, HTTPServer::Protocol::HttpsProxy);
auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
[navigationDelegate allowAnyTLSCertificate];
auto configuration = server.httpsProxyConfiguration();
enableSiteIsolation(configuration);
auto simulator = adoptNS([[DragAndDropSimulator alloc] initWithWebViewFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration]);
RetainPtr webView = [simulator webView];
webView.get().navigationDelegate = navigationDelegate.get();
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
[simulator runFrom:CGPointMake(50, 50) to:CGPointMake(150, 150)];
NSArray<NSString *> *events = [webView objectByEvaluatingJavaScript:@"window.events"];
EXPECT_EQ(4U, events.count);
EXPECT_WK_STREQ("dragstart", events[0]);
EXPECT_WK_STREQ("dragenter", events[1]);
EXPECT_WK_STREQ("dragleave", events[2]);
EXPECT_WK_STREQ("dragend", events[3]);
}
void writeImageDataToPasteboard(NSString *type, NSData *data)
{
[NSPasteboard.generalPasteboard declareTypes:@[type] owner:nil];
[NSPasteboard.generalPasteboard setData:data forType:type];
}
TEST(SiteIsolation, PasteGIF)
{
auto mainframeHTML = "<script>"
" window.events = [];"
" addEventListener('message', function(event) {"
" window.events.push(event.data);"
" });"
"</script>"
"<iframe width='300' height='300' src='https://domain2.com/subframe'></iframe>"_s;
auto subframeHTML = "<body>"
"<div id='editor' contenteditable style=\"height:100%; width: 100%;\"></div>"
"<script>"
"const editor = document.getElementById('editor');"
"editor.focus();"
"editor.addEventListener('paste', (event) => { window.parent.postMessage(event.clipboardData.files[0].name, '*'); });"
"</script>"
"</body>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { subframeHTML } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server, CGRectMake(0, 0, 800, 600));
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
CGPoint eventLocationInWindow = [webView convertPoint:CGPointMake(20, 20) toView:nil];
[webView mouseEnterAtPoint:eventLocationInWindow];
[webView mouseMoveToPoint:eventLocationInWindow withFlags:0];
[webView mouseDownAtPoint:eventLocationInWindow simulatePressure:NO];
[webView mouseUpAtPoint:eventLocationInWindow];
[webView waitForPendingMouseEvents];
auto *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"sunset-in-cupertino-400px" ofType:@"gif" inDirectory:@"TestWebKitAPI.resources"]];
writeImageDataToPasteboard((__bridge NSString *)kUTTypeGIF, data);
[webView paste:nil];
[webView mouseEnterAtPoint:eventLocationInWindow];
[webView mouseMoveToPoint:eventLocationInWindow withFlags:0];
[webView mouseDownAtPoint:eventLocationInWindow simulatePressure:NO];
[webView mouseUpAtPoint:eventLocationInWindow];
[webView waitForPendingMouseEvents];
NSArray<NSString *> *events = [webView objectByEvaluatingJavaScript:@"window.events"];
EXPECT_EQ(1U, events.count);
EXPECT_WK_STREQ("image.gif", events[0]);
}
#endif
TEST(SiteIsolation, ShutDownFrameProcessesAfterNavigation)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "hello"_s } },
{ "/apple"_s, { "hello"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
pid_t iframePID = findFramePID(frameTrees(webView.get()).get(), FrameType::Remote);
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s, { { RemoteFrame } } },
{ RemoteFrame, { { "https://webkit.org"_s } } }
});
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://apple.com/apple"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), { { "https://apple.com"_s } });
while (processStillRunning(iframePID))
Util::spinRunLoop();
}
TEST(SiteIsolation, OpenerProcessSharing)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/opener_iframe'></iframe>"_s } },
{ "/opened"_s, { "<iframe src='https://webkit.org/opened_iframe'></iframe>"_s } },
{ "/opener_iframe"_s, { "hello"_s } },
{ "/opened_iframe"_s, { "<script>alert('done')</script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, delegate] = siteIsolatedViewAndDelegate(server);
__block RetainPtr<TestWKWebView> openedWebView;
__block auto uiDelegate = adoptNS([TestUIDelegate new]);
webView.get().UIDelegate = uiDelegate.get();
uiDelegate.get().createWebViewWithConfiguration = ^(WKWebViewConfiguration *configuration, WKNavigationAction *action, WKWindowFeatures *windowFeatures) {
openedWebView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
static auto openedNavigationDelegate = adoptNS([TestNavigationDelegate new]);
[openedNavigationDelegate allowAnyTLSCertificate];
openedWebView.get().navigationDelegate = openedNavigationDelegate.get();
openedWebView.get().UIDelegate = uiDelegate.get();
return openedWebView.get();
};
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[delegate waitForDidFinishNavigation];
[webView evaluateJavaScript:@"w = window.open('/opened')" completionHandler:nil];
EXPECT_WK_STREQ([uiDelegate waitForAlert], "done");
auto openerMainFrame = [webView mainFrame];
auto openedMainFrame = [openedWebView mainFrame];
pid_t openerMainFramePid = openerMainFrame.info._processIdentifier;
pid_t openedMainFramePid = openedMainFrame.info._processIdentifier;
pid_t openerIframePid = openerMainFrame.childFrames.firstObject.info._processIdentifier;
pid_t openedIframePid = openedMainFrame.childFrames.firstObject.info._processIdentifier;
EXPECT_EQ(openerMainFramePid, openedMainFramePid);
EXPECT_NE(openerMainFramePid, 0);
EXPECT_EQ(openerIframePid, openedIframePid);
EXPECT_NE(openerIframePid, 0);
}
TEST(SiteIsolation, SetFocusedFrame)
{
auto mainframeHTML = "<iframe id='iframe' src='https://domain2.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server, CGRectMake(0, 0, 800, 600));
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_FALSE([webView mainFrame].info._isFocused);
EXPECT_FALSE([webView firstChildFrame].info._isFocused);
[webView evaluateJavaScript:@"document.getElementById('iframe').focus()" completionHandler:nil];
while ([webView mainFrame].info._isFocused || ![webView firstChildFrame].info._isFocused)
Util::spinRunLoop();
[webView evaluateJavaScript:@"window.focus()" completionHandler:nil];
while (![webView mainFrame].info._isFocused || [webView firstChildFrame].info._isFocused)
Util::spinRunLoop();
}
TEST(SiteIsolation, EvaluateJavaScriptInFrame)
{
HTTPServer server({
{ "/mainframe"_s, { "<iframe src='https://domain2.com/subframe'></iframe>"_s } },
{ "/subframe"_s, { "<script>test = 'abc';</script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_WK_STREQ("abc", [webView objectByEvaluatingJavaScript:@"window.test" inFrame:[[webView firstChildFrame] info]]);
}
TEST(SiteIsolation, MainFrameURLAfterFragmentNavigation)
{
NSString *json = @"["
"{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\"blocked_when_fragment_in_top_url\", \"if-top-url\":[\"fragment\"]}},"
"{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\"always_blocked\", \"if-top-url\":[\"http\"]}}"
"]";
__block bool doneRemoving { false };
[WKContentRuleListStore.defaultStore removeContentRuleListForIdentifier:@"Identifier" completionHandler:^(NSError *error) {
doneRemoving = true;
}];
Util::run(&doneRemoving);
__block RetainPtr<WKContentRuleList> list;
[WKContentRuleListStore.defaultStore compileContentRuleListForIdentifier:@"Identifier" encodedContentRuleList:json completionHandler:^(WKContentRuleList *ruleList, NSError *error) {
list = ruleList;
}];
while (!list)
Util::spinRunLoop();
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "hi"_s } },
{ "/blocked_when_fragment_in_top_url"_s, { "loaded successfully"_s } },
{ "/always_blocked"_s, { "loaded successfully"_s } },
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView.get().configuration.userContentController addContentRuleList:list.get()];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
auto canLoadURLInIFrame = [childFrame = RetainPtr { [webView firstChildFrame] }, webView = RetainPtr { webView }] (NSString *path) -> bool {
__block std::optional<bool> loadedSuccessfully;
[webView callAsyncJavaScript:[NSString stringWithFormat:@"try { let response = await fetch('%@'); return await response.text() } catch (e) { return 'load failed' }", path] arguments:nil inFrame:[childFrame info] inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
if ([result isEqualToString:@"loaded successfully"])
loadedSuccessfully = true;
else if ([result isEqualToString:@"load failed"])
loadedSuccessfully = false;
else
EXPECT_FALSE(true);
}];
while (!loadedSuccessfully)
Util::spinRunLoop();
return *loadedSuccessfully;
};
EXPECT_TRUE(canLoadURLInIFrame(@"/blocked_when_fragment_in_top_url"));
EXPECT_FALSE(canLoadURLInIFrame(@"/always_blocked"));
[webView evaluateJavaScript:@"window.location = '#fragment'" completionHandler:nil];
while (![webView.get().URL.fragment isEqualToString:@"fragment"])
Util::spinRunLoop();
EXPECT_FALSE(canLoadURLInIFrame(@"/blocked_when_fragment_in_top_url"));
EXPECT_FALSE(canLoadURLInIFrame(@"/always_blocked"));
}
TEST(SiteIsolation, FocusOpenedWindow)
{
auto openerHTML = "<script>"
" let w = window.open('https://domain2.com/opened');"
"</script>"_s;
HTTPServer server({
{ "/example"_s, { openerHTML } },
{ "/opened"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [opener, opened] = openerAndOpenedViews(server);
EXPECT_FALSE([[[opener.webView mainFrame] info] _isFocused]);
EXPECT_FALSE([[[opened.webView mainFrame] info] _isFocused]);
[opener.webView.get() evaluateJavaScript:@"w.focus()" completionHandler:nil];
while (![[[opened.webView mainFrame] info] _isFocused])
Util::spinRunLoop();
EXPECT_FALSE([[[opener.webView mainFrame] info] _isFocused]);
}
#if PLATFORM(MAC)
TEST(SiteIsolation, OpenedWindowFocusDelegates)
{
auto openerHTML = "<script>"
" let w = window.open('https://domain2.com/opened');"
"</script>"_s;
HTTPServer server({
{ "/example"_s, { openerHTML } },
{ "/opened"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [opener, opened] = openerAndOpenedViews(server);
__block bool calledFocusWebView = false;
[opened.uiDelegate setFocusWebView:^(WKWebView *viewToFocus) {
calledFocusWebView = true;
}];
__block bool calledUnfocusWebView = false;
[opened.uiDelegate setUnfocusWebView:^(WKWebView *viewToFocus) {
calledUnfocusWebView = true;
}];
[opener.webView.get() evaluateJavaScript:@"w.focus()" completionHandler:nil];
Util::run(&calledFocusWebView);
[opener.webView.get() evaluateJavaScript:@"w.blur()" completionHandler:nil];
Util::run(&calledUnfocusWebView);
}
#endif
TEST(SiteIsolation, FindStringInFrame)
{
HTTPServer server({
{ "/mainframe"_s, { "<iframe src='https://domain2.com/subframe'></iframe>"_s } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
EXPECT_FALSE([[webView findStringAndWait:@"Missing string" withConfiguration:findConfiguration.get()] matchFound]);
}
TEST(SiteIsolation, FindStringInNestedFrame)
{
HTTPServer server({
{ "/mainframe"_s, { "<iframe src='https://domain2.com/subframe'></iframe>"_s } },
{ "/subframe"_s, { "<iframe src='https://domain3.com/nested_subframe'></iframe>"_s } },
{ "/nested_subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
EXPECT_FALSE([[webView findStringAndWait:@"Missing string" withConfiguration:findConfiguration.get()] matchFound]);
}
TEST(SiteIsolation, FindStringSelection)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"
"<iframe src='https://domain3.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
using SelectionOffsets = std::array<std::pair<int, int>, 3>;
auto findStringAndValidateResults = [&findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[2].first endOffset:offsets[2].second inFrame:mainFrame.childFrames[1].info]);
};
std::array<SelectionOffsets, 4> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 11 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 0 }, { 0, 11 } } },
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
findConfiguration.get().backwards = YES;
for (auto it = selectionOffsetsForFrames.rbegin() + 1; it != selectionOffsetsForFrames.rend(); ++it)
findStringAndValidateResults(webView.get(), *it);
}
TEST(SiteIsolation, FindStringSelectionWithEmptyFrames)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"
"<iframe src='https://domain3.com/empty_subframe'></iframe>"
"<iframe src='https://domain4.com/subframe'></iframe>"
"<iframe src='https://domain5.com/empty_subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } },
{ "/empty_subframe"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
using SelectionOffsets = std::array<std::pair<int, int>, 3>;
auto findStringAndValidateResults = [&findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[2].first endOffset:offsets[2].second inFrame:mainFrame.childFrames[2].info]);
};
std::array<SelectionOffsets, 4> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 11 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 0 }, { 0, 11 } } },
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
findConfiguration.get().backwards = YES;
for (auto it = selectionOffsetsForFrames.rbegin() + 1; it != selectionOffsetsForFrames.rend(); ++it)
findStringAndValidateResults(webView.get(), *it);
}
TEST(SiteIsolation, FindStringSelectionNoWrap)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
findConfiguration.get().wraps = NO;
using SelectionOffsets = std::array<std::pair<int, int>, 2>;
auto findStringAndValidateResults = [findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
[[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound];
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
};
std::array<SelectionOffsets, 3> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 11 } } },
{ { { 0, 0 }, { 0, 0 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
findConfiguration.get().backwards = YES;
for (auto it = selectionOffsetsForFrames.rbegin() + 1; it != selectionOffsetsForFrames.rend(); ++it)
findStringAndValidateResults(webView.get(), *it);
}
TEST(SiteIsolation, FindStringSelectionBackwards)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"
"<iframe src='https://domain3.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
findConfiguration.get().backwards = YES;
using SelectionOffsets = std::array<std::pair<int, int>, 3>;
auto findStringAndValidateResults = [&findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[2].first endOffset:offsets[2].second inFrame:mainFrame.childFrames[1].info]);
};
std::array<SelectionOffsets, 4> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 0 }, { 0, 11 } } },
{ { { 0, 0 }, { 0, 11 }, { 0, 0 } } },
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
}
TEST(SiteIsolation, FindStringSelectionSameOriginFrames)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"
"<iframe src='https://domain2.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
using SelectionOffsets = std::array<std::pair<int, int>, 3>;
auto findStringAndValidateResults = [&findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[2].first endOffset:offsets[2].second inFrame:mainFrame.childFrames[1].info]);
};
std::array<SelectionOffsets, 4> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 11 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 0 }, { 0, 11 } } },
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
findConfiguration.get().backwards = YES;
for (auto it = selectionOffsetsForFrames.rbegin() + 1; it != selectionOffsetsForFrames.rend(); ++it)
findStringAndValidateResults(webView.get(), *it);
}
TEST(SiteIsolation, FindStringSelectionNestedFrames)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"
"<iframe src='https://domain3.com/subframe'></iframe>"_s;
auto subframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain4.com/nested_subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { subframeHTML } },
{ "/nested_subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
using SelectionOffsets = std::array<std::pair<int, int>, 5>;
auto findStringAndValidateResults = [&findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[2].first endOffset:offsets[2].second inFrame:mainFrame.childFrames[1].info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[3].first endOffset:offsets[3].second inFrame:mainFrame.childFrames[0].childFrames[0].info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[4].first endOffset:offsets[4].second inFrame:mainFrame.childFrames[1].childFrames[0].info]);
};
std::array<SelectionOffsets, 5> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 11 }, { 0, 0 }, { 0, 0 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 11 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 0 }, { 0, 11 }, { 0, 0 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 11 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
findConfiguration.get().backwards = YES;
for (auto it = selectionOffsetsForFrames.rbegin() + 1; it != selectionOffsetsForFrames.rend(); ++it)
findStringAndValidateResults(webView.get(), *it);
}
TEST(SiteIsolation, FindStringSelectionMultipleMatchesInMainFrame)
{
auto mainframeHTML = "<p>Hello world Hello world Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
using SelectionOffsets = std::array<std::pair<int, int>, 2>;
auto findStringAndValidateResults = [&findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
};
std::array<SelectionOffsets, 5> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 } } },
{ { { 12, 23 }, { 0, 0 } } },
{ { { 24, 35 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 11 } } },
{ { { 0, 11 }, { 0, 0 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
findConfiguration.get().backwards = YES;
for (auto it = selectionOffsetsForFrames.rbegin() + 1; it != selectionOffsetsForFrames.rend(); ++it)
findStringAndValidateResults(webView.get(), *it);
}
TEST(SiteIsolation, FindStringSelectionMultipleMatchesInChildFrame)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world Hello world Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
using SelectionOffsets = std::array<std::pair<int, int>, 2>;
auto findStringAndValidateResults = [&findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
};
std::array<SelectionOffsets, 5> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 11 } } },
{ { { 0, 0 }, { 12, 23 } } },
{ { { 0, 0 }, { 24, 35 } } },
{ { { 0, 11 }, { 0, 0 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
findConfiguration.get().backwards = YES;
for (auto it = selectionOffsetsForFrames.rbegin() + 1; it != selectionOffsetsForFrames.rend(); ++it)
findStringAndValidateResults(webView.get(), *it);
}
TEST(SiteIsolation, FindStringSelectionSameOriginFrameBeforeWrap)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
// FIXME(267907): If the iframe is not added like this the UI process and web processes may have mismatched frame trees.
auto addFrameToBody = @"let frame = document.createElement('iframe');"
"frame.setAttribute('src', 'https://domain1.com/subframe');"
"document.body.appendChild(frame);";
__block bool done = false;
[webView evaluateJavaScript:addFrameToBody completionHandler:^(id _Nullable, NSError * _Nullable error) {
done = true;
}];
Util::run(&done);
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
using SelectionOffsets = std::array<std::pair<int, int>, 3>;
auto findStringAndValidateResults = [&findConfiguration](TestWKWebView *webView, const SelectionOffsets& offsets) {
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
auto mainFrame = [webView mainFrame];
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[0].first endOffset:offsets[0].second inFrame:mainFrame.info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[1].first endOffset:offsets[1].second inFrame:mainFrame.childFrames[0].info]);
EXPECT_TRUE([webView selectionRangeHasStartOffset:offsets[2].first endOffset:offsets[2].second inFrame:mainFrame.childFrames[1].info]);
};
std::array<SelectionOffsets, 4> selectionOffsetsForFrames = { {
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 11 }, { 0, 0 } } },
{ { { 0, 0 }, { 0, 0 }, { 0, 11 } } },
{ { { 0, 11 }, { 0, 0 }, { 0, 0 } } }
} };
for (auto& offsets : selectionOffsetsForFrames)
findStringAndValidateResults(webView.get(), offsets);
findConfiguration.get().backwards = YES;
for (auto it = selectionOffsetsForFrames.rbegin() + 1; it != selectionOffsetsForFrames.rend(); ++it)
findStringAndValidateResults(webView.get(), *it);
}
TEST(SiteIsolation, FindStringMatchCount)
{
auto mainframeHTML = "<p>Hello world</p>"
"<iframe src='https://domain2.com/subframe'></iframe>"
"<iframe src='https://domain3.com/subframe'></iframe>"_s;
HTTPServer server({
{ "/mainframe"_s, { mainframeHTML } },
{ "/subframe"_s, { "<p>Hello world</p>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
auto findConfiguration = adoptNS([[WKFindConfiguration alloc] init]);
auto findDelegate = adoptNS([[WKWebViewFindStringFindDelegate alloc] init]);
[webView _setFindDelegate:findDelegate.get()];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_TRUE([[webView findStringAndWait:@"Hello World" withConfiguration:findConfiguration.get()] matchFound]);
EXPECT_EQ(3ul, [findDelegate matchesCount]);
}
#if PLATFORM(MAC)
TEST(SiteIsolation, ProcessDisplayNames)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://apple.com/apple'></iframe>"_s } },
{ "/apple"_s, { "<script></script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
[navigationDelegate allowAnyTLSCertificate];
auto storeConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]);
[storeConfiguration setHTTPSProxy:[NSURL URLWithString:[NSString stringWithFormat:@"https://127.0.0.1:%d/", server.port()]]];
auto viewConfiguration = adoptNS([WKWebViewConfiguration new]);
[viewConfiguration setWebsiteDataStore:adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]).get()];
enableSiteIsolation(viewConfiguration.get());
auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:viewConfiguration.get()]);
webView.get().navigationDelegate = navigationDelegate.get();
__block bool done { false };
[webView.get().configuration.websiteDataStore removeDataOfTypes:WKWebsiteDataStore.allWebsiteDataTypes modifiedSince:NSDate.distantPast completionHandler:^{
done = true;
}];
Util::run(&done);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
pid_t mainFramePID { 0 };
pid_t iframePID { 0 };
auto trees = frameTrees(webView.get());
EXPECT_EQ([trees count], 2u);
for (_WKFrameTreeNode *tree in trees.get()) {
if (tree.info._isLocalFrame)
mainFramePID = tree.info._processIdentifier;
else if (tree.childFrames.count)
iframePID = tree.childFrames[0].info._processIdentifier;
}
EXPECT_NE(mainFramePID, iframePID);
EXPECT_NE(mainFramePID, 0);
EXPECT_NE(iframePID, 0);
done = false;
WKProcessPool *pool = webView.get().configuration.processPool;
[pool _getActivePagesOriginsInWebProcessForTesting:mainFramePID completionHandler:^(NSArray<NSString *> *result) {
EXPECT_EQ(result.count, 1u);
EXPECT_WK_STREQ(result[0], "https://example.com");
done = true;
}];
Util::run(&done);
done = false;
[pool _getActivePagesOriginsInWebProcessForTesting:iframePID completionHandler:^(NSArray<NSString *> *result) {
EXPECT_EQ(result.count, 1u);
EXPECT_WK_STREQ(result[0], "https://apple.com");
done = true;
}];
Util::run(&done);
}
#endif
TEST(SiteIsolation, NavigateOpener)
{
HTTPServer server({
{ "/example"_s, { "<script>w = window.open('https://webkit.org/webkit')</script>"_s } },
{ "/webkit"_s, { "hi"_s } },
{ "/webkit2"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [opener, opened] = openerAndOpenedViews(server);
[opened.webView evaluateJavaScript:@"opener.location = '/webkit2'" completionHandler:nil];
[opener.navigationDelegate waitForDidFinishNavigation];
EXPECT_EQ(opened.webView.get()._webProcessIdentifier, opener.webView.get()._webProcessIdentifier);
checkFrameTreesInProcesses(opener.webView.get(), { { "https://webkit.org"_s } });
checkFrameTreesInProcesses(opened.webView.get(), { { "https://webkit.org"_s } });
}
TEST(SiteIsolation, NavigateOpenerToProvisionalNavigationFailure)
{
HTTPServer server({
{ "/example"_s, { "<script>w = window.open('https://webkit.org/webkit')</script>"_s } },
{ "/webkit"_s, { "hi"_s } },
{ "/terminate"_s, { HTTPResponse::Behavior::TerminateConnectionAfterReceivingResponse } }
}, HTTPServer::Protocol::HttpsProxy);
auto [opener, opened] = openerAndOpenedViews(server);
checkFrameTreesInProcesses(opener.webView.get(), { { "https://example.com"_s }, { RemoteFrame } });
checkFrameTreesInProcesses(opened.webView.get(), { { RemoteFrame }, { "https://webkit.org"_s } });
[opened.webView evaluateJavaScript:@"opener.location = 'https://webkit.org/terminate'" completionHandler:nil];
[opener.navigationDelegate waitForDidFailProvisionalNavigation];
EXPECT_NE(opened.webView.get()._webProcessIdentifier, opener.webView.get()._webProcessIdentifier);
checkFrameTreesInProcesses(opener.webView.get(), { { "https://example.com"_s }, { RemoteFrame } });
checkFrameTreesInProcesses(opened.webView.get(), { { RemoteFrame }, { "https://webkit.org"_s } });
[opened.webView evaluateJavaScript:@"opener.location = 'https://example.com/terminate'" completionHandler:nil];
[opener.navigationDelegate waitForDidFailProvisionalNavigation];
EXPECT_NE(opened.webView.get()._webProcessIdentifier, opener.webView.get()._webProcessIdentifier);
checkFrameTreesInProcesses(opener.webView.get(), { { "https://example.com"_s }, { RemoteFrame } });
checkFrameTreesInProcesses(opened.webView.get(), { { RemoteFrame }, { "https://webkit.org"_s } });
[opened.webView evaluateJavaScript:@"opener.location = 'https://apple.com/terminate'" completionHandler:nil];
[opener.navigationDelegate waitForDidFailProvisionalNavigation];
EXPECT_NE(opened.webView.get()._webProcessIdentifier, opener.webView.get()._webProcessIdentifier);
checkFrameTreesInProcesses(opener.webView.get(), { { "https://example.com"_s }, { RemoteFrame } });
checkFrameTreesInProcesses(opened.webView.get(), { { RemoteFrame }, { "https://webkit.org"_s } });
}
TEST(SiteIsolation, OpenProvisionalFailure)
{
HTTPServer server({
{ "/example"_s, { "<script>w = window.open('https://webkit.org/webkit')</script>"_s } },
{ "/webkit"_s, { HTTPResponse::Behavior::TerminateConnectionAfterReceivingResponse } }
}, HTTPServer::Protocol::HttpsProxy);
auto [opener, opened] = openerAndOpenedViews(server, @"https://example.com/example", false);
[opened.navigationDelegate waitForDidFailProvisionalNavigation];
checkFrameTreesInProcesses(opener.webView.get(), { { "https://example.com"_s } });
checkFrameTreesInProcesses(opened.webView.get(), { { "https://example.com"_s } });
}
TEST(SiteIsolation, NavigateIframeToProvisionalNavigationFailure)
{
HTTPServer server({
{ "/webkit"_s, { "<iframe id='testiframe' src='https://example.com/example'></iframe>"_s } },
{ "/example"_s, { "hi"_s } },
{ "/redirect_to_example_terminate"_s, { 302, { { "Location"_s, "https://example.com/terminate"_s } }, "redirecting..."_s } },
{ "/redirect_to_webkit_terminate"_s, { 302, { { "Location"_s, "https://webkit.org/terminate"_s } }, "redirecting..."_s } },
{ "/redirect_to_apple_terminate"_s, { 302, { { "Location"_s, "https://apple.com/terminate"_s } }, "redirecting..."_s } },
{ "/terminate"_s, { HTTPResponse::Behavior::TerminateConnectionAfterReceivingResponse } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://webkit.org/webkit"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://webkit.org"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://example.com"_s } }
},
});
__block bool provisionalLoadFailed { false };
navigationDelegate.get().didFailProvisionalLoadWithRequestInFrameWithError = ^(WKWebView *, NSURLRequest *, WKFrameInfo *frameInfo, NSError *error) {
EXPECT_WK_STREQ(error.domain, NSURLErrorDomain);
EXPECT_EQ(error.code, NSURLErrorNetworkConnectionLost);
EXPECT_FALSE(frameInfo.isMainFrame);
provisionalLoadFailed = true;
};
__block RetainPtr blockScopeWebView { webView };
auto checkProvisionalLoadFailure = ^(NSString *url) {
provisionalLoadFailed = false;
[blockScopeWebView evaluateJavaScript:[NSString stringWithFormat:@"document.getElementById('testiframe').src = '%@'", url] completionHandler:nil];
while (!provisionalLoadFailed)
Util::spinRunLoop();
checkFrameTreesInProcesses(blockScopeWebView.get(), {
{ "https://webkit.org"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://example.com"_s } }
},
});
};
checkProvisionalLoadFailure(@"https://example.com/terminate");
checkProvisionalLoadFailure(@"https://webkit.org/terminate");
checkProvisionalLoadFailure(@"https://apple.com/terminate");
checkProvisionalLoadFailure(@"https://example.com/redirect_to_example_terminate");
checkProvisionalLoadFailure(@"https://webkit.org/redirect_to_example_terminate");
checkProvisionalLoadFailure(@"https://apple.com/redirect_to_example_terminate");
checkProvisionalLoadFailure(@"https://example.com/redirect_to_webkit_terminate");
checkProvisionalLoadFailure(@"https://webkit.org/redirect_to_webkit_terminate");
checkProvisionalLoadFailure(@"https://apple.com/redirect_to_webkit_terminate");
checkProvisionalLoadFailure(@"https://example.com/redirect_to_apple_terminate");
checkProvisionalLoadFailure(@"https://webkit.org/redirect_to_apple_terminate");
checkProvisionalLoadFailure(@"https://apple.com/redirect_to_apple_terminate");
}
TEST(SiteIsolation, DrawAfterNavigateToDomainAgain)
{
HTTPServer server({
{ "/a"_s, { "<iframe src='https://b.com/b'></iframe>"_s } },
{ "/b"_s, { "hi"_s } },
{ "/c"_s, { "hi"_s } },
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://a.com/a"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://a.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://b.com"_s } }
}
});
[webView evaluateJavaScript:@"window.location = 'https://c.com/c'" completionHandler:nil];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://c.com"_s }
});
[webView evaluateJavaScript:@"window.location = 'https://a.com/a'" completionHandler:nil];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://a.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://b.com"_s } }
}
});
[webView waitForNextPresentationUpdate];
}
TEST(SiteIsolation, CancelProvisionalLoad)
{
HTTPServer server({
{ "/main"_s, {
"<iframe id='testiframe' src='https://example.com/respond_quickly'></iframe>"
"<iframe src='https://example.com/respond_quickly'></iframe>"_s
} },
{ "/respond_quickly"_s, { "hi"_s } },
{ "/never_respond"_s, { HTTPResponse::Behavior::NeverSendResponse } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://webkit.org/main"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://webkit.org"_s,
{ { RemoteFrame }, { RemoteFrame } }
}, { RemoteFrame,
{ { "https://example.com"_s }, { "https://example.com"_s } }
},
});
auto checkStateAfterSequentialFrameLoads = [webView = RetainPtr { webView }, navigationDelegate = RetainPtr { navigationDelegate }] (NSString *first, NSString *second, Vector<ExpectedFrameTree>&& expectedTrees) {
[webView evaluateJavaScript:[NSString stringWithFormat:@"i = document.getElementById('testiframe'); i.addEventListener('load', () => { alert('iframe loaded') }); i.src = '%@'; setTimeout(()=>{ i.src = '%@' }, Math.random() * 100)", first, second] completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "iframe loaded");
checkFrameTreesInProcesses(webView.get(), WTFMove(expectedTrees));
};
checkStateAfterSequentialFrameLoads(@"https://webkit.org/never_respond", @"https://example.com/respond_quickly", {
{ "https://webkit.org"_s,
{ { RemoteFrame }, { RemoteFrame } }
}, { RemoteFrame,
{ { "https://example.com"_s }, { "https://example.com"_s } }
},
});
checkStateAfterSequentialFrameLoads(@"https://example.com/never_respond", @"https://webkit.org/respond_quickly", {
{ "https://webkit.org"_s,
{ { RemoteFrame }, { "https://webkit.org"_s } }
}, { RemoteFrame,
{ { "https://example.com"_s }, { RemoteFrame } }
},
});
checkStateAfterSequentialFrameLoads(@"https://apple.com/never_respond", @"https://webkit.org/respond_quickly", {
{ "https://webkit.org"_s,
{ { RemoteFrame }, { "https://webkit.org"_s } }
}, { RemoteFrame,
{ { "https://example.com"_s }, { RemoteFrame } }
},
});
checkStateAfterSequentialFrameLoads(@"https://apple.com/never_respond", @"https://example.com/respond_quickly", {
{ "https://webkit.org"_s,
{ { RemoteFrame }, { RemoteFrame } }
}, { RemoteFrame,
{ { "https://example.com"_s }, { "https://example.com"_s } }
},
});
checkStateAfterSequentialFrameLoads(@"https://apple.com/never_respond", @"https://apple.com/respond_quickly", {
{ "https://webkit.org"_s,
{ { RemoteFrame }, { RemoteFrame } }
}, { RemoteFrame,
{ { "https://example.com"_s }, { RemoteFrame } }
}, { RemoteFrame,
{ { RemoteFrame }, { "https://apple.com"_s } }
}
});
}
// FIXME: If a provisional load happens in a RemoteFrame with frame children, does anything clear out those
// child frames when the load commits? Probably not. Needs a test.
// FIXME: Add a test that verifies that provisional frames are not accessible via DOMWindow.frames.
// FIXME: Make a test that tries to access its parent that used to be remote during a provisional navigation of
// the parent to that domain to verify that even the main frame uses provisional frames.
TEST(SiteIsolation, OpenThenClose)
{
HTTPServer server({
{ "/example"_s, { "<script>w = window.open('https://webkit.org/webkit')</script>"_s } },
{ "/webkit"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
RetainPtr<WKWebView> retainOpener;
@autoreleasepool {
auto [opener, opened] = openerAndOpenedViews(server, @"https://example.com/example", false);
retainOpener = opener.webView;
}
}
TEST(SiteIsolation, CustomUserAgent)
{
HTTPServer server({
{ "/mainframe"_s, { "<iframe src='https://domain2.com/subframe'></iframe>"_s } },
{ "/subframe"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
[webView setCustomUserAgent:@"Custom UserAgent"];
EXPECT_WK_STREQ(@"Custom UserAgent", [webView objectByEvaluatingJavaScript:@"navigator.userAgent" inFrame:[[webView firstChildFrame] info]]);
}
TEST(SiteIsolation, ApplicationNameForUserAgent)
{
auto mainframeHTML = "<iframe src='https://domain2.com/subframe'></iframe>"_s;
auto subframeHTML = "<script src='https://domain3.com/request_from_subframe'></script>"_s;
bool receivedRequestFromSubframe = false;
HTTPServer server(HTTPServer::UseCoroutines::Yes, [&](Connection connection) -> Task {
while (1) {
auto request = co_await connection.awaitableReceiveHTTPRequest();
auto path = HTTPServer::parsePath(request);
if (path == "/mainframe"_s) {
co_await connection.awaitableSend(HTTPResponse(mainframeHTML).serialize());
continue;
}
if (path == "/subframe"_s) {
co_await connection.awaitableSend(HTTPResponse(subframeHTML).serialize());
continue;
}
if (path == "/request_from_subframe"_s) {
auto headers = String::fromUTF8(request.span()).split("\r\n"_s);
auto userAgentIndex = headers.findIf([](auto& header) {
return header.startsWith("User-Agent:"_s);
});
co_await connection.awaitableSend(HTTPResponse(""_s).serialize());
EXPECT_TRUE(headers[userAgentIndex].endsWith(" Custom UserAgent"_s));
receivedRequestFromSubframe = true;
continue;
}
EXPECT_FALSE(true);
}
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView _setApplicationNameForUserAgent:@"Custom UserAgent"];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"navigator.userAgent" inFrame:[[webView firstChildFrame] info]] hasSuffix:@" Custom UserAgent"]);
Util::run(&receivedRequestFromSubframe);
}
TEST(SiteIsolation, WebsitePoliciesCustomUserAgent)
{
auto mainframeHTML = "<iframe src='https://domain2.com/subframe'></iframe>"_s;
auto subframeHTML = "<script src='https://domain3.com/request_from_subframe'></script>"_s;
bool receivedRequestFromSubframe = false;
bool firstRequest = true;
HTTPServer server(HTTPServer::UseCoroutines::Yes, [&](Connection connection) -> Task {
while (1) {
auto request = co_await connection.awaitableReceiveHTTPRequest();
auto path = HTTPServer::parsePath(request);
if (path == "/mainframe"_s) {
co_await connection.awaitableSend(HTTPResponse(mainframeHTML).serialize());
continue;
}
if (path == "/subframe"_s) {
co_await connection.awaitableSend(HTTPResponse(subframeHTML).serialize());
continue;
}
if (path == "/request_from_subframe"_s) {
auto headers = String::fromUTF8(request.span()).split("\r\n"_s);
auto userAgentIndex = headers.findIf([](auto& header) {
return header.startsWith("User-Agent:"_s);
});
co_await connection.awaitableSend(HTTPResponse(""_s).serialize());
if (firstRequest)
EXPECT_TRUE(headers[userAgentIndex] == "User-Agent: Custom UserAgent"_s);
else
EXPECT_TRUE(headers[userAgentIndex] == "User-Agent: Custom UserAgent2"_s);
receivedRequestFromSubframe = true;
firstRequest = false;
continue;
}
EXPECT_FALSE(true);
}
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
navigationDelegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *navigationAction, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) {
if (navigationAction.targetFrame.mainFrame)
[preferences _setCustomUserAgent:@"Custom UserAgent"];
decisionHandler(WKNavigationActionPolicyAllow, preferences);
};
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
Util::run(&receivedRequestFromSubframe);
receivedRequestFromSubframe = false;
EXPECT_WK_STREQ("Custom UserAgent", [webView objectByEvaluatingJavaScript:@"navigator.userAgent" inFrame:[[webView firstChildFrame] info]]);
navigationDelegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *navigationAction, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) {
if (navigationAction.targetFrame.mainFrame)
[preferences _setCustomUserAgent:@"Custom UserAgent2"];
decisionHandler(WKNavigationActionPolicyAllow, preferences);
};
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain3.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
Util::run(&receivedRequestFromSubframe);
EXPECT_WK_STREQ("Custom UserAgent2", [webView objectByEvaluatingJavaScript:@"navigator.userAgent" inFrame:[[webView firstChildFrame] info]]);
}
TEST(SiteIsolation, WebsitePoliciesCustomUserAgentDuringCrossSiteProvisionalNavigation)
{
auto mainframeHTML = "<iframe id='frame' src='https://domain2.com/subframe'></iframe>"_s;
auto subframeHTML = "<script src='https://domain2.com/request_from_subframe'></script>"_s;
bool receivedRequestFromSubframe = false;
HTTPServer server(HTTPServer::UseCoroutines::Yes, [&](Connection connection) -> Task {
while (1) {
auto request = co_await connection.awaitableReceiveHTTPRequest();
auto path = HTTPServer::parsePath(request);
if (path == "/mainframe"_s) {
co_await connection.awaitableSend(HTTPResponse(mainframeHTML).serialize());
continue;
}
if (path == "/subframe"_s) {
co_await connection.awaitableSend(HTTPResponse(subframeHTML).serialize());
continue;
}
if (path == "/request_from_subframe"_s) {
auto headers = String::fromUTF8(request.span()).split("\r\n"_s);
auto userAgentIndex = headers.findIf([](auto& header) {
return header.startsWith("User-Agent:"_s);
});
co_await connection.awaitableSend(HTTPResponse(""_s).serialize());
EXPECT_TRUE(headers[userAgentIndex] == "User-Agent: Custom UserAgent"_s);
receivedRequestFromSubframe = true;
continue;
}
if (path == "/missing"_s)
continue;
EXPECT_FALSE(true);
}
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
navigationDelegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *navigationAction, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) {
if (navigationAction.targetFrame.mainFrame)
[preferences _setCustomUserAgent:@"Custom UserAgent"];
decisionHandler(WKNavigationActionPolicyAllow, preferences);
};
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
Util::run(&receivedRequestFromSubframe);
receivedRequestFromSubframe = false;
navigationDelegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *navigationAction, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) {
if (navigationAction.targetFrame.mainFrame)
[preferences _setCustomUserAgent:@"Custom UserAgent2"];
decisionHandler(WKNavigationActionPolicyAllow, preferences);
};
navigationDelegate.get().didStartProvisionalNavigation = ^(WKWebView *webView, WKNavigation *) {
[webView evaluateJavaScript:@"document.getElementById('frame').src = 'https://domain4.com/subframe';" completionHandler:nil];
};
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain3.com/missing"]]];
Util::run(&receivedRequestFromSubframe);
}
TEST(SiteIsolation, WebsitePoliciesCustomNavigatorPlatform)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://frame.com/frame'></iframe>"_s } },
{ "/frame"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
navigationDelegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *navigationAction, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) {
if (navigationAction.targetFrame.mainFrame)
[preferences _setCustomNavigatorPlatform:@"Custom Navigator Platform"];
decisionHandler(WKNavigationActionPolicyAllow, preferences);
};
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_WK_STREQ("Custom Navigator Platform", [[webView objectByEvaluatingJavaScript:@"navigator.platform" inFrame:[[webView firstChildFrame] info]] stringValue]);
}
TEST(SiteIsolation, LoadHTMLString)
{
HTTPServer server({
{ "/webkit"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
NSString *html = @"<iframe src='https://webkit.org/webkit'></iframe>";
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadHTMLString:html baseURL:[NSURL URLWithString:@"https://example.com"]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://example.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://webkit.org"_s } }
},
});
[webView loadHTMLString:html baseURL:[NSURL URLWithString:@"https://webkit.org"]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://webkit.org"_s,
{ { "https://webkit.org"_s } }
},
});
}
TEST(SiteIsolation, WebsitePoliciesCustomUserAgentDuringSameSiteProvisionalNavigation)
{
auto mainframeHTML = "<iframe id='frame' src='https://domain2.com/subframe'></iframe>"_s;
auto subframeHTML = "<script src='https://domain2.com/request_from_subframe'></script>"_s;
bool receivedRequestFromSubframe = false;
HTTPServer server(HTTPServer::UseCoroutines::Yes, [&](Connection connection) -> Task {
while (1) {
auto request = co_await connection.awaitableReceiveHTTPRequest();
auto path = HTTPServer::parsePath(request);
if (path == "/mainframe"_s) {
co_await connection.awaitableSend(HTTPResponse(mainframeHTML).serialize());
continue;
}
if (path == "/subframe"_s) {
co_await connection.awaitableSend(HTTPResponse(subframeHTML).serialize());
continue;
}
if (path == "/request_from_subframe"_s) {
auto headers = String::fromUTF8(request.span()).split("\r\n"_s);
auto userAgentIndex = headers.findIf([](auto& header) {
return header.startsWith("User-Agent:"_s);
});
co_await connection.awaitableSend(HTTPResponse(""_s).serialize());
EXPECT_TRUE(headers[userAgentIndex] == "User-Agent: Custom UserAgent"_s);
receivedRequestFromSubframe = true;
continue;
}
if (path == "/missing"_s)
continue;
EXPECT_FALSE(true);
}
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
navigationDelegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *navigationAction, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) {
if (navigationAction.targetFrame.mainFrame)
[preferences _setCustomUserAgent:@"Custom UserAgent"];
decisionHandler(WKNavigationActionPolicyAllow, preferences);
};
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
[navigationDelegate waitForDidFinishNavigation];
Util::run(&receivedRequestFromSubframe);
receivedRequestFromSubframe = false;
navigationDelegate.get().decidePolicyForNavigationActionWithPreferences = ^(WKNavigationAction *navigationAction, WKWebpagePreferences *preferences, void (^decisionHandler)(WKNavigationActionPolicy, WKWebpagePreferences *)) {
if (navigationAction.targetFrame.mainFrame)
[preferences _setCustomUserAgent:@"Custom UserAgent2"];
decisionHandler(WKNavigationActionPolicyAllow, preferences);
};
navigationDelegate.get().didStartProvisionalNavigation = ^(WKWebView *webView, WKNavigation *) {
[webView evaluateJavaScript:@"document.getElementById('frame').src = 'https://domain3.com/subframe';" completionHandler:nil];
};
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/missing"]]];
Util::run(&receivedRequestFromSubframe);
}
TEST(SiteIsolation, ProvisionalLoadFailureOnCrossSiteRedirect)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='webkit_frame' src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { ""_s } },
{ "/redirect"_s, { 302, { { "Location"_s, "https://example.com/terminate"_s } }, "redirecting..."_s } },
{ "/terminate"_s, { HTTPResponse::Behavior::TerminateConnectionAfterReceivingResponse } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
__block bool done = false;
navigationDelegate.get().didFailProvisionalLoadWithRequestInFrameWithError = ^(WKWebView *, NSURLRequest *request, WKFrameInfo *, NSError *) {
EXPECT_WK_STREQ(request.URL.absoluteString, "https://example.com/terminate");
done = true;
};
[webView evaluateJavaScript:@"location.href = 'https://webkit.org/redirect'" inFrame:[[webView firstChildFrame] info] inContentWorld:WKContentWorld.pageWorld completionHandler:nil];
Util::run(&done);
}
TEST(SiteIsolation, SynchronouslyExecuteEditCommandSelectAll)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='iframe' src='https://webkit.org/frame'></iframe>"_s } },
{ "/frame"_s, { "test"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
RetainPtr childFrame = [webView firstChildFrame];
[webView evaluateJavaScript:@"document.getElementById('iframe').focus()" completionHandler:nil];
while (![[childFrame info] _isFocused])
childFrame = [webView firstChildFrame];
[webView _synchronouslyExecuteEditCommand:@"SelectAll" argument:nil];
while (![webView selectionRangeHasStartOffset:0 endOffset:4 inFrame:[childFrame info]])
Util::spinRunLoop();
}
#if PLATFORM(MAC)
TEST(SiteIsolation, SelectAll)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='iframe' src='https://webkit.org/frame'></iframe>"_s } },
{ "/frame"_s, { "test"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
RetainPtr childFrame = [webView firstChildFrame];
[webView evaluateJavaScript:@"document.getElementById('iframe').focus()" completionHandler:nil];
while (![[childFrame info] _isFocused])
childFrame = [webView firstChildFrame];
[webView selectAll:nil];
while (![webView selectionRangeHasStartOffset:0 endOffset:4 inFrame:[childFrame info]])
Util::spinRunLoop();
}
TEST(SiteIsolation, TopContentInsetAfterCrossSiteNavigation)
{
HTTPServer server({
{ "/source"_s, { "<script> location.href = 'https://webkit.org/destination'; </script>"_s } },
{ "/destination"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView _setTopContentInset:10];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/source"]]];
[navigationDelegate waitForDidFinishNavigation];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_EQ(-10, [[webView objectByEvaluatingJavaScript:@"window.innerHeight"] intValue]);
}
#endif
TEST(SiteIsolation, PresentationUpdateAfterCrossSiteNavigation)
{
HTTPServer server({
{ "/source"_s, { "<script> location.href = 'https://webkit.org/destination'; </script>"_s } },
{ "/destination"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/source"]]];
[navigationDelegate waitForDidFinishNavigation];
[navigationDelegate waitForDidFinishNavigation];
[webView waitForNextPresentationUpdate];
}
TEST(SiteIsolation, CanGoBackAfterLoadingAndNavigatingFrame)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='frame' src='https://webkit.org/source'></iframe>"_s } },
{ "/source"_s, { ""_s } },
{ "/destination"_s, { "<script> alert('done'); </script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_FALSE([webView canGoBack]);
[webView evaluateJavaScript:@"location.href = 'https://webkit.org/destination'" inFrame:[[webView firstChildFrame] info] completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "done");
EXPECT_TRUE([webView canGoBack]);
}
TEST(SiteIsolation, CanGoBackAfterNavigatingFrameCrossOrigin)
{
HTTPServer server({
{ "/example"_s, { "<iframe id='frame' src='https://domain1.com/source'></iframe>"_s } },
{ "/source"_s, { ""_s } },
{ "/destination"_s, { "<script> alert('destination'); </script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
[webView evaluateJavaScript:@"location.href = 'https://domain2.com/destination'" inFrame:[[webView firstChildFrame] info] completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "destination");
EXPECT_TRUE([webView canGoBack]);
}
TEST(SiteIsolation, NavigateIframeSameOriginBackForward)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/source'></iframe>"_s } },
{ "/source"_s, { "<script> alert('source'); </script>"_s } },
{ "/destination"_s, { "<script> alert('destination'); </script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "source");
RetainPtr childFrame = [webView firstChildFrame];
[webView evaluateJavaScript:@"location.href = 'https://webkit.org/destination'" inFrame:[childFrame info] completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "destination");
[webView goBack];
EXPECT_WK_STREQ("source", [webView _test_waitForAlert]);
EXPECT_WK_STREQ("https://webkit.org/source", [webView objectByEvaluatingJavaScript:@"location.href" inFrame:[childFrame info]]);
[webView goForward];
EXPECT_WK_STREQ("destination", [webView _test_waitForAlert]);
EXPECT_WK_STREQ("https://webkit.org/destination", [webView objectByEvaluatingJavaScript:@"location.href" inFrame:[childFrame info]]);
}
TEST(SiteIsolation, NavigateIframeCrossOriginBackForward)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://a.com/a'></iframe>"_s } },
{ "/a"_s, { "<script> alert('a'); </script>"_s } },
{ "/b"_s, { "<script> alert('b'); </script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "a");
[webView evaluateJavaScript:@"location.href = 'https://b.com/b'" inFrame:[[webView firstChildFrame] info] completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "b");
[webView goBack];
EXPECT_WK_STREQ([webView _test_waitForAlert], "a");
[webView goForward];
EXPECT_WK_STREQ([webView _test_waitForAlert], "b");
}
TEST(SiteIsolation, ProtocolProcessSeparation)
{
HTTPServer secureServer({
{ "/subdomain"_s, { "hi"_s } },
{ "/no_subdomain"_s, { "hi"_s } }
}, HTTPServer::Protocol::HttpsProxy);
HTTPServer plaintextServer({
{ "http://a.com/"_s, {
"<iframe src='https://a.com/no_subdomain'></iframe>"
"<iframe src='https://subdomain.a.com/subdomain'></iframe>"_s
} }
});
auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
[navigationDelegate allowAnyTLSCertificate];
auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
[storeConfiguration setHTTPSProxy:[NSURL URLWithString:[NSString stringWithFormat:@"https://127.0.0.1:%d/", secureServer.port()]]];
[storeConfiguration setHTTPProxy:[NSURL URLWithString:[NSString stringWithFormat:@"https://127.0.0.1:%d/", plaintextServer.port()]]];
auto viewConfiguration = adoptNS([WKWebViewConfiguration new]);
[viewConfiguration setWebsiteDataStore:adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]).get()];
enableSiteIsolation(viewConfiguration.get());
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:viewConfiguration.get()]);
webView.get().navigationDelegate = navigationDelegate.get();
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://a.com/"]]];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "http://a.com"_s,
{ { RemoteFrame }, { RemoteFrame } }
}, { RemoteFrame,
{ { "https://subdomain.a.com"_s }, { "https://a.com"_s } }
},
});
}
TEST(SiteIsolation, GoBackToPageWithIframe)
{
HTTPServer server({
{ "/a"_s, { "<iframe src='https://frame.com/frame'></iframe>"_s } },
{ "/b"_s, { ""_s } },
{ "/frame"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://a.com/a"]]];
[navigationDelegate waitForDidFinishNavigation];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://b.com/b"]]];
[navigationDelegate waitForDidFinishNavigation];
[webView goBack];
[navigationDelegate waitForDidFinishNavigation];
checkFrameTreesInProcesses(webView.get(), {
{ "https://a.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "https://frame.com"_s } }
},
});
}
TEST(SiteIsolation, NavigateNestedIframeSameOriginBackForward)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://a.com/nest'></iframe>"_s } },
{ "/nest"_s, { "<iframe src='https://a.com/a'></iframe>"_s } },
{ "/a"_s, { "<script> alert('a'); </script>"_s } },
{ "/b"_s, { "<script> alert('b'); </script>"_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "a");
RetainPtr childFrame = [[[webView firstChildFrame] childFrames] firstObject];
[webView evaluateJavaScript:@"location.href = 'https://a.com/b'" inFrame:[childFrame info] completionHandler:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "b");
[webView goBack];
EXPECT_WK_STREQ([webView _test_waitForAlert], "a");
[webView goForward];
EXPECT_WK_STREQ([webView _test_waitForAlert], "b");
}
TEST(SiteIsolation, AdvancedPrivacyProtectionsHideScreenMetricsFromBindings)
{
auto frameHTML = [NSString stringWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"audio-fingerprinting" ofType:@"html" inDirectory:@"TestWebKitAPI.resources"] encoding:NSUTF8StringEncoding error:NULL];
HTTPServer server({
{ "/example"_s, { "<iframe src='https://frame.com/frame'></iframe>"_s } },
{ "/frame"_s, { frameHTML } }
}, HTTPServer::Protocol::HttpsProxy);
auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
[navigationDelegate allowAnyTLSCertificate];
auto configuration = server.httpsProxyConfiguration();
enableSiteIsolation(configuration);
auto preferences = adoptNS([WKWebpagePreferences new]);
[preferences _setNetworkConnectionIntegrityPolicy:_WKWebsiteNetworkConnectionIntegrityPolicyEnhancedTelemetry | _WKWebsiteNetworkConnectionIntegrityPolicyEnabled];
[configuration setDefaultWebpagePreferences:preferences.get()];
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
webView.get().navigationDelegate = navigationDelegate.get();
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
RetainPtr childFrame = [webView firstChildFrame];
EXPECT_EQ(0, [[webView objectByEvaluatingJavaScript:@"screenX" inFrame:[childFrame info]] intValue]);
EXPECT_EQ(0, [[webView objectByEvaluatingJavaScript:@"screenY" inFrame:[childFrame info]] intValue]);
EXPECT_EQ(0, [[webView objectByEvaluatingJavaScript:@"screen.availLeft" inFrame:[childFrame info]] intValue]);
EXPECT_EQ(0, [[webView objectByEvaluatingJavaScript:@"screen.availTop" inFrame:[childFrame info]] intValue]);
}
TEST(SiteIsolation, UpdateWebpagePreferences)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://b.com/frame'></iframe>"_s } },
{ "/frame"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://a.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
auto preferences = adoptNS([WKWebpagePreferences new]);
[preferences _setCustomUserAgent:@"Custom UserAgent"];
[webView _updateWebpagePreferences:preferences.get()];
while (![[webView objectByEvaluatingJavaScript:@"navigator.userAgent" inFrame:[[webView firstChildFrame] info]] isEqualToString:@"Custom UserAgent"])
Util::spinRunLoop();
}
TEST(SiteIsolation, MainFrameRedirectBetweenExistingProcesses)
{
HTTPServer server({
{ "/example"_s, { "<iframe src='https://webkit.org/webkit'></iframe>"_s } },
{ "/webkit"_s, { "hi"_s } },
{ "/webkit_redirect"_s, { 302, { { "Location"_s, "https://example.com/redirected"_s } }, "redirecting..."_s } },
{ "/redirected"_s, { "hi"_s } },
}, HTTPServer::Protocol::HttpsProxy);
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(server);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/example"]]];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_EQ([[webView objectByEvaluatingJavaScript:@"window.length"] intValue], 1);
auto pidBefore = [webView _webProcessIdentifier];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org/webkit_redirect"]]];
[navigationDelegate waitForDidFinishNavigation];
EXPECT_EQ([[webView objectByEvaluatingJavaScript:@"window.length"] intValue], 0);
EXPECT_EQ([webView _webProcessIdentifier], pidBefore);
}
TEST(SiteIsolation, URLSchemeTask)
{
HTTPServer server({
{ "/example"_s, { ""_s } },
{ "/webkit"_s, { ""_s } }
}, HTTPServer::Protocol::HttpsProxy);
auto configuration = adoptNS([WKWebViewConfiguration new]);
auto handler = adoptNS([TestURLSchemeHandler new]);
handler.get().startURLSchemeTaskHandler = ^(WKWebView *, id<WKURLSchemeTask> task) {
if ([task.request.URL.path isEqualToString:@"/example"])
respond(task, "<iframe src='customscheme://webkit.org/webkit'></iframe>");
else if ([task.request.URL.path isEqualToString:@"/webkit"]) {
respond(task, "<script>"
"var xhr = new XMLHttpRequest();"
"xhr.open('GET', '/fetched');"
"xhr.onreadystatechange = function () {"
"if (xhr.readyState == xhr.DONE) { alert(xhr.responseURL + ' ' + xhr.responseText) }"
"};"
"xhr.send();"
"</script>");
} else if ([task.request.URL.path isEqualToString:@"/fetched"]) {
auto newRequest = adoptNS([[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"customscheme://webkit.org/redirected"]]);
[(id<WKURLSchemeTaskPrivate>)task _willPerformRedirection:adoptNS([NSURLResponse new]).get() newRequest:newRequest.get() completionHandler:^(NSURLRequest *request) {
respond(task, "hi");
}];
} else
EXPECT_TRUE(false);
};
[configuration setURLSchemeHandler:handler.get() forURLScheme:@"customscheme"];
auto [webView, navigationDelegate] = siteIsolatedViewAndDelegate(configuration);
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"customscheme://example.com/example"]]];
EXPECT_WK_STREQ([webView _test_waitForAlert], "customscheme://webkit.org/redirected hi");
checkFrameTreesInProcesses(webView.get(), {
{ "customscheme://example.com"_s,
{ { RemoteFrame } }
}, { RemoteFrame,
{ { "customscheme://webkit.org"_s } }
},
});
}
}