blob: 0c0839d76e05019f7d5f858f60af57b162a5e1cc [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/command_line.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/lock.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/browser/fenced_frame/fenced_frame.h"
#include "content/browser/fenced_frame/fenced_frame_url_mapping.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_entry_restore_context_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/origin_util.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/fenced_frame_test_utils.h"
#include "content/test/resource_load_observer.h"
#include "content/test/test_content_browser_client.h"
#include "net/base/features.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "services/network/public/cpp/web_sandbox_flags.h"
#include "services/network/public/mojom/web_sandbox_flags.mojom-shared.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/frame/user_activation_update_types.mojom.h"
#include "url/url_constants.h"
namespace content {
namespace {
EvalJsResult GetOriginFromRenderer(FrameTreeNode* node) {
return EvalJs(node, "self.origin");
}
} // namespace
class FrameTreeBrowserTest : public ContentBrowserTest {
public:
FrameTreeBrowserTest() = default;
FrameTreeBrowserTest(const FrameTreeBrowserTest&) = delete;
FrameTreeBrowserTest& operator=(const FrameTreeBrowserTest&) = delete;
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
};
// Ensures FrameTree correctly reflects page structure during navigations.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, FrameTreeShape) {
GURL base_url = embedded_test_server()->GetURL("A.com", "/site_isolation/");
// Load doc without iframes. Verify FrameTree just has root.
// Frame tree:
// Site-A Root
EXPECT_TRUE(NavigateToURL(shell(), base_url.Resolve("blank.html")));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(0U, root->child_count());
// Add 2 same-site frames. Verify 3 nodes in tree with proper names.
// Frame tree:
// Site-A Root -- Site-A frame1
// \-- Site-A frame2
LoadStopObserver observer1(shell()->web_contents());
EXPECT_TRUE(NavigateToURL(shell(), base_url.Resolve("frames-X-X.html")));
observer1.Wait();
ASSERT_EQ(2U, root->child_count());
EXPECT_EQ(0U, root->child_at(0)->child_count());
EXPECT_EQ(0U, root->child_at(1)->child_count());
}
// TODO(ajwong): Talk with nasko and merge this functionality with
// FrameTreeShape.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, FrameTreeShape2) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
// Check that the root node is properly created.
ASSERT_EQ(3UL, root->child_count());
EXPECT_EQ(std::string(), root->frame_name());
ASSERT_EQ(2UL, root->child_at(0)->child_count());
EXPECT_STREQ("1-1-name", root->child_at(0)->frame_name().c_str());
// Verify the deepest node exists and has the right name.
ASSERT_EQ(2UL, root->child_at(2)->child_count());
EXPECT_EQ(1UL, root->child_at(2)->child_at(1)->child_count());
EXPECT_EQ(0UL, root->child_at(2)->child_at(1)->child_at(0)->child_count());
EXPECT_STREQ(
"3-1-name",
root->child_at(2)->child_at(1)->child_at(0)->frame_name().c_str());
// Navigate to about:blank, which should leave only the root node of the frame
// tree in the browser process.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
root = wc->GetPrimaryFrameTree().root();
EXPECT_EQ(0UL, root->child_count());
EXPECT_EQ(std::string(), root->frame_name());
}
// Test that we can navigate away if the previous renderer doesn't clean up its
// child frames.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, FrameTreeAfterCrash) {
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
// Ensure the view and frame are live.
RenderFrameHostImpl* rfh1 = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
RenderViewHostImpl* rvh = rfh1->render_view_host();
EXPECT_TRUE(rvh->IsRenderViewLive());
EXPECT_TRUE(rfh1->IsRenderFrameLive());
// Crash the renderer so that it doesn't send any FrameDetached messages.
RenderProcessHostWatcher crash_observer(
shell()->web_contents(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
ASSERT_TRUE(
shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->Shutdown(
0));
crash_observer.Wait();
// The frame tree should be cleared.
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
EXPECT_EQ(0UL, root->child_count());
// Ensure the view and frame aren't live anymore.
EXPECT_FALSE(rvh->IsRenderViewLive());
EXPECT_FALSE(rfh1->IsRenderFrameLive());
// Navigate to a new URL.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(0UL, root->child_count());
EXPECT_EQ(url, root->current_url());
RenderFrameHostImpl* rfh2 = root->current_frame_host();
// Ensure the view and frame are live again.
EXPECT_TRUE(rvh->IsRenderViewLive());
EXPECT_TRUE(rfh2->IsRenderFrameLive());
}
// Test that we can navigate away if the previous renderer doesn't clean up its
// child frames.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, NavigateWithLeftoverFrames) {
GURL base_url = embedded_test_server()->GetURL("A.com", "/site_isolation/");
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/frame_tree/top.html")));
// Hang the renderer so that it doesn't send any FrameDetached messages.
// (This navigation will never complete, so don't wait for it.)
shell()->LoadURL(GURL(blink::kChromeUIHangURL));
// Check that the frame tree still has children.
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
ASSERT_EQ(3UL, root->child_count());
// Navigate to a new URL. We use LoadURL because NavigateToURL will try to
// wait for the previous navigation to stop.
TestNavigationObserver tab_observer(wc, 1);
shell()->LoadURL(base_url.Resolve("blank.html"));
tab_observer.Wait();
// The frame tree should now be cleared.
EXPECT_EQ(0UL, root->child_count());
}
// Ensure that IsRenderFrameLive is true for main frames and same-site iframes.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, IsRenderFrameLive) {
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// The root and subframe should each have a live RenderFrame.
EXPECT_TRUE(
root->current_frame_host()->render_view_host()->IsRenderViewLive());
EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
// Load a same-site page into iframe and it should still be live.
GURL http_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), http_url));
EXPECT_TRUE(
root->current_frame_host()->render_view_host()->IsRenderViewLive());
EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
}
// Ensure that origins are correctly set on navigations.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, OriginSetOnNavigation) {
GURL about_blank(url::kAboutBlankURL);
GURL main_url(
embedded_test_server()->GetURL("a.com", "/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContents* contents = shell()->web_contents();
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root =
static_cast<WebContentsImpl*>(contents)->GetPrimaryFrameTree().root();
// Extra '/' is added because the replicated origin is serialized in RFC 6454
// format, which dictates no trailing '/', whereas GURL::GetOrigin does put a
// '/' at the end.
EXPECT_EQ(main_url.DeprecatedGetOriginAsURL().spec(),
root->current_origin().Serialize() + '/');
EXPECT_EQ(
main_url.DeprecatedGetOriginAsURL().spec(),
root->current_frame_host()->GetLastCommittedOrigin().Serialize() + '/');
// The iframe is inititially same-origin.
EXPECT_TRUE(
root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
root->child_at(0)->current_frame_host()->GetLastCommittedOrigin()));
EXPECT_EQ(root->current_origin().Serialize(), GetOriginFromRenderer(root));
EXPECT_EQ(root->child_at(0)->current_origin().Serialize(),
GetOriginFromRenderer(root->child_at(0)));
// Navigate the iframe cross-origin.
GURL frame_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
EXPECT_EQ(frame_url, root->child_at(0)->current_url());
EXPECT_EQ(frame_url.DeprecatedGetOriginAsURL().spec(),
root->child_at(0)->current_origin().Serialize() + '/');
EXPECT_FALSE(
root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
root->child_at(0)->current_frame_host()->GetLastCommittedOrigin()));
EXPECT_EQ(root->current_origin().Serialize(), GetOriginFromRenderer(root));
EXPECT_EQ(root->child_at(0)->current_origin().Serialize(),
GetOriginFromRenderer(root->child_at(0)));
// Parent-initiated about:blank navigation should inherit the parent's a.com
// origin.
NavigateIframeToURL(contents, "1-1-id", about_blank);
EXPECT_EQ(about_blank, root->child_at(0)->current_url());
EXPECT_EQ(main_url.DeprecatedGetOriginAsURL().spec(),
root->child_at(0)->current_origin().Serialize() + '/');
EXPECT_EQ(root->current_frame_host()->GetLastCommittedOrigin().Serialize(),
root->child_at(0)
->current_frame_host()
->GetLastCommittedOrigin()
.Serialize());
EXPECT_TRUE(
root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
root->child_at(0)->current_frame_host()->GetLastCommittedOrigin()));
EXPECT_EQ(root->current_origin().Serialize(), GetOriginFromRenderer(root));
EXPECT_EQ(root->child_at(0)->current_origin().Serialize(),
GetOriginFromRenderer(root->child_at(0)));
GURL data_url("data:text/html,foo");
EXPECT_TRUE(NavigateToURL(shell(), data_url));
// Navigating to a data URL should set a unique origin. This is represented
// as "null" per RFC 6454.
EXPECT_EQ("null", root->current_origin().Serialize());
EXPECT_TRUE(
contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque());
EXPECT_EQ("null", GetOriginFromRenderer(root));
// Re-navigating to a normal URL should update the origin.
EXPECT_TRUE(NavigateToURL(shell(), main_url));
EXPECT_EQ(main_url.DeprecatedGetOriginAsURL().spec(),
root->current_origin().Serialize() + '/');
EXPECT_EQ(
main_url.DeprecatedGetOriginAsURL().spec(),
contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().Serialize() +
'/');
EXPECT_FALSE(
contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque());
EXPECT_EQ(root->current_origin().Serialize(), GetOriginFromRenderer(root));
}
// Tests a cross-origin navigation to a blob URL. The main frame initiates this
// navigation on its grandchild. It should wind up in the main frame's process.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, NavigateGrandchildToBlob) {
WebContents* contents = shell()->web_contents();
FrameTreeNode* root =
static_cast<WebContentsImpl*>(contents)->GetPrimaryFrameTree().root();
// First, snapshot the FrameTree for a normal A(B(A)) case where all frames
// are served over http. The blob test should result in the same structure.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(a))")));
std::string reference_tree = DepictFrameTree(*root);
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// The root node will initiate the navigation; its grandchild node will be the
// target of the navigation.
FrameTreeNode* target = root->child_at(0)->child_at(0);
RenderFrameDeletedObserver deleted_observer(target->current_frame_host());
std::string html =
"<html><body><div>This is blob content.</div>"
"<script>"
"window.parent.parent.postMessage('HI', self.origin);"
"</script></body></html>";
std::string script = JsReplace(
"new Promise((resolve) => {"
" window.addEventListener('message', resolve, false);"
" var blob = new Blob([$1], {type: 'text/html'});"
" var blob_url = URL.createObjectURL(blob);"
" frames[0][0].location.href = blob_url;"
"}).then((event) => {"
" document.body.appendChild(document.createTextNode(event.data));"
" return event.source.location.href;"
"});",
html);
std::string blob_url_string = EvalJs(root, script).ExtractString();
// Wait for the RenderFrame to go away, if this will be cross-process.
if (AreAllSitesIsolatedForTesting())
deleted_observer.WaitUntilDeleted();
EXPECT_EQ(GURL(blob_url_string), target->current_url());
EXPECT_EQ(url::kBlobScheme, target->current_url().scheme());
EXPECT_FALSE(target->current_origin().opaque());
EXPECT_EQ("a.com", target->current_origin().host());
EXPECT_EQ(url::kHttpScheme, target->current_origin().scheme());
EXPECT_EQ("This is blob content.",
EvalJs(target, "document.body.children[0].innerHTML"));
EXPECT_EQ(reference_tree, DepictFrameTree(*root));
}
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, NavigateChildToAboutBlank) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// The leaf node (c.com) will be navigated. Its parent node (b.com) will
// initiate the navigation.
FrameTreeNode* target =
contents->GetPrimaryFrameTree().root()->child_at(0)->child_at(0);
RenderFrameHost* initiator_rfh = target->parent();
// Give the target a name.
EXPECT_TRUE(ExecJs(target, "window.name = 'target';"));
// Use window.open(about:blank), then poll the document for access.
EvalJsResult about_blank_origin = EvalJs(
initiator_rfh,
"new Promise(resolve => {"
" var didNavigate = false;"
" var intervalID = setInterval(function() {"
" if (!didNavigate) {"
" didNavigate = true;"
" window.open('about:blank', 'target');"
" }"
" // Poll the document until it doesn't throw a SecurityError.\n"
" try {"
" frames[0].document.write('Hi from ' + document.domain);"
" } catch (e) { return; }"
" clearInterval(intervalID);"
" resolve(frames[0].self.origin);"
" }, 16);"
"});");
EXPECT_EQ(target->current_origin(), about_blank_origin);
EXPECT_EQ(GURL(url::kAboutBlankURL), target->current_url());
EXPECT_EQ(url::kAboutScheme, target->current_url().scheme());
EXPECT_FALSE(target->current_origin().opaque());
EXPECT_EQ("b.com", target->current_origin().host());
EXPECT_EQ(url::kHttpScheme, target->current_origin().scheme());
EXPECT_EQ("Hi from b.com", EvalJs(target, "document.body.innerHTML"));
}
// Nested iframes, three origins: A(B(C)). Frame A navigates C to about:blank
// (via window.open). This should wind up in A's origin per the spec. Test fails
// because of http://crbug.com/564292
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest,
DISABLED_NavigateGrandchildToAboutBlank) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// The leaf node (c.com) will be navigated. Its grandparent node (a.com) will
// initiate the navigation.
FrameTreeNode* target =
contents->GetPrimaryFrameTree().root()->child_at(0)->child_at(0);
RenderFrameHost* initiator_rfh = target->parent()->GetParent();
// Give the target a name.
EXPECT_TRUE(ExecJs(target, "window.name = 'target';"));
// Use window.open(about:blank), then poll the document for access.
EvalJsResult about_blank_origin =
EvalJs(initiator_rfh,
"new Promise((resolve) => {"
" var didNavigate = false;"
" var intervalID = setInterval(() => {"
" if (!didNavigate) {"
" didNavigate = true;"
" window.open('about:blank', 'target');"
" }"
" // May raise a SecurityError, that's expected.\n"
" try {"
" frames[0][0].document.write('Hi from ' + document.domain);"
" } catch (e) { return; }"
" clearInterval(intervalID);"
" resolve(frames[0][0].self.origin);"
" }, 16);"
"});");
EXPECT_EQ(target->current_origin(), about_blank_origin);
EXPECT_EQ(GURL(url::kAboutBlankURL), target->current_url());
EXPECT_EQ(url::kAboutScheme, target->current_url().scheme());
EXPECT_FALSE(target->current_origin().opaque());
EXPECT_EQ("a.com", target->current_origin().host());
EXPECT_EQ(url::kHttpScheme, target->current_origin().scheme());
EXPECT_EQ("Hi from a.com", EvalJs(target, "document.body.innerHTML"));
}
// Tests a cross-origin navigation to a data: URL. The main frame initiates this
// navigation on its grandchild. It should wind up in the main frame's process
// and have precursor origin of the main frame origin.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, NavigateGrandchildToDataUrl) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b(c))"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// The leaf node (c.com) will be navigated. Its grandparent node (a.com) will
// initiate the navigation.
FrameTreeNode* target =
contents->GetPrimaryFrameTree().root()->child_at(0)->child_at(0);
RenderFrameHostImpl* initiator_rfh = target->parent()->GetParent();
// Give the target a name.
EXPECT_TRUE(ExecJs(target, "window.name = 'target';"));
// Navigate the target frame through the initiator frame.
{
TestFrameNavigationObserver observer(target);
EXPECT_TRUE(ExecJs(initiator_rfh,
"window.open('data:text/html,content', 'target');"));
observer.Wait();
}
url::Origin original_target_origin =
target->current_frame_host()->GetLastCommittedOrigin();
EXPECT_TRUE(original_target_origin.opaque());
EXPECT_EQ(original_target_origin.GetTupleOrPrecursorTupleIfOpaque(),
url::SchemeHostPort(main_url));
// Navigate the grandchild frame again cross-process to foo.com, then
// go back in session history. The origin for the data: URL must be preserved.
{
TestFrameNavigationObserver observer(target);
EXPECT_TRUE(ExecJs(target, JsReplace("window.location = $1",
embedded_test_server()->GetURL(
"foo.com", "/title2.html"))));
observer.Wait();
}
EXPECT_NE(original_target_origin,
target->current_frame_host()->GetLastCommittedOrigin());
{
TestFrameNavigationObserver observer(target);
contents->GetController().GoBack();
observer.Wait();
}
url::Origin target_origin =
target->current_frame_host()->GetLastCommittedOrigin();
EXPECT_TRUE(target_origin.opaque());
EXPECT_EQ(target_origin.GetTupleOrPrecursorTupleIfOpaque(),
url::SchemeHostPort(main_url));
EXPECT_EQ(target_origin, original_target_origin);
}
// Ensures that iframe with srcdoc is always put in the same origin as its
// parent frame.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, ChildFrameWithSrcdoc) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = contents->GetPrimaryFrameTree().root();
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
std::string frame_origin = EvalJs(child, "self.origin;").ExtractString();
EXPECT_TRUE(
child->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
url::Origin::Create(GURL(frame_origin))));
EXPECT_FALSE(
root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
url::Origin::Create(GURL(frame_origin))));
// Create a new iframe with srcdoc and add it to the main frame. It should
// be created in the same SiteInstance as the parent.
{
std::string script(
"var f = document.createElement('iframe');"
"f.srcdoc = 'some content';"
"document.body.appendChild(f)");
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, script));
EXPECT_EQ(2U, root->child_count());
observer.Wait();
EXPECT_TRUE(root->child_at(1)->current_url().IsAboutSrcdoc());
EvalJsResult js_result = EvalJs(root->child_at(1), "self.origin");
EXPECT_EQ(root->current_frame_host()
->GetLastCommittedURL()
.DeprecatedGetOriginAsURL(),
GURL(js_result.ExtractString()));
EXPECT_NE(child->current_frame_host()
->GetLastCommittedURL()
.DeprecatedGetOriginAsURL(),
GURL(js_result.ExtractString()));
}
// Set srcdoc on the existing cross-site frame. It should navigate the frame
// back to the origin of the parent.
{
std::string script(
"var f = document.getElementById('child-0');"
"f.srcdoc = 'some content';");
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, script));
observer.Wait();
EXPECT_TRUE(child->current_url().IsAboutSrcdoc());
EXPECT_EQ(root->current_frame_host()->GetLastCommittedOrigin().Serialize(),
EvalJs(child, "self.origin"));
}
}
// Ensure that sandbox flags are correctly set in the main frame when set by
// Content-Security-Policy header.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, SandboxFlagsSetForMainFrame) {
GURL main_url(embedded_test_server()->GetURL("/csp_sandboxed_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Verify that sandbox flags are set properly for the root FrameTreeNode and
// RenderFrameHost. Root frame is sandboxed with "allow-scripts".
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures,
root->active_sandbox_flags());
EXPECT_EQ(root->active_sandbox_flags(),
root->current_frame_host()->active_sandbox_flags());
// Verify that child frames inherit sandbox flags from the root. First frame
// has no explicitly set flags of its own, and should inherit those from the
// root. Second frame is completely sandboxed.
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures,
root->child_at(0)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures,
root->child_at(0)->active_sandbox_flags());
EXPECT_EQ(root->child_at(0)->active_sandbox_flags(),
root->child_at(0)->current_frame_host()->active_sandbox_flags());
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll,
root->child_at(1)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll,
root->child_at(1)->active_sandbox_flags());
EXPECT_EQ(root->child_at(1)->active_sandbox_flags(),
root->child_at(1)->current_frame_host()->active_sandbox_flags());
// Navigating the main frame to a different URL should clear sandbox flags.
GURL unsandboxed_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root, unsandboxed_url));
// Verify that sandbox flags are cleared properly for the root FrameTreeNode
// and RenderFrameHost.
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->active_sandbox_flags());
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->current_frame_host()->active_sandbox_flags());
}
// Ensure that sandbox flags are correctly set when child frames are created.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, SandboxFlagsSetForChildFrames) {
GURL main_url(embedded_test_server()->GetURL("/sandboxed_frames.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Verify that sandbox flags are set properly for all FrameTreeNodes.
// First frame is completely sandboxed; second frame uses "allow-scripts",
// which resets both SandboxFlags::Scripts and
// SandboxFlags::AutomaticFeatures bits per blink::parseSandboxPolicy(), and
// third frame has "allow-scripts allow-same-origin".
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll,
root->child_at(0)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures,
root->child_at(1)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kOrigin,
root->child_at(2)->effective_frame_policy().sandbox_flags);
// Sandboxed frames should set a unique origin unless they have the
// "allow-same-origin" directive.
EXPECT_EQ("null", root->child_at(0)->current_origin().Serialize());
EXPECT_EQ("null", root->child_at(1)->current_origin().Serialize());
EXPECT_EQ(main_url.DeprecatedGetOriginAsURL().spec(),
root->child_at(2)->current_origin().Serialize() + "/");
// Navigating to a different URL should not clear sandbox flags.
GURL frame_url(embedded_test_server()->GetURL("/title1.html"));
NavigateFrameToURL(root->child_at(0), frame_url);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll,
root->child_at(0)->effective_frame_policy().sandbox_flags);
}
// Ensure that sandbox flags are correctly set in the child frames when set by
// Content-Security-Policy header, and in combination with the sandbox iframe
// attribute.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest,
SandboxFlagsSetByCSPForChildFrames) {
GURL main_url(embedded_test_server()->GetURL("/sandboxed_frames_csp.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Verify that sandbox flags are set properly for all FrameTreeNodes.
// First frame has no iframe sandbox flags, but the framed document is served
// with a CSP header which sets "allow-scripts", "allow-popups" and
// "allow-pointer-lock".
// Second frame is sandboxed with "allow-scripts", "allow-pointer-lock" and
// "allow-orientation-lock", and the framed document is also served with a CSP
// header which uses "allow-popups" and "allow-pointer-lock". The resulting
// sandbox for the frame should only have "allow-pointer-lock".
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->active_sandbox_flags());
EXPECT_EQ(root->active_sandbox_flags(),
root->current_frame_host()->active_sandbox_flags());
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->child_at(0)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(
network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kPopups &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kTopNavigationToCustomProtocols,
root->child_at(0)->active_sandbox_flags());
EXPECT_EQ(root->child_at(0)->active_sandbox_flags(),
root->child_at(0)->current_frame_host()->active_sandbox_flags());
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kOrientationLock,
root->child_at(1)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock,
root->child_at(1)->active_sandbox_flags());
EXPECT_EQ(root->child_at(1)->active_sandbox_flags(),
root->child_at(1)->current_frame_host()->active_sandbox_flags());
// Navigating to a different URL *should* clear CSP-set sandbox flags, but
// should retain those flags set by the frame owner.
GURL frame_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->child_at(0)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kNone,
root->child_at(0)->active_sandbox_flags());
EXPECT_EQ(root->child_at(0)->active_sandbox_flags(),
root->child_at(0)->current_frame_host()->active_sandbox_flags());
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), frame_url));
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kOrientationLock,
root->child_at(1)->effective_frame_policy().sandbox_flags);
EXPECT_EQ(network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kOrientationLock,
root->child_at(1)->active_sandbox_flags());
EXPECT_EQ(root->child_at(1)->active_sandbox_flags(),
root->child_at(1)->current_frame_host()->active_sandbox_flags());
}
// Ensure that a popup opened from a subframe sets its opener to the subframe's
// FrameTreeNode, and that the opener is cleared if the subframe is destroyed.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest, SubframeOpenerSetForNewWindow) {
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Open a new window from a subframe.
ShellAddedObserver new_shell_observer;
GURL popup_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(
ExecJs(root->child_at(0), JsReplace("window.open($1);", popup_url)));
Shell* new_shell = new_shell_observer.GetShell();
WebContents* new_contents = new_shell->web_contents();
EXPECT_TRUE(WaitForLoadStop(new_contents));
// Check that the new window's opener points to the correct subframe on
// original window.
FrameTreeNode* popup_root =
static_cast<WebContentsImpl*>(new_contents)->GetPrimaryFrameTree().root();
EXPECT_EQ(root->child_at(0), popup_root->opener());
// Close the original window. This should clear the new window's opener.
shell()->Close();
EXPECT_EQ(nullptr, popup_root->opener());
}
// Tests that the user activation bits get cleared when a same-site document is
// installed in the frame.
IN_PROC_BROWSER_TEST_F(FrameTreeBrowserTest,
ClearUserActivationForNewDocument) {
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Set the user activation bits.
root->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
EXPECT_TRUE(root->HasStickyUserActivation());
EXPECT_TRUE(root->HasTransientUserActivation());
// Install a new same-site document to check the clearing of user activation
// bits.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
}
// This test class was originally inserted for testing fenced frame
// implementations for both implementations, namely, ShadowDOM, and kMPArch. For
// new tests, consider adding them to MPArchFencedFramesFrameTreeBrowserTest, if
// ShadowDOM tests are not necessary.
class FencedFrameTreeBrowserTest
: public FrameTreeBrowserTest,
public ::testing::WithParamInterface<
blink::features::FencedFramesImplementationType> {
public:
// Provides meaningful param names instead of /0 and /1.
static std::string DescribeParams(
const ::testing::TestParamInfo<ParamType>& info) {
switch (info.param) {
case blink::features::FencedFramesImplementationType::kShadowDOM:
return "ShadowDOM";
case blink::features::FencedFramesImplementationType::kMPArch:
return "MPArch";
}
}
FencedFrameTreeBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
scoped_feature_list_.InitWithFeaturesAndParameters(
{{blink::features::kFencedFrames,
{{"implementation_type",
GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM
? "shadow_dom"
: "mparch"}}},
{blink::features::kThirdPartyStoragePartitioning, {}},
{net::features::kPartitionedCookies, {}},
{features::kPrivacySandboxAdsAPIsOverride, {}}},
{/* disabled_features */});
}
net::Error InvalidUrnError() {
return GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM
? net::ERR_ABORTED
: net::ERR_INVALID_URL;
}
// This is needed because `TestFrameNavigationObserver` doesn't work properly
// from within the context of a fenced frame's FrameTree. See the comments
// below.
void NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
const ToRenderFrameHost& adapter,
GURL url,
const std::string& navigate_script,
net::Error expected_net_error_code = net::OK) {
RenderFrameHostImpl* rfh =
static_cast<RenderFrameHostImpl*>(adapter.render_frame_host());
EXPECT_TRUE(rfh->frame_tree_node()->IsInFencedFrameTree());
RenderFrameHostImpl* target_rfh = rfh->GetParentOrOuterDocument();
ExecuteNavigationOrHistoryScriptInFencedFrameTree(
target_rfh, rfh, navigate_script, expected_net_error_code);
}
void UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
const ToRenderFrameHost& adapter,
const std::string& history_script) {
RenderFrameHostImpl* rfh =
static_cast<RenderFrameHostImpl*>(adapter.render_frame_host());
EXPECT_TRUE(rfh->frame_tree_node()->IsInFencedFrameTree());
ExecuteNavigationOrHistoryScriptInFencedFrameTree(rfh, rfh, history_script);
}
void ExecuteNavigationOrHistoryScriptInFencedFrameTree(
RenderFrameHostImpl* target_rfh,
RenderFrameHostImpl* fenced_frame_rfh,
const std::string& script,
net::Error expected_net_error_code = net::OK) {
TestFrameNavigationObserver observer(fenced_frame_rfh);
EXPECT_TRUE(ExecJs(target_rfh, script));
observer.Wait();
EXPECT_EQ(observer.last_net_error_code(), expected_net_error_code);
}
FrameTreeNode* AddIframeInFencedFrame(FrameTreeNode* fenced_frame,
unsigned int child_index) {
EXPECT_TRUE(
ExecJs(fenced_frame,
"var iframe_within_ff = document.createElement('iframe');"
"document.body.appendChild(iframe_within_ff);"));
EXPECT_EQ(child_index + 1, fenced_frame->child_count());
auto* iframe = fenced_frame->child_at(child_index);
EXPECT_FALSE(iframe->IsFencedFrameRoot());
EXPECT_TRUE(iframe->IsInFencedFrameTree());
return iframe;
}
// Navigates the element created in AddIframeInFencedFrame.
void NavigateIframeInFencedFrame(
FrameTreeNode* iframe,
const GURL& url,
net::Error expected_net_error_code = net::OK) {
EXPECT_FALSE(iframe->IsFencedFrameRoot());
EXPECT_TRUE(iframe->IsInFencedFrameTree());
// Navigate the iframe.
std::string navigate_script =
JsReplace("iframe_within_ff.src = $1;", url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
iframe, url, navigate_script, expected_net_error_code);
}
FrameTreeNode* AddNestedFencedFrame(FrameTreeNode* fenced_frame,
unsigned int child_index) {
EXPECT_TRUE(ExecJs(
fenced_frame,
"var nested_fenced_frame = document.createElement('fencedframe');"
"document.body.appendChild(nested_fenced_frame);"));
EXPECT_EQ(child_index + 1, fenced_frame->child_count());
auto* nested_fenced_frame =
GetFencedFrameRootNode(fenced_frame->child_at(child_index));
EXPECT_TRUE(nested_fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(nested_fenced_frame->IsInFencedFrameTree());
return nested_fenced_frame;
}
// Navigates the element created in AddNestedFencedFrame.
void NavigateNestedFencedFrame(FrameTreeNode* nested_fenced_frame,
const GURL& url) {
EXPECT_TRUE(nested_fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(nested_fenced_frame->IsInFencedFrameTree());
std::string navigate_script =
JsReplace("nested_fenced_frame.src = $1;", url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
nested_fenced_frame, url, navigate_script);
}
void SetUpOnMainThread() override {
// Set up the host resolver to allow serving separate sites, so we can
// perform cross-process navigation.
host_resolver()->AddRule("*", "127.0.0.1");
// Fenced frames require potentially trustworthy URLs so creating an https
// server.
https_server_.RegisterRequestMonitor(
base::BindRepeating(&FencedFrameTreeBrowserTest::ObserveRequestHeaders,
base::Unretained(this)));
https_server_.ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
net::test_server::RegisterDefaultHandlers(&https_server_);
ASSERT_TRUE(https_server_.Start());
}
// Invoked on "EmbeddedTestServer IO Thread".
void ObserveRequestHeaders(const net::test_server::HttpRequest& request) {
base::AutoLock auto_lock(requests_lock_);
std::string val = request.headers.find("Cookie") != request.headers.end()
? request.headers.at("Cookie").c_str()
: "";
cookie_headers_map_.insert(std::make_pair(request.GetURL().path(), val));
val = request.headers.find("Sec-Fetch-Dest") != request.headers.end()
? request.headers.at("Sec-Fetch-Dest").c_str()
: "";
sec_fetch_dest_headers_map_.insert(
std::make_pair(request.GetURL().path(), val));
}
// Returns true if the cookie header was present in the last request received
// by the server with the same `url.path()`. Also asserts that the cookie
// header value matches that given in `expected_value`, if it exists. Also
// clears the value that was just checked by the method invocation.
bool CheckAndClearCookieHeader(
const GURL& url,
const std::string& expected_value = "",
const base::Location& from_here = base::Location::Current()) {
base::AutoLock auto_lock(requests_lock_);
SCOPED_TRACE(from_here.ToString());
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::string file_name = url.path();
CHECK(cookie_headers_map_.find(file_name) != cookie_headers_map_.end());
std::string header = cookie_headers_map_[file_name];
EXPECT_EQ(expected_value, header);
cookie_headers_map_.erase(file_name);
return !header.empty();
}
bool CheckAndClearSecFetchDestHeader(
const GURL& url,
const std::string& expected_value = "",
const base::Location& from_here = base::Location::Current()) {
base::AutoLock auto_lock(requests_lock_);
SCOPED_TRACE(from_here.ToString());
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::string file_name = url.path();
CHECK(sec_fetch_dest_headers_map_.find(file_name) !=
sec_fetch_dest_headers_map_.end());
std::string header = sec_fetch_dest_headers_map_[file_name];
EXPECT_EQ(expected_value, header);
sec_fetch_dest_headers_map_.erase(file_name);
return !header.empty();
}
net::EmbeddedTestServer* https_server() { return &https_server_; }
~FencedFrameTreeBrowserTest() override {
// Shutdown the server explicitly so that there is no race with the
// destruction of cookie_headers_map_ and invocation of RequestMonitor.
if (https_server_.Started()) {
EXPECT_TRUE(https_server_.ShutdownAndWaitUntilComplete());
}
}
WebContentsImpl* web_contents() {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
RenderFrameHostImpl* primary_main_frame_host() {
return web_contents()->GetPrimaryMainFrame();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
base::Lock requests_lock_;
std::map<std::string, std::string> cookie_headers_map_
GUARDED_BY(requests_lock_);
std::map<std::string, std::string> sec_fetch_dest_headers_map_
GUARDED_BY(requests_lock_);
net::EmbeddedTestServer https_server_;
};
// Tests that the fenced frame gets navigated to an actual url given a urn:uuid.
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
CheckFencedFrameNavigationWithUUID) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
GURL https_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(https_url);
EXPECT_TRUE(urn_uuid.is_valid());
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
{
TestFrameNavigationObserver observer(fenced_frame_root_node);
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, navigate_urn_script));
observer.WaitForCommit();
}
EXPECT_EQ(
https_url,
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(
url::Origin::Create(https_url),
fenced_frame_root_node->current_frame_host()->GetLastCommittedOrigin());
// Parent will still see the src as the urn_uuid and not the mapped url.
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, "f.src"));
// The parent will not be able to access window.frames[0] as fenced frames are
// not visible via frames[].
EXPECT_FALSE(ExecJs(root, "window.frames[0].location"));
EXPECT_EQ(0, EvalJs(root, "window.frames.length"));
}
IN_PROC_BROWSER_TEST_P(
FencedFrameTreeBrowserTest,
TwoFencedFrameNavigationToSameSharedStorageOriginatedUUID_SameMetadata) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
{
EXPECT_TRUE(ExecJs(root,
"var f1 = document.createElement('fencedframe');"
"f1.mode = 'opaque-ads';"
"document.body.appendChild(f1);"));
EXPECT_TRUE(ExecJs(root,
"var f2 = document.createElement('fencedframe');"
"f2.mode = 'opaque-ads';"
"document.body.appendChild(f2);"));
}
EXPECT_EQ(2U, root->child_count());
FrameTreeNode* fenced_frame_root_node1 =
GetFencedFrameRootNode(root->child_at(0));
FrameTreeNode* fenced_frame_root_node2 =
GetFencedFrameRootNode(root->child_at(1));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
const GURL urn_uuid = url_mapping.GeneratePendingMappedURN();
const GURL mapped_url =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
SimulateSharedStorageURNMappingComplete(
url_mapping, urn_uuid, mapped_url,
/*shared_storage_origin=*/url::Origin::Create(GURL("https://bar.com")),
/*budget_to_charge=*/2.0);
{
TestFrameNavigationObserver observer(
fenced_frame_root_node1->current_frame_host());
std::string navigate_urn_script = JsReplace("f1.src = $1;", urn_uuid);
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, navigate_urn_script));
observer.Wait();
}
{
TestFrameNavigationObserver observer(
fenced_frame_root_node2->current_frame_host());
std::string navigate_urn_script = JsReplace("f2.src = $1;", urn_uuid);
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, navigate_urn_script));
observer.Wait();
}
EXPECT_TRUE(fenced_frame_root_node1->FindSharedStorageBudgetMetadata());
EXPECT_EQ(fenced_frame_root_node1->FindSharedStorageBudgetMetadata(),
fenced_frame_root_node2->FindSharedStorageBudgetMetadata());
}
// Test the scenario where the FF navigation is deferred and then resumed, and
// the mapped url is a valid one. The navigation is expected to succeed.
IN_PROC_BROWSER_TEST_P(
FencedFrameTreeBrowserTest,
FencedFrameNavigationWithPendingMappedUUID_MappingSuccess_ValidURL) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
const GURL urn_uuid = url_mapping.GeneratePendingMappedURN();
const GURL mapped_url =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
TestFrameNavigationObserver observer(
fenced_frame_root_node->current_frame_host());
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, navigate_urn_script));
// After the previous EvalJs, the NavigationRequest should have been created,
// but may not have begun. Wait for BeginNavigation() and expect it to be
// deferred on fenced frame url mapping.
NavigationRequest* request = fenced_frame_root_node->navigation_request();
if (!request->is_deferred_on_fenced_frame_url_mapping_for_testing()) {
base::RunLoop run_loop;
request->set_begin_navigation_callback_for_testing(
run_loop.QuitWhenIdleClosure());
run_loop.Run();
EXPECT_TRUE(request->is_deferred_on_fenced_frame_url_mapping_for_testing());
}
EXPECT_TRUE(url_mapping.HasObserverForTesting(urn_uuid, request));
auto* budget_metadata =
fenced_frame_root_node->FindSharedStorageBudgetMetadata();
EXPECT_FALSE(budget_metadata);
// Trigger the mapping to resume the deferred navigation.
SimulateSharedStorageURNMappingComplete(
url_mapping, urn_uuid, mapped_url,
/*shared_storage_origin=*/url::Origin::Create(GURL("https://bar.com")),
/*budget_to_charge=*/2.0);
EXPECT_FALSE(url_mapping.HasObserverForTesting(urn_uuid, request));
observer.Wait();
EXPECT_EQ(
mapped_url,
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
budget_metadata = fenced_frame_root_node->FindSharedStorageBudgetMetadata();
EXPECT_TRUE(budget_metadata);
EXPECT_EQ(budget_metadata->origin,
url::Origin::Create(GURL("https://bar.com")));
EXPECT_DOUBLE_EQ(budget_metadata->budget_to_charge, 2.0);
}
// Test the scenario where the FF navigation is deferred and then resumed, and
// the mapped url is invalid. The navigation is expected to fail.
IN_PROC_BROWSER_TEST_P(
FencedFrameTreeBrowserTest,
FencedFrameNavigationWithPendingMappedUUID_MappingSuccess_InvalidURL) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
const GURL urn_uuid = url_mapping.GeneratePendingMappedURN();
const GURL mapped_url =
https_server()->GetURL("a.test", "/fenced_frames/nonexistent-url.html");
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
TestFrameNavigationObserver observer(
fenced_frame_root_node->current_frame_host());
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, navigate_urn_script));
// After the previous EvalJs, the NavigationRequest should have been created,
// but may not have begun. Wait for BeginNavigation() and expect it to be
// deferred on fenced frame url mapping.
NavigationRequest* request = fenced_frame_root_node->navigation_request();
if (!request->is_deferred_on_fenced_frame_url_mapping_for_testing()) {
base::RunLoop run_loop;
request->set_begin_navigation_callback_for_testing(
run_loop.QuitWhenIdleClosure());
run_loop.Run();
EXPECT_TRUE(request->is_deferred_on_fenced_frame_url_mapping_for_testing());
}
EXPECT_TRUE(url_mapping.HasObserverForTesting(urn_uuid, request));
// Trigger the mapping to resume the deferred navigation.
SimulateSharedStorageURNMappingComplete(
url_mapping, urn_uuid, mapped_url,
/*shared_storage_origin=*/url::Origin::Create(GURL("https://bar.com")),
/*budget_to_charge=*/2.0);
EXPECT_FALSE(url_mapping.HasObserverForTesting(urn_uuid, request));
// In NavigationRequest::OnResponseStarted(), for fenced frame, it manually
// fails the navigation with net::ERR_BLOCKED_BY_RESPONSE.
observer.Wait();
EXPECT_EQ(observer.last_net_error_code(), net::ERR_BLOCKED_BY_RESPONSE);
auto* metadata = fenced_frame_root_node->FindSharedStorageBudgetMetadata();
EXPECT_FALSE(metadata);
}
IN_PROC_BROWSER_TEST_P(
FencedFrameTreeBrowserTest,
FencedFrameNavigationWithPendingMappedUUID_NavigationCanceledDuringDeferring) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
const GURL urn_uuid = url_mapping.GeneratePendingMappedURN();
const GURL mapped_url =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
TestFrameNavigationObserver observer(
fenced_frame_root_node->current_frame_host());
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, navigate_urn_script));
// After the previous EvalJs, the NavigationRequest should have been created,
// but may not have begun. Wait for BeginNavigation() and expect it to be
// deferred on fenced frame url mapping.
NavigationRequest* request = fenced_frame_root_node->navigation_request();
if (!request->is_deferred_on_fenced_frame_url_mapping_for_testing()) {
base::RunLoop run_loop;
request->set_begin_navigation_callback_for_testing(
run_loop.QuitWhenIdleClosure());
run_loop.Run();
EXPECT_TRUE(request->is_deferred_on_fenced_frame_url_mapping_for_testing());
}
EXPECT_TRUE(url_mapping.HasObserverForTesting(urn_uuid, request));
// Navigate to a new URL. The previous navigation should have been canceled.
// And `request` should have been removed from `url_mapping`.
const GURL new_url =
https_server()->GetURL("a.test", "/fenced_frames/empty.html");
EXPECT_EQ(new_url.spec(),
EvalJs(root, JsReplace("f.src = $1;", new_url.spec())));
EXPECT_FALSE(url_mapping.HasObserverForTesting(urn_uuid, request));
observer.Wait();
EXPECT_EQ(
new_url,
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
CheckFencedFrameCookiesNavigation) {
// Create an a.test main page and set cookies. Then create a same-origin
// fenced frame. Its request should not carry the cookies that were set.
GURL main_url = https_server()->GetURL("a.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
RenderFrameHostImpl* root_rfh =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->current_frame_host();
// Set SameSite=Lax and SameSite=None cookies and retrieve them.
EXPECT_TRUE(ExecJs(root_rfh,
"document.cookie = 'B=2; SameSite=Lax';"
"document.cookie = 'C=2; SameSite=None; Secure';"));
EXPECT_EQ("B=2; C=2", EvalJs(root_rfh, "document.cookie;"));
// Test the fenced frame.
EXPECT_TRUE(ExecJs(root_rfh,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root_rfh->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root_rfh->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
GURL https_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root_rfh->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(https_url);
EXPECT_TRUE(urn_uuid.is_valid());
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, urn_uuid, navigate_urn_script);
EXPECT_EQ(
https_url,
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(
url::Origin::Create(https_url),
fenced_frame_root_node->current_frame_host()->GetLastCommittedOrigin());
EXPECT_FALSE(CheckAndClearCookieHeader(https_url));
// Run the same test for an iframe inside the fenced frame. It shouldn't be
// able to send cookies either.
// Add a nested iframe inside the fenced frame which needs to be a URL that
// also opts in to be allowed to load inside of a fenced frame.
GURL iframe_url(
https_server()->GetURL("a.test", "/fenced_frames/nested.html"));
EXPECT_EQ(0U, fenced_frame_root_node->child_count());
AddIframeInFencedFrame(fenced_frame_root_node, 0);
NavigateIframeInFencedFrame(fenced_frame_root_node->child_at(0), iframe_url);
EXPECT_EQ(iframe_url, fenced_frame_root_node->child_at(0)
->current_frame_host()
->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(iframe_url), fenced_frame_root_node->child_at(0)
->current_frame_host()
->GetLastCommittedOrigin());
EXPECT_FALSE(CheckAndClearCookieHeader(iframe_url));
// Check that a subresource request from the main document should have the
// cookies since that is outside the fenced frame tree.
ResourceLoadObserver observer(shell());
GURL image_url = https_server()->GetURL("a.test", "/image.jpg");
EXPECT_TRUE(
ExecJs(root_rfh, JsReplace("var img = document.createElement('img');"
"document.body.appendChild(img);",
image_url)));
std::string load_script = JsReplace("img.src = $1;", image_url.spec());
EXPECT_EQ(image_url.spec(), EvalJs(root_rfh, load_script));
observer.WaitForResourceCompletion(image_url);
EXPECT_TRUE(CheckAndClearCookieHeader(image_url, "B=2; C=2"));
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
CheckPartitionedCookiesWithNonce) {
// Create an a.test main page and set cookies. Then create a same-origin
// fenced frame.
GURL main_url = https_server()->GetURL("a.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
RenderFrameHostImpl* root_rfh =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->current_frame_host();
// Set SameSite=Lax and SameSite=None cookies and retrieve them.
EXPECT_TRUE(ExecJs(root_rfh,
"document.cookie = 'B=2; SameSite=Lax';"
"document.cookie = 'C=2; SameSite=None; Secure';"));
EXPECT_EQ("B=2; C=2", EvalJs(root_rfh, "document.cookie;"));
// Add and navigate a fenced frame.
EXPECT_TRUE(ExecJs(root_rfh,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root_rfh->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root_rfh->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
GURL https_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root_rfh->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(https_url);
EXPECT_TRUE(urn_uuid.is_valid());
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, urn_uuid, navigate_urn_script);
EXPECT_EQ(
https_url,
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(
url::Origin::Create(https_url),
fenced_frame_root_node->current_frame_host()->GetLastCommittedOrigin());
// Create cookies in the Fenced Frame.
EXPECT_TRUE(ExecJs(fenced_frame_root_node->current_frame_host(),
"document.cookie = 'B=3; SameSite=Lax';"
"document.cookie = 'C=3; SameSite=None; Secure';"));
const net::IsolationInfo& isolation_info =
fenced_frame_root_node->current_frame_host()
->GetIsolationInfoForSubresources();
EXPECT_TRUE(isolation_info.nonce());
absl::optional<net::CookiePartitionKey> partition_key =
net::CookiePartitionKey::FromNetworkIsolationKey(
isolation_info.network_isolation_key());
EXPECT_TRUE(partition_key && partition_key->nonce());
net::CookiePartitionKeyCollection cookie_partition_key_collection =
net::CookiePartitionKeyCollection::FromOptional(partition_key);
std::vector<net::CanonicalCookie> cookies =
GetCanonicalCookies(shell()->web_contents()->GetBrowserContext(),
https_url, cookie_partition_key_collection);
EXPECT_EQ(2u, cookies.size());
for (auto cookie : cookies) {
EXPECT_TRUE(cookie.IsPartitioned());
EXPECT_TRUE(cookie.PartitionKey() && cookie.PartitionKey()->nonce());
EXPECT_EQ(cookie.PartitionKey()->nonce(), partition_key->nonce());
EXPECT_EQ("3", cookie.Value());
}
// Run the same test for an iframe inside the fenced frame. It should be
// able to access the same cookies.
// Add a nested iframe inside the fenced frame which needs to be a URL that
// also opts in to be allowed to load inside of a fenced frame.
GURL iframe_url(
https_server()->GetURL("a.test", "/fenced_frames/nested.html"));
EXPECT_EQ(0U, fenced_frame_root_node->child_count());
AddIframeInFencedFrame(fenced_frame_root_node, 0);
NavigateIframeInFencedFrame(fenced_frame_root_node->child_at(0), iframe_url);
EXPECT_EQ(iframe_url, fenced_frame_root_node->child_at(0)
->current_frame_host()
->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(iframe_url), fenced_frame_root_node->child_at(0)
->current_frame_host()
->GetLastCommittedOrigin());
EXPECT_EQ("B=3; C=3",
EvalJs(fenced_frame_root_node->child_at(0)->current_frame_host(),
"document.cookie;"));
}
// Tests when a frame is considered a fenced frame or being inside a fenced
// frame tree.
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, CheckIsFencedFrame) {
GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"var fenced_frame = document.createElement('fencedframe');"
"document.body.appendChild(fenced_frame);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
// Add an iframe.
EXPECT_TRUE(ExecJs(root,
"var iframe = document.createElement('iframe');"
"document.body.appendChild(iframe);"));
EXPECT_EQ(2U, root->child_count());
EXPECT_FALSE(root->child_at(1)->IsFencedFrameRoot());
EXPECT_FALSE(root->child_at(1)->IsInFencedFrameTree());
// Add a nested iframe inside the fenced frame.
// Before we execute script on the fenced frame, we must navigate it once.
// This is because the root of a FrameTree does not call
// RenderFrameHostImpl::RenderFrameCreated() on its owned RFHI, meaning there
// is nothing to execute script in.
{
// Navigate the fenced frame.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
std::string navigate_script =
JsReplace("fenced_frame.src = $1;", fenced_frame_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, fenced_frame_url, navigate_script);
}
AddIframeInFencedFrame(fenced_frame_root_node, 0);
AddNestedFencedFrame(fenced_frame_root_node, 1);
EXPECT_EQ(2U, fenced_frame_root_node->child_count());
}
// Tests a nonce is correctly set in the isolation info for a fenced frame tree.
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
CheckIsolationInfoAndStorageKeyNonce) {
GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
auto* fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
// Before we check the IsolationInfo/StorageKey on the fenced frame, we must
// navigate it once. This is because the root of a FrameTree does not call
// RenderFrameHostImpl::RenderFrameCreated() on its owned RFHI.
{
// Navigate the fenced frame.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
std::string navigate_script =
JsReplace("f.src = $1;", fenced_frame_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, fenced_frame_url, navigate_script);
}
// There should be a nonce in the IsolationInfo.
const net::IsolationInfo& isolation_info =
fenced_frame->current_frame_host()->GetIsolationInfoForSubresources();
EXPECT_TRUE(isolation_info.nonce().has_value());
absl::optional<base::UnguessableToken> fenced_frame_nonce =
fenced_frame->fenced_frame_nonce();
EXPECT_TRUE(fenced_frame_nonce.has_value());
EXPECT_EQ(fenced_frame_nonce.value(), isolation_info.nonce().value());
// There should be a nonce in the StorageKey.
EXPECT_TRUE(
fenced_frame->current_frame_host()->storage_key().nonce().has_value());
EXPECT_EQ(fenced_frame_nonce.value(),
fenced_frame->current_frame_host()->storage_key().nonce().value());
// Add an iframe. It should not have a nonce.
EXPECT_TRUE(ExecJs(root,
"var subframe = document.createElement('iframe');"
"document.body.appendChild(subframe);"));
EXPECT_EQ(2U, root->child_count());
auto* iframe = root->child_at(1);
EXPECT_FALSE(iframe->IsFencedFrameRoot());
EXPECT_FALSE(iframe->IsInFencedFrameTree());
const net::IsolationInfo& iframe_isolation_info =
iframe->current_frame_host()->GetIsolationInfoForSubresources();
EXPECT_FALSE(iframe_isolation_info.nonce().has_value());
EXPECT_FALSE(iframe->current_frame_host()->storage_key().nonce().has_value());
// Navigate the iframe. It should still not have a nonce.
EXPECT_TRUE(NavigateToURLFromRenderer(
iframe, https_server()->GetURL("b.test", "/title1.html")));
const net::IsolationInfo& iframe_new_isolation_info =
iframe->current_frame_host()->GetIsolationInfoForSubresources();
EXPECT_FALSE(iframe_new_isolation_info.nonce().has_value());
EXPECT_FALSE(iframe->current_frame_host()->storage_key().nonce().has_value());
// Add a nested iframe inside the fenced frame which needs to be a URL that
// also opts in to be allowed to load inside of a fenced frame.
AddIframeInFencedFrame(fenced_frame, 0);
const net::IsolationInfo& nested_iframe_isolation_info =
fenced_frame->child_at(0)
->current_frame_host()
->GetIsolationInfoForSubresources();
EXPECT_TRUE(nested_iframe_isolation_info.nonce().has_value());
// Check that a nested iframe in the fenced frame tree has the same nonce
// value as its parent.
EXPECT_EQ(fenced_frame_nonce.value(),
nested_iframe_isolation_info.nonce().value());
absl::optional<base::UnguessableToken> nested_iframe_nonce =
fenced_frame->child_at(0)->fenced_frame_nonce();
EXPECT_EQ(nested_iframe_isolation_info.nonce().value(),
nested_iframe_nonce.value());
EXPECT_EQ(fenced_frame_nonce.value(), fenced_frame->child_at(0)
->current_frame_host()
->storage_key()
.nonce()
.value());
// Navigate the iframe. It should still have the same nonce.
NavigateIframeInFencedFrame(
fenced_frame->child_at(0),
https_server()->GetURL("b.test", "/fenced_frames/title1.html"));
const net::IsolationInfo& nested_iframe_new_isolation_info =
fenced_frame->child_at(0)
->current_frame_host()
->GetIsolationInfoForSubresources();
EXPECT_EQ(nested_iframe_new_isolation_info.nonce().value(),
nested_iframe_nonce.value());
EXPECT_EQ(fenced_frame_nonce.value(), fenced_frame->child_at(0)
->current_frame_host()
->storage_key()
.nonce()
.value());
// Add a nested fenced frame.
auto* nested_fenced_frame = AddNestedFencedFrame(fenced_frame, 1);
GetFencedFrameRootNode(fenced_frame->child_at(1));
absl::optional<base::UnguessableToken> nested_fframe_nonce =
nested_fenced_frame->fenced_frame_nonce();
EXPECT_TRUE(nested_fframe_nonce.has_value());
// Check that a nested fenced frame has a different value than its parent
// fenced frame.
EXPECT_NE(fenced_frame_nonce.value(), nested_fframe_nonce.value());
// Check that the nonce does not change when there is a cross-document
// navigation.
NavigateNestedFencedFrame(
nested_fenced_frame,
https_server()->GetURL("b.test", "/fenced_frames/title1.html"));
absl::optional<base::UnguessableToken> new_fenced_frame_nonce =
fenced_frame->fenced_frame_nonce();
EXPECT_NE(absl::nullopt, new_fenced_frame_nonce);
EXPECT_EQ(new_fenced_frame_nonce.value(), fenced_frame_nonce.value());
}
// Tests that a fenced frame and a same-origin iframe at the same level do not
// share the same storage partition.
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, CheckUniqueStorage) {
GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
auto* fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
// Before we check the storage key on the fenced frame, we must navigate it
// once. This is because the root of a FrameTree does not call
// RenderFrameHostImpl::RenderFrameCreated() on its owned RFHI.
{
// Navigate the fenced frame.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
std::string navigate_script =
JsReplace("f.src = $1;", fenced_frame_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, fenced_frame_url, navigate_script);
}
// There should be a nonce in the StorageKey.
EXPECT_TRUE(
fenced_frame->current_frame_host()->storage_key().nonce().has_value());
absl::optional<base::UnguessableToken> fenced_frame_nonce =
fenced_frame->fenced_frame_nonce();
EXPECT_TRUE(fenced_frame_nonce.has_value());
EXPECT_EQ(fenced_frame_nonce.value(),
fenced_frame->current_frame_host()->storage_key().nonce().value());
// Add an iframe.
EXPECT_TRUE(ExecJs(root,
"var subframe = document.createElement('iframe');"
"document.body.appendChild(subframe);"));
EXPECT_EQ(2U, root->child_count());
auto* iframe = root->child_at(1);
EXPECT_FALSE(iframe->IsFencedFrameRoot());
EXPECT_FALSE(iframe->IsInFencedFrameTree());
EXPECT_FALSE(iframe->current_frame_host()->storage_key().nonce().has_value());
// Navigate the iframe. It should still not have a nonce.
EXPECT_TRUE(NavigateToURLFromRenderer(
iframe, https_server()->GetURL("a.test", "/title1.html")));
EXPECT_FALSE(iframe->current_frame_host()->storage_key().nonce().has_value());
// Set and read a value in the fenced frame's local storage.
EXPECT_TRUE(ExecJs(fenced_frame, "localStorage[\"foo\"] = \"a\""));
EXPECT_EQ("a", EvalJs(fenced_frame, "localStorage[\"foo\"]"));
// Set and read a value in the iframe's local storage.
EXPECT_TRUE(ExecJs(iframe, "localStorage[\"foo\"] = \"b\""));
EXPECT_EQ("b", EvalJs(iframe, "localStorage[\"foo\"]"));
// Set and read a value in the top-frame's local storage.
EXPECT_TRUE(ExecJs(root, "localStorage[\"foo\"] = \"c\""));
EXPECT_EQ("c", EvalJs(root, "localStorage[\"foo\"]"));
// This shouldn't impact the fenced frame's local storage:
EXPECT_EQ("a", EvalJs(fenced_frame, "localStorage[\"foo\"]"));
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
CheckFencedFrameNotNavigatedWithoutOptIn) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
GURL https_url(https_server()->GetURL("a.test", "/title1.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(https_url);
EXPECT_TRUE(urn_uuid.is_valid());
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, urn_uuid, navigate_urn_script,
net::ERR_BLOCKED_BY_RESPONSE);
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
CheckNestedIframeNotNavigatedWithoutOptIn) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
{
// Navigate the fenced frame.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
std::string navigate_script =
JsReplace("f.src = $1;", fenced_frame_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, fenced_frame_url, navigate_script);
}
// Add a nested iframe inside the fenced frame and navigate.
AddIframeInFencedFrame(fenced_frame_root_node, 0);
GURL iframe_url(https_server()->GetURL("a.test", "/title1.html"));
NavigateIframeInFencedFrame(fenced_frame_root_node->child_at(0), iframe_url,
net::ERR_BLOCKED_BY_RESPONSE);
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, CheckSecFetchDestHeader) {
GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"var fenced_frame = document.createElement('fencedframe');"
"document.body.appendChild(fenced_frame);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
{
// Navigate the fenced frame.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
std::string navigate_script =
JsReplace("fenced_frame.src = $1;", fenced_frame_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, fenced_frame_url, navigate_script);
EXPECT_TRUE(
CheckAndClearSecFetchDestHeader(fenced_frame_url, "fencedframe"));
}
// Add a nested iframe inside the fenced frame and navigate.
AddIframeInFencedFrame(fenced_frame_root_node, 0);
GURL iframe_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
NavigateIframeInFencedFrame(fenced_frame_root_node->child_at(0), iframe_url);
EXPECT_TRUE(CheckAndClearSecFetchDestHeader(iframe_url, "fencedframe"));
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, CheckOpaqueUrlFlag) {
GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Create a fenced frame.
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
// Navigate the fenced frame from the initial empty document toward a URL
// with a client side redirect.
//
// With MPArch, since this was a navigation toward an opaque URL, in the
// 'opaque-ads' mode, initiated from the embedder, the navigation must use
// and commit a document with
// `is_fenced_frame_root_originating_from_opaque_url` to true.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/redirect.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(fenced_frame_url);
EXPECT_TRUE(urn_uuid.is_valid());
std::string navigate_script = JsReplace("f.src = $1;", urn_uuid.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, fenced_frame_url, navigate_script);
EXPECT_EQ(
fenced_frame_root_node->current_frame_host()
->is_fenced_frame_root_originating_from_opaque_url(),
GetParam() == blink::features::FencedFramesImplementationType::kMPArch);
// Navigate the fenced frame again, but toward a non-opaque URL. Since this
// is initiated from the embedder, the new document must commit with
// `is_fenced_frame_root_originating_from_opaque_url` to false.
GURL second_url(
https_server()->GetURL("a.test", "/fenced_frames/title0.html"));
std::string second_navigate_script =
JsReplace("f.src = $1;", second_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, fenced_frame_url, second_navigate_script);
EXPECT_FALSE(fenced_frame_root_node->current_frame_host()
->is_fenced_frame_root_originating_from_opaque_url());
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
CancelledNavigationCheckOpaqueUrlFlag) {
GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Create a fenced frame.
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
// Navigate the fenced frame from the initial empty document toward an opaque
// URL. With MPArch, since this was in the 'opaque-ads' mode, initiated from
// the embedder, the navigation must use and commit a document with
// `is_fenced_frame_root_originating_from_opaque_url` to true.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(fenced_frame_url);
EXPECT_TRUE(urn_uuid.is_valid());
std::string navigate_script = JsReplace("f.src = $1;", urn_uuid.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, fenced_frame_url, navigate_script);
EXPECT_EQ(
fenced_frame_root_node->current_frame_host()
->is_fenced_frame_root_originating_from_opaque_url(),
GetParam() == blink::features::FencedFramesImplementationType::kMPArch);
// Navigate the fenced frame again, but toward a non-opaque URL and the
// navigation is cancelled. The navigation is not committed and therefore
// `is_fenced_frame_root_originating_from_opaque_url` of the document doesn't
// change.
GURL second_url(https_server()->GetURL("a.test", "/nocontent"));
std::string second_navigate_script =
JsReplace("f.src = $1;", second_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, fenced_frame_url, second_navigate_script,
net::ERR_ABORTED);
EXPECT_EQ(fenced_frame_root_node->current_frame_host()->GetLastCommittedURL(),
fenced_frame_url);
// The fenced frame's document initiates a navigation. The previous cancelled
// navigation from the embedder shouldn't have made any side effects. The next
// committed document must continue to have
// `is_fenced_frame_root_originating_from_opaque_url` true.
GURL redirect_url(
https_server()->GetURL("a.test", "/fenced_frames/title0.html"));
EXPECT_TRUE(ExecJs(fenced_frame_root_node->current_frame_host(),
JsReplace("location.href = $1;", redirect_url.spec())));
EXPECT_TRUE(content::WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(fenced_frame_root_node->current_frame_host()->GetLastCommittedURL(),
redirect_url);
EXPECT_EQ(
fenced_frame_root_node->current_frame_host()
->is_fenced_frame_root_originating_from_opaque_url(),
GetParam() == blink::features::FencedFramesImplementationType::kMPArch);
}
namespace {
class ScopedInsecureContentTestContentBrowserClient
: public TestContentBrowserClient {
public:
ScopedInsecureContentTestContentBrowserClient()
: old_client(SetBrowserClientForTesting(this)) {}
~ScopedInsecureContentTestContentBrowserClient() override {
SetBrowserClientForTesting(old_client);
}
void OverrideWebkitPrefs(WebContents* web_contents,
blink::web_pref::WebPreferences* prefs) override {
// Browser will both run and display insecure content.
prefs->allow_running_insecure_content = true;
}
private:
raw_ptr<ContentBrowserClient> old_client;
};
} // namespace
class FencedFrameIgnoreCertErrors : public FencedFrameTreeBrowserTest {
public:
FencedFrameIgnoreCertErrors()
: https_server_mismatched_(net::EmbeddedTestServer::TYPE_HTTPS) {}
protected:
void SetUpOnMainThread() override {
https_server_mismatched_.ServeFilesFromSourceDirectory(
GetTestDataFilePath());
https_server_mismatched_.SetSSLConfig(
net::EmbeddedTestServer::CERT_MISMATCHED_NAME);
ASSERT_TRUE(https_server_mismatched_.Start());
// We need to have a dedicated browser context for the tests.
// Or, SSLManager::UpdateEntry() doesn't update the entry if
// |ssl_host_state_delegate_| is nullptr.
browser_context_ = std::make_unique<TestBrowserContext>();
FencedFrameTreeBrowserTest::SetUpOnMainThread();
}
// Tests should call CreateWebContents() to use web_contents() in the test.
void CreateWebContents() {
ASSERT_FALSE(web_contents_.get());
web_contents_ =
WebContents::Create(WebContents::CreateParams(browser_context_.get()));
}
void SetUpCommandLine(base::CommandLine* command_line) override {
FencedFrameTreeBrowserTest::SetUpCommandLine(command_line);
// Browser will ignore certificate errors.
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void TearDownOnMainThread() override {
web_contents_.reset();
FencedFrameTreeBrowserTest::TearDownOnMainThread();
}
void TearDown() override {
GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE,
browser_context_.release());
FencedFrameTreeBrowserTest::TearDown();
}
net::EmbeddedTestServer* https_server_mismatched() {
return &https_server_mismatched_;
}
WebContents* web_contents() {
// web_contents_ should be initialized before calling this method.
EXPECT_TRUE(web_contents_.get());
return web_contents_.get();
}
private:
net::EmbeddedTestServer https_server_mismatched_;
std::unique_ptr<BrowserContext> browser_context_;
std::unique_ptr<WebContents> web_contents_;
};
IN_PROC_BROWSER_TEST_P(FencedFrameIgnoreCertErrors, FencedframeHasCertError) {
CreateWebContents();
// Allow insecure content.
ScopedInsecureContentTestContentBrowserClient scoped_content_browser_client;
GURL main_frame_url =
https_server_mismatched()->GetURL("a.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(web_contents(), main_frame_url));
EXPECT_FALSE(web_contents()
->GetController()
.GetLastCommittedEntry()
->GetSSL()
.content_status &
SSLStatus::RAN_CONTENT_WITH_CERT_ERRORS);
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(web_contents())
->GetPrimaryFrameTree()
.root();
// Create a fenced frame element.
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
// Navigate the fenced frame.
GURL fenced_frame_url(https_server_mismatched()->GetURL(
"b.test", "/fenced_frames/title1.html"));
TestFrameNavigationObserver observer(fenced_frame_root_node);
EXPECT_TRUE(ExecJs(root, JsReplace("f.src = $1;", fenced_frame_url.spec())));
observer.WaitForCommit();
EXPECT_EQ(
fenced_frame_url,
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
EXPECT_TRUE(web_contents()
->GetController()
.GetLastCommittedEntry()
->GetSSL()
.content_status &
SSLStatus::RAN_CONTENT_WITH_CERT_ERRORS);
}
namespace {
class TestJavaScriptDialogManager : public JavaScriptDialogManager,
public WebContentsDelegate {
public:
TestJavaScriptDialogManager() = default;
~TestJavaScriptDialogManager() override = default;
// WebContentsDelegate overrides
JavaScriptDialogManager* GetJavaScriptDialogManager(
WebContents* source) override {
return this;
}
// JavaScriptDialogManager overrides
void RunJavaScriptDialog(WebContents* web_contents,
RenderFrameHost* render_frame_host,
JavaScriptDialogType dialog_type,
const std::u16string& message_text,
const std::u16string& default_prompt_text,
DialogClosedCallback callback,
bool* did_suppress_message) override {}
void RunBeforeUnloadDialog(WebContents* web_contents,
RenderFrameHost* render_frame_host,
bool is_reload,
DialogClosedCallback callback) override {}
void CancelDialogs(WebContents* web_contents, bool reset_state) override {
cancel_dialogs_called_ = true;
}
bool cancel_dialogs_called() { return cancel_dialogs_called_; }
private:
bool cancel_dialogs_called_ = false;
};
} // namespace
// Test that navigation in fenced frame happens regardless of dialogs.
// It should also keep the dialogs as-is.
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, ShouldIgnoreJsDialog) {
GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"var fenced_frame = document.createElement('fencedframe');"
"document.body.appendChild(fenced_frame);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
{
// Navigate the fenced frame.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
std::string navigate_script =
JsReplace("fenced_frame.src = $1;", fenced_frame_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, fenced_frame_url, navigate_script);
EXPECT_TRUE(
CheckAndClearSecFetchDestHeader(fenced_frame_url, "fencedframe"));
}
// Setup test dialog manager and create dialog.
TestJavaScriptDialogManager dialog_manager;
web_contents()->SetDelegate(&dialog_manager);
web_contents()->RunJavaScriptDialog(web_contents()->GetPrimaryMainFrame(),
u"", u"", JAVASCRIPT_DIALOG_TYPE_ALERT,
false, base::NullCallback());
{
// Navigate fenced frame.
const GURL new_url =
https_server()->GetURL("a.test", "/fenced_frames/empty.html");
std::string navigate_script =
JsReplace("fenced_frame.src = $1;", new_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, new_url, navigate_script);
}
// We should not dismiss dialogs when the fenced frame's subframe navigates
// and swaps its RFH.
EXPECT_FALSE(dialog_manager.cancel_dialogs_called());
// Clean up test dialog manager.
web_contents()->SetDelegate(nullptr);
web_contents()->SetJavaScriptDialogManagerForTesting(nullptr);
}
// An observer class that asserts the page transition always is
// `ui::PageTransition::PAGE_TRANSITION_AUTO_SUBFRAME`.
class AlwaysAutoSubframeNavigationObserver : public WebContentsObserver {
public:
explicit AlwaysAutoSubframeNavigationObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
void DidFinishNavigation(NavigationHandle* navigation_handle) override {
EXPECT_TRUE(ui::PageTransitionCoreTypeIs(
navigation_handle->GetPageTransition(),
ui::PageTransition::PAGE_TRANSITION_AUTO_SUBFRAME));
}
};
// Tests that any navigation or history API calls always replace the current
// entry and do not increase the back/forward entries.
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
NavigationAndHistoryShouldBeReplaceOnly) {
GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Add the fenced frame element.
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
auto* fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
// Instantiate a navigation observer to assert from here on the navigations
// are always `ui::PageTransition::PAGE_TRANSITION_AUTO_SUBFRAME`.
AlwaysAutoSubframeNavigationObserver auto_subframe_observer(
shell()->web_contents());
// ShadowDOM fenced frames have the same NavigationController as the top-level
// frame, therefore the count here is 1 because of the navigation of the
// top-level frame. MPArch fenced frame has its own NavigationController.
if (GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM) {
EXPECT_EQ(root->navigator().controller().GetEntryCount(),
fenced_frame->navigator().controller().GetEntryCount());
} else if (blink::features::IsInitialNavigationEntryEnabled()) {
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
} else {
EXPECT_EQ(0, fenced_frame->navigator().controller().GetEntryCount());
}
// 1. Navigate the fenced frame: both cross-document and fragment navigation.
GURL fenced_frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
std::string navigate_script =
JsReplace("f.src = $1;", fenced_frame_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, fenced_frame_url, navigate_script);
GURL fragment_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html#123"));
navigate_script = JsReplace("f.src = $1;", fragment_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, fragment_url, navigate_script);
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
// Do a cross-site navigation to exercise RemoteFrame::Navigate path in the
// navigation after this one.
GURL cross_site_url =
https_server()->GetURL("d.test", "/fenced_frames/title1.html");
std::string navigate_script_2 =
JsReplace("f.src = $1;", cross_site_url.spec());
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, cross_site_url, navigate_script_2);
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, fenced_frame_url, navigate_script);
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
// 2. Do a pushState in the fenced frame which would've normally added a new
// history entry. The entry count should stay at 1. Also test a replaceState,
// reload and location.replace.
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, "window.history.pushState({}, null);");
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, "window.history.replaceState({}, null);");
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame, "window.location.reload()");
GURL replace_url(
https_server()->GetURL("c.test", "/fenced_frames/title1.html"));
std::string script = JsReplace("location.replace($1);", replace_url);
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(fenced_frame,
script);
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
// 3. Add an iframe to the fenced frame and navigate it. The entry count
// should stay at 1.
AddIframeInFencedFrame(fenced_frame, 0 /* child_index */);
NavigateIframeInFencedFrame(
fenced_frame->child_at(0),
https_server()->GetURL("b.test", "/fenced_frames/title1.html"));
EXPECT_EQ(
1, fenced_frame->child_at(0)->navigator().controller().GetEntryCount());
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
// 4. Do history changes from the iframe. The entry count should
// stay at 1.
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame->child_at(0), "window.history.pushState({}, null);");
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame->child_at(0), "window.history.replaceState({}, null);");
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame->child_at(0), "window.location.reload()");
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame->child_at(0), script);
EXPECT_EQ(
1, fenced_frame->child_at(0)->navigator().controller().GetEntryCount());
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
// 5. Add a nested fenced frame and navigate it. The entry count should stay
// at 1.
FrameTreeNode* nested_fenced_frame =
AddNestedFencedFrame(fenced_frame, 1 /* child_index */);
NavigateNestedFencedFrame(
nested_fenced_frame,
https_server()->GetURL("b.test", "/fenced_frames/title1.html"));
EXPECT_EQ(1, nested_fenced_frame->navigator().controller().GetEntryCount());
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
// 6. Do history changes from the nested fenced frame. The entry
// count should stay at 1.
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
nested_fenced_frame, "window.history.pushState({}, null);");
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
nested_fenced_frame, "window.history.replaceState({}, null);");
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
nested_fenced_frame, "window.location.reload()");
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(
nested_fenced_frame, script);
EXPECT_EQ(1, nested_fenced_frame->navigator().controller().GetEntryCount());
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
}
// Tests successfully going back to a page with a fenced frame.
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
GoBackToPageWithFencedFrame) {
GURL main_url(https_server()->GetURL(
"a.test", "/fenced_frames/basic_fenced_frame_src.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(1U, root->child_count());
auto* fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
GURL fenced_frame_url_1 =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
// ShadowDOM fenced frames have the same NavigationController as the top-level
// frame, therefore the count here is 1 because of the navigation of the
// top-level frame. MPArch fenced frame has its own NavigationController and
// the count is 1 due to the fenced frame's navigation based on `src`
// attribute.
if (GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM) {
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
EXPECT_EQ(root->navigator().controller().GetEntryCount(),
fenced_frame->navigator().controller().GetEntryCount());
} else {
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
}
EXPECT_EQ(fenced_frame_url_1,
fenced_frame->current_frame_host()->GetLastCommittedURL());
// Navigate the fenced frame. It should do a replace navigation and therefore
// the `controller().GetEntryCount()` stays at 1.
GURL fenced_frame_url_2(
https_server()->GetURL("c.test", "/fenced_frames/title1.html"));
std::string script = JsReplace("location.assign($1);", fenced_frame_url_2);
UpdateHistoryOrReloadFromFencedFrameTreeAndWaitForFinishedLoad(fenced_frame,
script);
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
EXPECT_EQ(fenced_frame_url_2,
fenced_frame->current_frame_host()->GetLastCommittedURL());
// Navigate the top-level page to another document.
GURL new_main_url(https_server()->GetURL("b.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), new_main_url));
EXPECT_EQ(2, root->navigator().controller().GetEntryCount());
EXPECT_EQ(new_main_url, root->current_frame_host()->GetLastCommittedURL());
// Go back.
TestNavigationObserver back_load_observer(shell()->web_contents());
root->navigator().controller().GoBack();
back_load_observer.Wait();
EXPECT_EQ(2, root->navigator().controller().GetEntryCount());
EXPECT_EQ(1U, root->child_count());
fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
// ShadowDOM fenced frames have the same NavigationController as the top-level
// frame, therefore the count here is 2 because of the navigation of the
// top-level frame.
// Note the last committed url is the latest one in shadowDOM due to the joint
// history maintained in the single navigation controller and going back can
// therefore get the latest navigation in the frame which is
// `fenced_frame_url_2`. However, when back/forward cache is disabled,
// it will navigate to `fenced_frame_url_1`.
// MPArch fenced frame has its own NavigationController which is not retained
// when the top-level page navigates. Therefore going back lands on the
// initial navigation in the Fenced Frame.
if (GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM) {
EXPECT_EQ(2, root->navigator().controller().GetEntryCount());
EXPECT_EQ(root->navigator().controller().GetEntryCount(),
fenced_frame->navigator().controller().GetEntryCount());
if (content::BackForwardCache::IsBackForwardCacheFeatureEnabled())
EXPECT_EQ(fenced_frame_url_2,
fenced_frame->current_frame_host()->GetLastCommittedURL());
else
EXPECT_EQ(fenced_frame_url_1,
fenced_frame->current_frame_host()->GetLastCommittedURL());
} else {
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
EXPECT_EQ(fenced_frame_url_1,
fenced_frame->current_frame_host()->GetLastCommittedURL());
}
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, ReloadPageWithFencedFrame) {
GURL main_url(
https_server()->GetURL("a.test", "/fenced_frames/opaque_ads.html"));
GURL fenced_frame_url =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
TestNavigationObserver reload_observer(web_contents());
EXPECT_TRUE(ExecJs(root, "window.location.reload();"));
reload_observer.Wait();
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
auto* fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_EQ(fenced_frame_url, fenced_frame->current_url());
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
NavigateUnfencedTopAndGoBack) {
GURL main_url(
https_server()->GetURL("a.test", "/fenced_frames/opaque_ads.html"));
GURL fenced_frame_url =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
TestNavigationObserver load_observer(web_contents());
EXPECT_TRUE(NavigateToURL(shell(), main_url));
load_observer.Wait();
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
auto* fenced_frame = GetFencedFrameRootNode(root->child_at(0));
GURL new_main_url(https_server()->GetURL("b.test", "/hello.html"));
// Now let's try to use unfencedTop and come back to the page with the fenced
// frame.
TestFrameNavigationObserver observer(root);
EXPECT_TRUE(ExecJs(fenced_frame, JsReplace("window.open($1, '_unfencedTop');",
new_main_url)));
observer.Wait();
EXPECT_EQ(2, root->navigator().controller().GetEntryCount());
EXPECT_EQ(new_main_url, root->current_frame_host()->GetLastCommittedURL());
// Go back.
{
TestNavigationObserver back_load_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, "history.back();"));
back_load_observer.Wait();
}
EXPECT_EQ(2, root->navigator().controller().GetEntryCount());
EXPECT_EQ(main_url, root->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(1U, root->child_count());
fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
EXPECT_EQ(fenced_frame_url, fenced_frame->current_url());
}
// Simulates the crash in crbug.com/1317642 by disabling BFCache and going back
// to a page with a fenced frame navigation, which in shadowDOM FFs will lead to
// a AUTO_SUBFRAME navigation initiated in the browser.
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
GoBackToPageWithFencedFrameNavigationNoBFCache) {
GURL main_url(https_server()->GetURL(
"a.test",
"/fenced_frames/basic_fenced_frame_src_navigate_on_click.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(1U, root->child_count());
auto* fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
// Since the fenced frame is not yet navigated, it's specific controller
// should have no entries, or should be on the initial NavigationEntry.
if (GetParam() == blink::features::FencedFramesImplementationType::kMPArch) {
EXPECT_TRUE(
!fenced_frame->navigator().controller().GetLastCommittedEntry() ||
fenced_frame->navigator()
.controller()
.GetLastCommittedEntry()
->IsInitialEntry());
}
TestFrameNavigationObserver observer(fenced_frame);
EXPECT_TRUE(
ExecJs(root, "document.getElementsByTagName('button')[0].click();"));
observer.WaitForCommit();
GURL fenced_frame_url_1 =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
// ShadowDOM fenced frames have the same NavigationController as the top-level
// frame, therefore checking `root->navigator().controller()` while MPArch
// fenced frame has its own NavigationController so checking
// `fenced_frame->navigator().controller()`.
if (GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM) {
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
EXPECT_EQ(root->navigator().controller().GetEntryCount(),
fenced_frame->navigator().controller().GetEntryCount());
} else {
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
}
EXPECT_EQ(fenced_frame_url_1,
fenced_frame->current_frame_host()->GetLastCommittedURL());
DisableBackForwardCacheForTesting(shell()->web_contents(),
BackForwardCache::TEST_REQUIRES_NO_CACHING);
// Navigate the top-level page to another document.
GURL new_main_url(https_server()->GetURL("b.test", "/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), new_main_url));
EXPECT_EQ(2, root->navigator().controller().GetEntryCount());
EXPECT_EQ(new_main_url, root->current_frame_host()->GetLastCommittedURL());
// Go back.
TestNavigationObserver back_load_observer(shell()->web_contents());
root->navigator().controller().GoBack();
back_load_observer.Wait();
EXPECT_EQ(2, root->navigator().controller().GetEntryCount());
EXPECT_EQ(1U, root->child_count());
fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
// ShadowDOM fenced frames have the same NavigationController as the top-level
// frame, therefore the count here is 2 because of the navigation of the
// top-level frame.
// Note the last committed url is the latest one in shadowDOM due to the joint
// history maintained in the single navigation controller and going back can
// therefore get the latest navigation in the frame which is
// `fenced_frame_url_1`.
// MPArch fenced frame has its own NavigationController which is not retained
// when the top-level page navigates. Therefore going back lands on the
// initial fenced frame without any navigation.
if (GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM) {
EXPECT_EQ(2, root->navigator().controller().GetEntryCount());
EXPECT_EQ(root->navigator().controller().GetEntryCount(),
fenced_frame->navigator().controller().GetEntryCount());
EXPECT_EQ(fenced_frame_url_1,
fenced_frame->current_frame_host()->GetLastCommittedURL());
} else {
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_TRUE(
!fenced_frame->navigator().controller().GetLastCommittedEntry() ||
fenced_frame->navigator()
.controller()
.GetLastCommittedEntry()
->IsInitialEntry());
}
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
RestorePageWithFencedFrameNavigation) {
GURL main_url(https_server()->GetURL(
"a.test",
"/fenced_frames/basic_fenced_frame_src_navigate_on_click.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(1U, root->child_count());
auto* fenced_frame = GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame->IsInFencedFrameTree());
TestFrameNavigationObserver observer(fenced_frame);
EXPECT_TRUE(
ExecJs(root, "document.getElementsByTagName('button')[0].click();"));
observer.WaitForCommit();
GURL fenced_frame_url_1 =
https_server()->GetURL("a.test", "/fenced_frames/title1.html");
// ShadowDOM fenced frames have the same NavigationController as the top-level
// frame, therefore checking `root->navigator().controller()` while MPArch
// fenced frame has its own NavigationController so checking
// `fenced_frame->navigator().controller()`.
if (GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM) {
EXPECT_EQ(1, root->navigator().controller().GetEntryCount());
EXPECT_EQ(root->navigator().controller().GetEntryCount(),
fenced_frame->navigator().controller().GetEntryCount());
} else {
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount());
}
EXPECT_EQ(fenced_frame_url_1,
fenced_frame->current_frame_host()->GetLastCommittedURL());
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
std::unique_ptr<NavigationEntryImpl> restored_entry =
NavigationEntryImpl::FromNavigationEntry(
NavigationController::CreateNavigationEntry(
main_url, Referrer(), absl::nullopt, ui::PAGE_TRANSITION_RELOAD,
false, std::string(), controller.GetBrowserContext(),
nullptr /* blob_url_loader_factory */));
std::unique_ptr<NavigationEntryRestoreContextImpl> context =
std::make_unique<NavigationEntryRestoreContextImpl>();
restored_entry->SetPageState(blink::PageState::CreateFromURL(main_url),
context.get());
EXPECT_EQ(0U, restored_entry->root_node()->children.size());
// Restore the new entry in a new tab and verify the fenced frame loads.
std::vector<std::unique_ptr<NavigationEntry>> entries;
entries.push_back(std::move(restored_entry));
Shell* new_shell = Shell::CreateNewWindow(
controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
FrameTreeNode* new_root =
static_cast<WebContentsImpl*>(new_shell->web_contents())
->GetPrimaryFrameTree()
.root();
NavigationControllerImpl& new_controller =
static_cast<NavigationControllerImpl&>(
new_shell->web_contents()->GetController());
new_controller.Restore(entries.size() - 1, RestoreType::kRestored, &entries);
ASSERT_EQ(0u, entries.size());
{
TestNavigationObserver restore_observer(new_shell->web_contents());
new_controller.LoadIfNecessary();
restore_observer.Wait();
}
ASSERT_EQ(1U, new_root->child_count());
EXPECT_EQ(main_url, new_root->current_url());
auto* restored_fenced_frame = GetFencedFrameRootNode(new_root->child_at(0));
EXPECT_TRUE(restored_fenced_frame->IsFencedFrameRoot());
EXPECT_TRUE(restored_fenced_frame->IsInFencedFrameTree());
EXPECT_EQ(1, new_controller.GetEntryCount());
EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
NavigationEntryImpl* new_entry = controller.GetLastCommittedEntry();
EXPECT_EQ(main_url, new_entry->root_node()->frame_entry->url());
if (GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM) {
// ShadowDOM FF gets restored as it is part of the restored NavigationEntry
// from the primary FrameTree's NavigationController
ASSERT_EQ(1U, new_entry->root_node()->children.size());
EXPECT_EQ(fenced_frame_url_1,
new_entry->root_node()->children[0]->frame_entry->url());
} else {
// MPArch navigation controller wouldn't have any entry since it's not
// restored. Therefore we will only have the initial fenced frame without
// any navigation.
ASSERT_EQ(0U, new_entry->root_node()->children.size());
EXPECT_TRUE(!restored_fenced_frame->navigator()
.controller()
.GetLastCommittedEntry() ||
restored_fenced_frame->navigator()
.controller()
.GetLastCommittedEntry()
->IsInitialEntry());
}
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, CheckInvalidUrnError) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
GURL urn_uuid = GURL("urn:uuid:12345678-9abc-def0-1234-56789abcdef0");
EXPECT_TRUE(urn_uuid.is_valid());
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, urn_uuid, navigate_urn_script, InvalidUrnError());
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest,
CheckCSPFencedFrameSrcOpaqueURL) {
const struct {
const char* csp;
bool expect_allowed;
} kTestCases[]{
{"fenced-frame-src 'none'", false},
{"fenced-frame-src 'self'", false},
{"fenced-frame-src *", true},
{"fenced-frame-src data:", false},
{"fenced-frame-src https:", true},
{"fenced-frame-src https://*:*", true},
{"fenced-frame-src https://*", false},
{"fenced-frame-src https://b.test:*", false},
};
for (const auto& test_case : kTestCases) {
GURL main_url = https_server()->GetURL("a.test", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root, JsReplace(R"(
var violation = new Promise(resolve => {
document.addEventListener("securitypolicyviolation", (e) => {
resolve(e.violatedDirective + ";" + e.blockedURI);
});
});
var meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = $1;
document.head.appendChild(meta);
)",
test_case.csp)));
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
GURL https_url(
https_server()->GetURL("b.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(https_url);
EXPECT_TRUE(urn_uuid.is_valid());
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
net::Error expected_net_error_code =
test_case.expect_allowed ? net::OK : net::ERR_BLOCKED_BY_CSP;
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, urn_uuid, navigate_urn_script,
expected_net_error_code);
if (!test_case.expect_allowed)
EXPECT_EQ("fenced-frame-src;", EvalJs(root, "violation"));
absl::optional<blink::mojom::FencedFrameMode> fenced_frame_mode =
fenced_frame_root_node->GetFencedFrameMode();
EXPECT_TRUE(fenced_frame_mode.has_value());
EXPECT_EQ(fenced_frame_mode.value(),
blink::mojom::FencedFrameMode::kOpaqueAds);
}
}
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, FenceUserActivation) {
// This test exercises browser-side user activation in the following layout:
// A: Top-level page (origin 1)
// B: fencedframe (origin 1)
// C1: iframe (origin 1)
// D: fencedframe (origin 1)
// E1: iframe (origin 1)
// E2: iframe (origin 2)
// C2: iframe (origin 2)
// F: fencedframe (origin 1)
// G: iframe (origin 1)
//
// See the design document for more details on intended semantics:
// https://docs.google.com/document/d/1WnIhXOFycoje_sEoZR3Mo0YNSR2Ki7LABIC_HEWFaog/
// Chrome disallows navigation to a URL in a frame that has more than one
// ancestor with that URL, so I have to circumvent it with query params.
const GURL kOrigin1Url =
https_server()->GetURL("a.test", "/fenced_frames/empty.html");
const GURL kOrigin1Url2 =
https_server()->GetURL("a.test", "/fenced_frames/empty.html?");
const GURL kOrigin1Url3 =
https_server()->GetURL("a.test", "/fenced_frames/empty.html??");
const GURL kOrigin2Url =
https_server()->GetURL("b.test", "/fenced_frames/empty.html");
// Navigate the top-level page.
EXPECT_TRUE(NavigateToURL(shell(), kOrigin1Url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
auto* nodeA = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
ASSERT_NE(nullptr, nodeA);
// Construct the children described above.
auto* nodeB = AddNestedFencedFrame(nodeA, 0);
ASSERT_NE(nullptr, nodeB);
NavigateNestedFencedFrame(nodeB, kOrigin1Url);
auto* nodeC1 = AddIframeInFencedFrame(nodeB, 0);
ASSERT_NE(nullptr, nodeC1);
NavigateIframeInFencedFrame(nodeC1, kOrigin1Url2);
auto* nodeD = AddNestedFencedFrame(nodeC1, 0);
ASSERT_NE(nullptr, nodeD);
NavigateNestedFencedFrame(nodeD, kOrigin1Url2);
auto* nodeE1 = AddIframeInFencedFrame(nodeD, 0);
ASSERT_NE(nullptr, nodeE1);
NavigateIframeInFencedFrame(nodeE1, kOrigin1Url3);
auto* nodeE2 = AddIframeInFencedFrame(nodeD, 1);
ASSERT_NE(nullptr, nodeE2);
NavigateIframeInFencedFrame(nodeE2, kOrigin2Url);
auto* nodeC2 = AddIframeInFencedFrame(nodeB, 1);
ASSERT_NE(nullptr, nodeC2);
NavigateIframeInFencedFrame(nodeC2, kOrigin2Url);
auto* nodeF = AddNestedFencedFrame(nodeA, 1);
ASSERT_NE(nullptr, nodeF);
NavigateNestedFencedFrame(nodeF, kOrigin1Url);
auto* nodeG = AddIframeInFencedFrame(nodeF, 0);
ASSERT_NE(nullptr, nodeG);
NavigateIframeInFencedFrame(nodeG, kOrigin1Url2);
// Now that the layout is set up, perform the actual user activation tests.
std::vector<FrameTreeNode*> nodes = {nodeA, nodeB, nodeC1, nodeD, nodeE1,
nodeE2, nodeC2, nodeF, nodeG};
// Create some helper functions so we can express the user activation
// notification test cases more concisely.
auto ClearAll = [&nodes]() {
// User activation can only be cleared per frame tree in MPArch, so we'll
// do it from every node just to be safe.
for (auto* node : nodes) {
node->current_frame_host()->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kClearActivation,
blink::mojom::UserActivationNotificationType::kNone);
}
for (auto* node : nodes) {
EXPECT_FALSE(node->HasStickyUserActivation());
EXPECT_FALSE(node->HasTransientUserActivation());
}
};
auto Activate = [](FrameTreeNode* node) {
node->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
};
auto EXPECT_STICKY = [&nodes](std::vector<bool> should_be_activated) {
ASSERT_EQ(nodes.size(), should_be_activated.size());
for (size_t i = 0; i < nodes.size(); ++i) {
if (should_be_activated[i]) {
EXPECT_TRUE(nodes[i]->HasStickyUserActivation());
EXPECT_TRUE(nodes[i]->HasTransientUserActivation());
} else {
EXPECT_FALSE(nodes[i]->HasStickyUserActivation());
EXPECT_FALSE(nodes[i]->HasTransientUserActivation());
}
}
};
// Activate A, and check that no other frames are activated.
ClearAll(); // Clear all user activations before we start.
Activate(nodeA);
EXPECT_STICKY({true /*A*/, false /*B*/, false /*C1*/, false /*D*/,
false /*E1*/, false /*E2*/, false /*C2*/, false /*F*/,
false /*G*/});
// Activate B, and check that only B and C1 are activated.
ClearAll();
Activate(nodeB);
EXPECT_STICKY({false /*A*/, true /*B*/, true /*C1*/, false /*D*/,
false /*E1*/, false /*E2*/, false /*C2*/, false /*F*/,
false /*G*/});
// Activate C1, and check that only B and C1 are activated.
ClearAll();
Activate(nodeC1);
EXPECT_STICKY({false /*A*/, true /*B*/, true /*C1*/, false /*D*/,
false /*E1*/, false /*E2*/, false /*C2*/, false /*F*/,
false /*G*/});
// Activate C2, and check that only B and C2 are activated.
ClearAll();
Activate(nodeC2);
EXPECT_STICKY({false /*A*/, true /*B*/, false /*C1*/, false /*D*/,
false /*E1*/, false /*E2*/, true /*C2*/, false /*F*/,
false /*G*/});
// Activate D, and check that only D and E1 are activated.
ClearAll();
Activate(nodeD);
EXPECT_STICKY({false /*A*/, false /*B*/, false /*C1*/, true /*D*/,
true /*E1*/, false /*E2*/, false /*C2*/, false /*F*/,
false /*G*/});
// Activate E1, and check that only D and E1 are activated.
ClearAll();
Activate(nodeE1);
EXPECT_STICKY({false /*A*/, false /*B*/, false /*C1*/, true /*D*/,
true /*E1*/, false /*E2*/, false /*C2*/, false /*F*/,
false /*G*/});
// Activate E2, and check that only D and E2 are activated.
ClearAll();
Activate(nodeE2);
EXPECT_STICKY({false /*A*/, false /*B*/, false /*C1*/, true /*D*/,
false /*E1*/, true /*E2*/, false /*C2*/, false /*F*/,
false /*G*/});
// Activating F and G is equivalent to activating B and C1, so we omit them.
// Create some helper functions so we can express the user activation
// consumption test cases more concisely.
auto ActivateAll = [&nodes]() {
// Activate every individual frame just to be safe.
for (auto* node : nodes) {
node->current_frame_host()->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
}
for (auto* node : nodes) {
EXPECT_TRUE(node->HasStickyUserActivation());
EXPECT_TRUE(node->HasTransientUserActivation());
}
};
auto Consume = [](FrameTreeNode* node) {
node->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kConsumeTransientActivation,
blink::mojom::UserActivationNotificationType::kTest);
};
auto EXPECT_TRANSIENT = [&nodes](std::vector<bool> should_be_activated) {
ASSERT_EQ(nodes.size(), should_be_activated.size());
for (size_t i = 0; i < nodes.size(); ++i) {
EXPECT_TRUE(nodes[i]->HasStickyUserActivation());
if (should_be_activated[i]) {
EXPECT_TRUE(nodes[i]->HasTransientUserActivation());
} else {
EXPECT_FALSE(nodes[i]->HasTransientUserActivation());
}
}
};
// These tests are the opposites of the ones above.
// Consume A, and check that no other frames are consumed.
ActivateAll(); // Activate all frames before we start.
Consume(nodeA);
EXPECT_TRANSIENT({false /*A*/, true /*B*/, true /*C1*/, true /*D*/,
true /*E1*/, true /*E2*/, true /*C2*/, true /*F*/,
true /*G*/});
// Consume B, and check that only B, C1, and C2 are consumed.
ActivateAll();
Consume(nodeB);
EXPECT_TRANSIENT({true /*A*/, false /*B*/, false /*C1*/, true /*D*/,
true /*E1*/, true /*E2*/, false /*C2*/, true /*F*/,
true /*G*/});
// Consume C2, and check that only B, C1, and C2 are consumed.
ActivateAll();
Consume(nodeC2);
EXPECT_TRANSIENT({true /*A*/, false /*B*/, false /*C1*/, true /*D*/,
true /*E1*/, true /*E2*/, false /*C2*/, true /*F*/,
true /*G*/});
// Consume D, and check that only D, E1, and E2 are consumed.
ActivateAll();
Consume(nodeD);
EXPECT_TRANSIENT({true /*A*/, true /*B*/, true /*C1*/, false /*D*/,
false /*E1*/, false /*E2*/, true /*C2*/, true /*F*/,
true /*G*/});
// Consume E1, and check that only D, E1, and E2 are consumed.
ActivateAll();
Consume(nodeE1);
EXPECT_TRANSIENT({true /*A*/, true /*B*/, true /*C1*/, false /*D*/,
false /*E1*/, false /*E2*/, true /*C2*/, true /*F*/,
true /*G*/});
}
// TODO(https://crbug.com/1335512): Flaky.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_FencedAdSizes DISABLED_FencedAdSizes
#else
#define MAYBE_FencedAdSizes FencedAdSizes
#endif
IN_PROC_BROWSER_TEST_P(FencedFrameTreeBrowserTest, MAYBE_FencedAdSizes) {
// This test exercises restrictions on fenced frame sizes in opaque-ads mode.
// See the design document for more details on intended semantics:
// https://docs.google.com/document/d/1MVqxc2nzde3cJYIRC8vnXH-a4A6J4GQE-1vBuXhQsPE/edit#
enum class TestType {
kFixed,
kScaleWidthConstantHeightExact,
kScaleWidthConstantHeightApproximate,
kScaleWidthConstantAspectRatioExact,
kScaleWidthConstantAspectRatioApproximate,
};
// Test that an opaque-ads mode fenced frame created with size
// `input_width` by `input_height` gets snapped to size
// `output_width` by `output_height` on desktop.
auto TestAdSize = [&](int input_width, int input_height, TestType test_type,
int output_width, int output_height) {
// Navigate the top-level page.
const GURL kUrl =
https_server()->GetURL("a.test", "/fenced_frames/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), kUrl));
// It is safe to obtain the root frame tree node here, as it doesn't change.
auto* nodeA = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
ASSERT_NE(nullptr, nodeA);
if (test_type != TestType::kFixed) {
#if !BUILDFLAG(IS_ANDROID)
// Ignore mobile-only tests on platforms other than Android.
return;
#else
// Set up tests that scale with screen width.
int screen_width = EvalJs(nodeA, "screen.width").ExtractInt();
// Scale the height to match the aspect ratio, if relevant.
if (test_type == TestType::kScaleWidthConstantAspectRatioExact ||
test_type == TestType::kScaleWidthConstantAspectRatioApproximate) {
output_height = (input_height * screen_width) / input_width;
input_height = output_height;
}
// Make the width match the screen width.
input_width = screen_width;
output_width = screen_width;
// If we want to test coercion to sizes that scale with constant height,
// make the requested width a little wrong.
if (test_type == TestType::kScaleWidthConstantHeightApproximate ||
test_type == TestType::kScaleWidthConstantAspectRatioApproximate) {
input_width++;
}
#endif
}
// Create an opaque-ads fenced frame nodeB with size
// `input_width` by `input_height`.
EXPECT_TRUE(ExecJs(
nodeA,
JsReplace(
"var nested_fenced_frame = document.createElement('fencedframe');"
"nested_fenced_frame.mode = 'opaque-ads';"
"nested_fenced_frame.width = $1;"
"nested_fenced_frame.height = $2;"
"document.body.appendChild(nested_fenced_frame);",
input_width, input_height)));
EXPECT_EQ(1UL, nodeA->child_count());
auto* nodeB = GetFencedFrameRootNode(nodeA->child_at(0));
EXPECT_TRUE(nodeB->IsFencedFrameRoot());
EXPECT_TRUE(nodeB->IsInFencedFrameTree());
ASSERT_NE(nullptr, nodeB);
// Check the size of the frame before navigating.
auto frame_width =
EvalJs(nodeA, "getComputedStyle(nested_fenced_frame).width")
.ExtractString();
auto frame_height =
EvalJs(nodeA, "getComputedStyle(nested_fenced_frame).height")
.ExtractString();
// Wait for 2 rAFs to make things deterministic.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(nodeA, "", "").error.empty());
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(nodeA, "", "").error.empty());
// Navigate the fenced frame, which should force its inner size to the
// nearest allowed one.
NavigateNestedFencedFrame(nodeB, kUrl);
// Check that the outer container size hasn't changed.
EXPECT_EQ(EvalJs(nodeA, "getComputedStyle(nested_fenced_frame).width")
.ExtractString(),
frame_width);
EXPECT_EQ(EvalJs(nodeA, "getComputedStyle(nested_fenced_frame).height")
.ExtractString(),
frame_height);
// Wait for 2 rAFs to make things deterministic.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(nodeA, "", "").error.empty());
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(nodeA, "", "").error.empty());
// Check that the inner size is what we expect.
// TODO(kojii|gtanzer): There is a known bug with the size 0,0,
// where the fenced frame can be resized once.
int inner_width = EvalJs(nodeB, "innerWidth").ExtractInt();
int inner_height = EvalJs(nodeB, "innerHeight").ExtractInt();
if (input_width == 0 && input_height == 0) {
output_width = 0;
output_height = 0;
}
EXPECT_EQ(inner_width, output_width);
EXPECT_EQ(inner_height, output_height);
// Attempt to change the size of the fenced frame from the embedder.
const int new_width = 970;
const int new_height = 90;
EXPECT_TRUE(ExecJs(nodeA, JsReplace("nested_fenced_frame.width = $1;"
"nested_fenced_frame.height = $2;",
new_width, new_height)));
NavigateNestedFencedFrame(nodeB, kUrl);
// Force a style recomputation.
ASSERT_TRUE(EvalJs(nodeA, "getComputedStyle(nested_fenced_frame).width")
.error.empty());
// Wait for 2 rAFs to make things deterministic.
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(nodeA, "", "").error.empty());
ASSERT_TRUE(EvalJsAfterLifecycleUpdate(nodeA, "", "").error.empty());
// Check that the inner size hasn't changed.
// TODO(kojii|gtanzer): There is still a known bug with the size 0,0.
inner_width = EvalJs(nodeB, "innerWidth").ExtractInt();
inner_height = EvalJs(nodeB, "innerHeight").ExtractInt();
if (input_width == 0 && input_height == 0) {
output_width = new_width;
output_height = new_height;
}
EXPECT_EQ(inner_width, output_width);
EXPECT_EQ(inner_height, output_height);
};
// Run all the individual test cases we want.
// {input_width, input_height, test_type, output_width, output_height}
std::vector<std::tuple<int, int, TestType, int, int>> test_cases = {
// Exact match between requested size and fixed allowed size.
{320, 50, TestType::kFixed, 320, 50},
{728, 90, TestType::kFixed, 728, 90},
{970, 90, TestType::kFixed, 970, 90},
{320, 100, TestType::kFixed, 320, 100},
{160, 600, TestType::kFixed, 160, 600},
{300, 250, TestType::kFixed, 300, 250},
{970, 250, TestType::kFixed, 970, 250},
{336, 280, TestType::kFixed, 336, 280},
{320, 480, TestType::kFixed, 320, 480},
{300, 600, TestType::kFixed, 300, 600},
{300, 1050, TestType::kFixed, 300, 1050},
// Approximate match between requested size and fixed allowed size.
{320, 49, TestType::kFixed, 320, 50},
{319, 50, TestType::kFixed, 320, 50},
// Edge cases for requested size.
{0, 0, TestType::kFixed, 320, 50},
{0, 100, TestType::kFixed, 320, 50},
{100, 0, TestType::kFixed, 320, 50},
// Exact match between requested size and allowed size that scales with
// constant height.
{0, 50, TestType::kScaleWidthConstantHeightExact, 0, 50},
{0, 100, TestType::kScaleWidthConstantHeightExact, 0, 100},
{0, 250, TestType::kScaleWidthConstantHeightExact, 0, 250},
// Approximate match between requested size and allowed size that scales
// with constant height.
{0, 50, TestType::kScaleWidthConstantHeightApproximate, 0, 50},
{0, 100, TestType::kScaleWidthConstantHeightApproximate, 0, 100},
{0, 250, TestType::kScaleWidthConstantHeightApproximate, 0, 250},
// Constant height scaling is only supported on sizes where it is
// declared (e.g. not for height 99).
{0, 99, TestType::kScaleWidthConstantHeightExact, 0, 100},
// Exact match between requested size and allowed size that scales with
// constant aspect ratio.
{32, 5, TestType::kScaleWidthConstantAspectRatioExact, 0, 0},
{16, 5, TestType::kScaleWidthConstantAspectRatioExact, 0, 0},
{6, 5, TestType::kScaleWidthConstantAspectRatioExact, 0, 0},
{2, 3, TestType::kScaleWidthConstantAspectRatioExact, 0, 0},
{1, 2, TestType::kScaleWidthConstantAspectRatioExact, 0, 0},
// Approximate match between requested size and allowed size that scales
// with constant aspect ratio.
{32, 5, TestType::kScaleWidthConstantAspectRatioApproximate, 0, 0},
{16, 5, TestType::kScaleWidthConstantAspectRatioApproximate, 0, 0},
{6, 5, TestType::kScaleWidthConstantAspectRatioApproximate, 0, 0},
{2, 3, TestType::kScaleWidthConstantAspectRatioApproximate, 0, 0},
{1, 2, TestType::kScaleWidthConstantAspectRatioApproximate, 0, 0},
};
for (auto& test_case : test_cases) {
TestAdSize(std::get<0>(test_case), std::get<1>(test_case),
std::get<2>(test_case), std::get<3>(test_case),
std::get<4>(test_case));
}
}
INSTANTIATE_TEST_SUITE_P(
All,
FencedFrameTreeBrowserTest,
::testing::Values(
blink::features::FencedFramesImplementationType::kShadowDOM,
blink::features::FencedFramesImplementationType::kMPArch),
&FencedFrameTreeBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
All,
FencedFrameIgnoreCertErrors,
::testing::Values(
blink::features::FencedFramesImplementationType::kShadowDOM,
blink::features::FencedFramesImplementationType::kMPArch),
&FencedFrameTreeBrowserTest::DescribeParams);
class FencedFrameReportEventBrowserTest : public FencedFrameTreeBrowserTest {
public:
// TODO(crbug.com/1123606): Disable window.fence.reportEvent in iframes.
// Remove this constructor and `scoped_feature_list_` once FLEDGE stops
// supporting iframes.
FencedFrameReportEventBrowserTest() {
scoped_feature_list_.InitWithFeaturesAndParameters(
{{blink::features::kAllowURNsInIframes, {}}},
{/* disabled_features */});
}
void SetUpOnMainThread() override {
// Set up the host resolver to allow serving separate sites, so we can
// perform cross-process navigation.
host_resolver()->AddRule("*", "127.0.0.1");
// Fenced frames require potentially trustworthy URLs so creating an https
// server.
https_server()->RegisterRequestMonitor(
base::BindRepeating(&FencedFrameTreeBrowserTest::ObserveRequestHeaders,
base::Unretained(this)));
https_server()->ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server()->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
}
// An object representing a single step of a reportEvent test.
// First, we navigate the fenced frame to a new URL.
// Second, we call reportEvent and validate the results.
struct Step {
// Whether the navigation should be embedder-initiated or fenced-frame
// initiated.
bool is_embedder_initiated = false;
// Whether the navigation should be via a urn:uuid or a normal URL.
// (This should always be false when `!is_embedder_initiated`.
bool is_opaque = false;
struct Target {
// The origin for the navigation.
std::string origin;
// The path for the resource to load.
std::string path;
};
// The initial navigation target (may be redirected).
Target target;
// A list of redirects that the navigation should take. The last redirect
// target will be the ultimate destination of the navigation.
std::vector<Target> redirects;
// Whether the reportEvent should succeed.
bool should_have_metadata = false;
};
// A helper function for specifying reportEvent tests. Each step consists of a
// series of `Step`s specified above.
void RunTest(std::vector<Step>& steps) {
// In order to check events reported over the network, we register an HTTP
// response interceptor for each successful reportEvent request we expect.
// We register an additional one so that we can check for spurious requests
// at the end of the test.
EXPECT_TRUE(steps.size() > 0);
std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>
responses;
std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>
redirects;
for (size_t i = 0; i < steps.size() + 1; ++i) {
responses.emplace_back(
std::make_unique<net::test_server::ControllableHttpResponse>(
https_server(), "/_report_event_server.html"));
}
// We also register interceptors for redirections that we want to perform.
// Each redirect must be from a unique path so that messages aren't
// unintentionally intercepted and blocked.
{
std::set<std::string> paths;
for (auto& step : steps) {
ASSERT_FALSE(step.target.origin.empty());
ASSERT_FALSE(step.target.path.empty());
int redirect_index = 0;
for (auto& redirect_target : step.redirects) {
ASSERT_TRUE(paths.find(redirect_target.path) == paths.end());
ASSERT_FALSE(redirect_target.origin.empty());
ASSERT_FALSE(redirect_target.path.empty());
paths.insert(redirect_target.path);
// Intercept the previous navigation target in the chain.
std::string previous_path =
redirect_index ? step.redirects[redirect_index - 1].path
: step.target.path;
redirects.emplace_back(
std::make_unique<net::test_server::ControllableHttpResponse>(
https_server(), previous_path));
redirect_index++;
}
}
}
ASSERT_TRUE(https_server()->Start());
// Set up the embedder and a fenced frame.
GURL main_url = https_server()->GetURL("a.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
// Create reporting metadata.
ReportingMetadata fenced_frame_reporting;
GURL reporting_url(
https_server()->GetURL("c.test", "/_report_event_server.html"));
fenced_frame_reporting
.metadata[blink::mojom::ReportingDestination::kBuyer]["click"] =
reporting_url;
// Get the urn mapping object.
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
int navigation_index = 0;
int response_index = 0;
int redirect_index = 0;
for (auto& step : steps) {
// Configure the navigation.
GURL navigate_url =
https_server()->GetURL(step.target.origin, step.target.path);
GURL expect_url = navigate_url;
if (step.is_opaque) {
GURL urn_uuid =
url_mapping.AddFencedFrameURL(navigate_url, fenced_frame_reporting);
EXPECT_TRUE(urn_uuid.is_valid());
navigate_url = urn_uuid;
}
// Initiate the navigation.
TestFrameNavigationObserver observer(fenced_frame_root_node);
if (step.is_embedder_initiated) {
EXPECT_TRUE(ExecJs(root, JsReplace("f.src = $1", navigate_url)));
} else {
EXPECT_TRUE(ExecJs(fenced_frame_root_node,
JsReplace("location.href = $1", navigate_url)));
}
// Redirect the navigation if relevant.
for (auto& redirect_target : step.redirects) {
GURL redirect_url = https_server()->GetURL(redirect_target.origin,
redirect_target.path);
expect_url = redirect_url;
auto& redirect = *redirects[redirect_index];
redirect.WaitForRequest();
std::string redirect_response =
std::string("HTTP/1.1 302 Moved Temporarily\r\nLocation: ") +
redirect_url.spec() + std::string("\r\n\r\n");
redirect.Send(redirect_response);
redirect.Done();
redirect_index++;
}
// Check that the navigation worked as intended.
observer.WaitForCommit();
EXPECT_EQ(
expect_url,
fenced_frame_root_node->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(expect_url),
fenced_frame_root_node->current_frame_host()
->GetLastCommittedOrigin());
navigation_index++;
// Perform the reportEvent call, with a unique body.
const char report_event_script[] = R"(
window.fence.reportEvent({
eventType: 'click',
eventData: 'click $1',
destination: ['buyer'],
});
)";
EXPECT_TRUE(ExecJs(fenced_frame_root_node,
JsReplace(report_event_script, navigation_index)));
// If relevant, check that the event report succeeded.
if (step.should_have_metadata) {
auto& response = *responses[response_index];
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content,
JsReplace("click $1", navigation_index));
response.Done();
response_index++;
}
}
// Check for any spurious waiting reported events.
EXPECT_TRUE(ExecJs(root, JsReplace("f.src = $1", reporting_url)));
auto& response = *responses[response_index];
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, "");
response.Done();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// The simplest test case: URN navigation into reportEvent.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
FencedFrameReportEventEmbedderURNNavigation) {
std::vector<Step> config = {
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/title1.html"},
.should_have_metadata = true,
},
};
RunTest(config);
}
// Reporting metadata should persist across FF-initiated same-origin
// navigations.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
FencedFrameReportEventFFSameOriginNavigation) {
std::vector<Step> config = {
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/title1.html"},
.should_have_metadata = true,
},
{
.target = {"a.test", "/fenced_frames/title1.html?foo"},
.should_have_metadata = true,
},
};
RunTest(config);
}
// Reporting metadata should be dropped upon cross-origin navigations,
// but come back upon new URN navigations.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
FencedFrameReportEventFFCrossOriginNavigation) {
std::vector<Step> config = {
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/title1.html"},
.should_have_metadata = true,
},
{
.target = {"b.test", "/fenced_frames/title1.html"},
.should_have_metadata = false,
},
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/title1.html"},
.should_have_metadata = true,
},
};
RunTest(config);
}
// Embedder-initiated URL navigations should always be considered cross-origin.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
FencedFrameReportEventEmbedderURLNavigation) {
// In ShadowDOM, embedder-initiated navigations aren't given a unique
// initiator origin, so we have to disable this test.
// (Same-origin embedder-initiated navigations will be considered
// same-origin, and therefore the reporting metadata will remain.)
if (GetParam() ==
blink::features::FencedFramesImplementationType::kShadowDOM) {
return;
}
std::vector<Step> config = {
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/title1.html"},
.should_have_metadata = true,
},
{
.is_embedder_initiated = true,
.is_opaque = false,
.target = {"a.test", "/fenced_frames/title1.html"},
.should_have_metadata = false,
},
};
RunTest(config);
}
// Same-origin redirects in the initial URN navigation shouldn't affect
// reporting metadata.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
FencedFrameReportEventEmbedderSameOriginRedirect) {
std::vector<Step> config = {
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/redirect1.html"},
.redirects =
{
{"a.test", "/fenced_frames/redirect2.html"},
{"a.test", "/fenced_frames/title1.html"},
},
.should_have_metadata = true,
},
};
RunTest(config);
}
// Cross-origin redirects in the initial URN navigation shouldn't affect
// reporting metadata either.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
FencedFrameReportEventEmbedderCrossOriginRedirect) {
std::vector<Step> config = {
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/redirect1.html"},
.redirects =
{
{"b.test", "/fenced_frames/redirect2.html"},
{"c.test", "/fenced_frames/title1.html"},
},
.should_have_metadata = true,
},
};
RunTest(config);
}
// Metadata should be preserved if all URLs in an FF-initiated redirect chain
// are same-origin.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
FencedFrameReportEventFFSameOriginRedirect) {
std::vector<Step> config = {
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/title1.html"},
.should_have_metadata = true,
},
{
.target = {"a.test", "/fenced_frames/redirect1.html"},
.redirects =
{
{"a.test", "/fenced_frames/redirect2.html"},
{"a.test", "/fenced_frames/title1.html?foo"},
},
.should_have_metadata = true,
},
};
RunTest(config);
}
// Metadata should be dropped if any URLs in an FF-initiated redirect chain
// are cross-origin.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
FencedFrameReportEventFFCrossOriginRedirect) {
std::vector<Step> config = {
{
.is_embedder_initiated = true,
.is_opaque = true,
.target = {"a.test", "/fenced_frames/title1.html"},
.should_have_metadata = true,
},
{
.target = {"a.test", "/fenced_frames/redirect1.html"},
.redirects =
{
{"b.test", "/fenced_frames/redirect2.html"},
{"a.test", "/fenced_frames/title1.html"},
},
.should_have_metadata = false,
},
};
RunTest(config);
}
// (Temporary test for FLEDGE iframe OT.)
// Tests that an iframe with a urn:uuid commits the navigation with the
// associated reporting metadata and `fence.reportEvent` sends the beacon to
// the registered reporting url.
// TODO(crbug.com/1123606): Disable window.fence.reportEvent in iframes.
// Remove this test once the FLEDGE origin trial stops supporting iframes.
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
IframeReportingMetadata) {
net::test_server::ControllableHttpResponse response(https_server(),
"/title2.html");
ASSERT_TRUE(https_server()->Start());
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('iframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* iframe_node = root->child_at(0);
// Add reporting metadata.
ReportingMetadata fenced_frame_reporting;
GURL reporting_url(https_server()->GetURL("c.test", "/title2.html"));
fenced_frame_reporting.metadata[blink::mojom::ReportingDestination::kBuyer]
["mouse interaction"] = reporting_url;
fenced_frame_reporting
.metadata[blink::mojom::ReportingDestination::kBuyer]["click"] =
https_server()->GetURL("c.test", "/title1.html");
GURL https_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid =
url_mapping.AddFencedFrameURL(https_url, fenced_frame_reporting);
EXPECT_TRUE(urn_uuid.is_valid());
TestFencedFrameURLMappingResultObserver mapping_observer;
url_mapping.ConvertFencedFrameURNToURL(urn_uuid, &mapping_observer);
TestFrameNavigationObserver observer(iframe_node);
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, JsReplace("f.src = $1;", urn_uuid)));
observer.WaitForCommit();
EXPECT_TRUE(mapping_observer.mapping_complete_observed());
EXPECT_EQ(reporting_url,
mapping_observer.reporting_metadata()
.metadata[blink::mojom::ReportingDestination::kBuyer]
["mouse interaction"]);
EXPECT_EQ(https_url,
iframe_node->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(https_url),
iframe_node->current_frame_host()->GetLastCommittedOrigin());
std::string event_data = "this is a click";
EXPECT_TRUE(ExecJs(iframe_node, JsReplace("window.fence.reportEvent({"
" eventType: 'mouse interaction',"
" eventData: $1,"
" destination: ['buyer']});",
event_data)));
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, event_data);
}
IN_PROC_BROWSER_TEST_P(FencedFrameReportEventBrowserTest,
NestedIframeReportEvent) {
net::test_server::ControllableHttpResponse response(https_server(),
"/title2.html");
ASSERT_TRUE(https_server()->Start());
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('fencedframe');"
"f.mode = 'opaque-ads';"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* fenced_frame_root_node =
GetFencedFrameRootNode(root->child_at(0));
EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot());
EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree());
// Add reporting metadata.
ReportingMetadata fenced_frame_reporting;
GURL reporting_url(https_server()->GetURL("c.test", "/title2.html"));
fenced_frame_reporting.metadata[blink::mojom::ReportingDestination::kBuyer]
["mouse interaction"] = reporting_url;
GURL https_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid =
url_mapping.AddFencedFrameURL(https_url, fenced_frame_reporting);
EXPECT_TRUE(urn_uuid.is_valid());
// Navigate the fenced frame.
std::string navigate_urn_script = JsReplace("f.src = $1;", urn_uuid);
NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
fenced_frame_root_node, urn_uuid, navigate_urn_script);
// Add a nested iframe inside the fenced frame and navigate.
AddIframeInFencedFrame(fenced_frame_root_node, 0);
EXPECT_EQ(1U, fenced_frame_root_node->child_count());
FrameTreeNode* nested_iframe_node = fenced_frame_root_node->child_at(0);
GURL iframe_url(
https_server()->GetURL("a.test", "/fenced_frames/title0.html"));
NavigateIframeInFencedFrame(nested_iframe_node, iframe_url);
std::string event_data = "this is a click";
EXPECT_TRUE(
ExecJs(nested_iframe_node, JsReplace("window.fence.reportEvent({"
" eventType: 'mouse interaction',"
" eventData: $1,"
" destination: ['buyer']});",
event_data)));
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, event_data);
}
INSTANTIATE_TEST_SUITE_P(
All,
FencedFrameReportEventBrowserTest,
::testing::Values(
blink::features::FencedFramesImplementationType::kShadowDOM,
blink::features::FencedFramesImplementationType::kMPArch),
&FencedFrameTreeBrowserTest::DescribeParams);
// Parameterized on whether the feature is enabled or not.
class UUIDFrameTreeBrowserTest : public FrameTreeBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
UUIDFrameTreeBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
if (GetParam()) {
scoped_feature_list_.InitAndEnableFeature(
blink::features::kAllowURNsInIframes);
} else {
scoped_feature_list_.InitAndDisableFeature(
blink::features::kAllowURNsInIframes);
}
}
void SetUpOnMainThread() override {
// Set up the host resolver to allow serving separate sites, so we can
// perform cross-process navigation.
host_resolver()->AddRule("*", "127.0.0.1");
https_server_.ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
ASSERT_TRUE(https_server_.Start());
}
net::EmbeddedTestServer* https_server() { return &https_server_; }
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
bool NavigateIframeAndCheckURL(WebContents* web_contents,
const std::string& iframe_id,
const GURL& url,
const GURL& expected_commit_url) {
TestNavigationObserver nav_observer(web_contents);
if (!BeginNavigateIframeToURL(web_contents, iframe_id, url))
return false;
nav_observer.Wait();
EXPECT_EQ(expected_commit_url, nav_observer.last_navigation_url());
return nav_observer.last_navigation_succeeded();
}
static std::string DescribeParams(
const ::testing::TestParamInfo<ParamType>& info) {
return info.param ? "AllowURNsInIframes" : "DoNotAllowURNsInIframes";
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
net::EmbeddedTestServer https_server_;
};
IN_PROC_BROWSER_TEST_P(UUIDFrameTreeBrowserTest,
CheckIframeNavigationWithUUID) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
GURL initial_frame_url = https_server()->GetURL("a.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('iframe');"
"f.id = \"test_iframe\";"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
// Initially navigate the iframe to somewhere specific.
EXPECT_TRUE(NavigateIframeAndCheckURL(web_contents(), "test_iframe",
initial_frame_url, initial_frame_url));
GURL frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(frame_url);
EXPECT_TRUE(urn_uuid.is_valid());
if (GetParam()) {
// If the feature is enabled, we should navigate to the mapped page.
EXPECT_TRUE(NavigateIframeAndCheckURL(web_contents(), "test_iframe",
urn_uuid, frame_url));
} else {
// If the feature is disabled, navigation should fail.
EXPECT_FALSE(NavigateIframeAndCheckURL(web_contents(), "test_iframe",
urn_uuid, GURL()));
}
// Parent will still see the src as the urn_uuid and not the mapped url.
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, "f.src"));
// The parent will be able to access window.frames[0] as iframes are
// visible via frames[].
EXPECT_EQ(1, EvalJs(root, "window.frames.length"));
}
IN_PROC_BROWSER_TEST_P(UUIDFrameTreeBrowserTest,
CheckIframeNavigationWithInvalidUUID) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
GURL initial_frame_url = https_server()->GetURL("a.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
{
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('iframe');"
"f.id = \"test_iframe\";"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1U, root->child_count());
// Initially navigate the iframe to somewhere specific.
EXPECT_TRUE(NavigateIframeAndCheckURL(web_contents(), "test_iframe",
initial_frame_url, initial_frame_url));
GURL urn_uuid("urn:uuid:c36973b5-e5d9-de59-e4c4-364f137b3c7a");
// We expect iframe navigations to invalid URNs to fail, regardless of if the
// feature is enabled.
EXPECT_FALSE(NavigateIframeAndCheckURL(web_contents(), "test_iframe",
urn_uuid, GURL()));
// Parent still sees the src as the urn_uuid.
EXPECT_EQ(urn_uuid.spec(), EvalJs(root, "f.src"));
// The parent will be able to access window.frames[0] as iframes are
// visible via frames[].
EXPECT_EQ(1, EvalJs(root, "window.frames.length"));
}
IN_PROC_BROWSER_TEST_P(UUIDFrameTreeBrowserTest,
CheckMainFrameNavigationWithUUIDFails) {
GURL main_url = https_server()->GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
GURL frame_url(
https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
FencedFrameURLMapping& url_mapping =
root->current_frame_host()->GetPage().fenced_frame_urls_map();
GURL urn_uuid = url_mapping.AddFencedFrameURL(frame_url);
EXPECT_TRUE(urn_uuid.is_valid());
// Top page navigation to a URN should fail regardless of if the feature is
// enabled.
EXPECT_FALSE(NavigateToURL(shell(), urn_uuid));
}
INSTANTIATE_TEST_SUITE_P(All,
UUIDFrameTreeBrowserTest,
::testing::Values(true, false),
&UUIDFrameTreeBrowserTest::DescribeParams);
class CrossProcessFrameTreeBrowserTest : public ContentBrowserTest {
public:
CrossProcessFrameTreeBrowserTest() = default;
CrossProcessFrameTreeBrowserTest(const CrossProcessFrameTreeBrowserTest&) =
delete;
CrossProcessFrameTreeBrowserTest& operator=(
const CrossProcessFrameTreeBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
};
// Ensure that we can complete a cross-process subframe navigation.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
CreateCrossProcessSubframeProxies) {
GURL main_url(embedded_test_server()->GetURL("/site_per_process_main.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// There should not be a proxy for the root's own SiteInstance.
SiteInstanceImpl* root_instance =
root->current_frame_host()->GetSiteInstance();
EXPECT_FALSE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(root_instance->group()));
// Load same-site page into iframe.
GURL http_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), http_url));
// Load cross-site page into iframe.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
// Ensure that we have created a new process for the subframe.
ASSERT_EQ(2U, root->child_count());
FrameTreeNode* child = root->child_at(0);
SiteInstanceImpl* child_instance =
child->current_frame_host()->GetSiteInstance();
RenderViewHost* rvh = child->current_frame_host()->render_view_host();
RenderProcessHost* rph = child->current_frame_host()->GetProcess();
EXPECT_NE(shell()->web_contents()->GetPrimaryMainFrame()->GetRenderViewHost(),
rvh);
EXPECT_NE(shell()->web_contents()->GetSiteInstance(), child_instance);
EXPECT_NE(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), rph);
// Ensure that the root node has a proxy for the child node's SiteInstance.
EXPECT_TRUE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(child_instance->group()));
// Also ensure that the child has a proxy for the root node's SiteInstance.
EXPECT_TRUE(child->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(root_instance->group()));
// The nodes should not have proxies for their own SiteInstance.
EXPECT_FALSE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(root_instance->group()));
EXPECT_FALSE(child->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(child_instance->group()));
// Ensure that the RenderViews and RenderFrames are all live.
EXPECT_TRUE(
root->current_frame_host()->render_view_host()->IsRenderViewLive());
EXPECT_TRUE(
child->current_frame_host()->render_view_host()->IsRenderViewLive());
EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
}
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
OriginSetOnNavigations) {
GURL main_url(embedded_test_server()->GetURL("/site_per_process_main.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(root->current_origin().Serialize() + '/',
main_url.DeprecatedGetOriginAsURL().spec());
// First frame is an about:blank frame. Check that its origin is correctly
// inherited from the parent.
EXPECT_EQ(root->child_at(0)->current_origin().Serialize() + '/',
main_url.DeprecatedGetOriginAsURL().spec());
// Second frame loads a same-site page. Its origin should also be the same
// as the parent.
EXPECT_EQ(root->child_at(1)->current_origin().Serialize() + '/',
main_url.DeprecatedGetOriginAsURL().spec());
// Load cross-site page into the first frame.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
EXPECT_EQ(root->child_at(0)->current_origin().Serialize() + '/',
cross_site_url.DeprecatedGetOriginAsURL().spec());
// The root's origin shouldn't have changed.
EXPECT_EQ(root->current_origin().Serialize() + '/',
main_url.DeprecatedGetOriginAsURL().spec());
{
GURL data_url("data:text/html,foo");
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(
ExecJs(root->child_at(1), JsReplace("window.location = $1", data_url)));
observer.Wait();
}
// Navigating to a data URL should set a unique origin. This is represented
// as "null" per RFC 6454. A frame navigating itself to a data: URL does not
// require a process transfer, but should retain the original origin
// as its precursor.
EXPECT_EQ(root->child_at(1)->current_origin().Serialize(), "null");
EXPECT_TRUE(root->child_at(1)->current_origin().opaque());
ASSERT_EQ(
url::SchemeHostPort(main_url),
root->child_at(1)->current_origin().GetTupleOrPrecursorTupleIfOpaque())
<< "Expected the precursor origin to be preserved; should be the "
"initiator of a data: navigation.";
// Adding an <iframe sandbox srcdoc=> frame should result in a unique origin
// that is different-origin from its data: URL parent.
{
TestNavigationObserver observer(shell()->web_contents());
ASSERT_EQ(0U, root->child_at(1)->child_count());
EXPECT_TRUE(
ExecJs(root->child_at(1), JsReplace(
R"(
var iframe = document.createElement('iframe');
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.srcdoc = $1;
document.body.appendChild(iframe);
)",
"<html><body>This sandboxed doc should "
"be different-origin.</body></html>")));
observer.Wait();
ASSERT_EQ(1U, root->child_at(1)->child_count());
}
url::Origin root_origin = root->current_origin();
url::Origin child_1 = root->child_at(1)->current_origin();
url::Origin child_1_0 = root->child_at(1)->child_at(0)->current_origin();
EXPECT_FALSE(root_origin.opaque());
EXPECT_TRUE(child_1.opaque());
EXPECT_TRUE(child_1_0.opaque());
EXPECT_NE(child_1, child_1_0);
EXPECT_EQ(url::SchemeHostPort(main_url),
root_origin.GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(url::SchemeHostPort(main_url),
child_1.GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(url::SchemeHostPort(main_url),
child_1_0.GetTupleOrPrecursorTupleIfOpaque());
{
TestNavigationObserver observer(shell()->web_contents());
ASSERT_EQ(1U, root->child_at(1)->child_count());
EXPECT_TRUE(
ExecJs(root->child_at(1), JsReplace(
R"(
var iframe = document.createElement('iframe');
iframe.srcdoc = $1;
document.body.appendChild(iframe);
)",
"<html><body>This srcdoc document should "
"be same-origin.</body></html>")));
observer.Wait();
ASSERT_EQ(2U, root->child_at(1)->child_count());
}
EXPECT_EQ(root_origin, root->current_origin());
EXPECT_EQ(child_1, root->child_at(1)->current_origin());
EXPECT_EQ(child_1_0, root->child_at(1)->child_at(0)->current_origin());
url::Origin child_1_1 = root->child_at(1)->child_at(1)->current_origin();
EXPECT_EQ(child_1, child_1_1);
EXPECT_NE(child_1_0, child_1_1);
{
TestNavigationObserver observer(shell()->web_contents());
ASSERT_EQ(2U, root->child_at(1)->child_count());
EXPECT_TRUE(
ExecJs(root->child_at(1), JsReplace(
R"(
var iframe = document.createElement('iframe');
iframe.src = 'data:text/html;base64,' + btoa($1);
document.body.appendChild(iframe);
)",
"<html><body>This data: doc should be "
"different-origin.</body></html>")));
observer.Wait();
ASSERT_EQ(3U, root->child_at(1)->child_count());
}
EXPECT_EQ(root_origin, root->current_origin());
EXPECT_EQ(child_1, root->child_at(1)->current_origin());
EXPECT_EQ(child_1_0, root->child_at(1)->child_at(0)->current_origin());
EXPECT_EQ(child_1_1, root->child_at(1)->child_at(1)->current_origin());
url::Origin child_1_2 = root->child_at(1)->child_at(2)->current_origin();
EXPECT_NE(child_1, child_1_2);
EXPECT_NE(child_1_0, child_1_2);
EXPECT_NE(child_1_1, child_1_2);
EXPECT_EQ(url::SchemeHostPort(main_url),
child_1_2.GetTupleOrPrecursorTupleIfOpaque());
// If the parent navigates its child to a data URL, it should transfer
// to the parent's process, and the precursor origin should track the
// parent's origin.
{
GURL data_url("data:text/html,foo2");
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, JsReplace("frames[0].location = $1", data_url)));
observer.Wait();
EXPECT_EQ(data_url, root->child_at(0)->current_url());
}
EXPECT_EQ(root->child_at(0)->current_origin().Serialize(), "null");
EXPECT_TRUE(root->child_at(0)->current_origin().opaque());
EXPECT_EQ(
url::SchemeHostPort(main_url),
root->child_at(0)->current_origin().GetTupleOrPrecursorTupleIfOpaque());
EXPECT_EQ(root->current_frame_host()->GetProcess(),
root->child_at(0)->current_frame_host()->GetProcess());
}
// Test to verify that a blob: URL that is created by a unique opaque origin
// will correctly set the origin_to_commit on a session history navigation.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
OriginForBlobUrlsFromUniqueOpaqueOrigin) {
// Start off with a navigation to data: URL in the main frame. It should
// result in a unique opaque origin without any precursor information.
GURL data_url("data:text/html,foo<iframe id='child' src='" +
embedded_test_server()->GetURL("/title1.html").spec() +
"'></iframe>");
EXPECT_TRUE(NavigateToURL(shell(), data_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_TRUE(root->current_origin().opaque());
EXPECT_FALSE(
root->current_origin().GetTupleOrPrecursorTupleIfOpaque().IsValid());
EXPECT_EQ(1UL, root->child_count());
FrameTreeNode* child = root->child_at(0);
// Create a blob: URL and navigate the child frame to it.
std::string html = "<html><body>This is blob content.</body></html>";
std::string script = JsReplace(
"var blob = new Blob([$1], {type: 'text/html'});"
"var blob_url = URL.createObjectURL(blob);"
"document.getElementById('child').src = blob_url;"
"blob_url;",
html);
GURL blob_url;
{
TestFrameNavigationObserver observer(child);
blob_url = GURL(EvalJs(root, script).ExtractString());
observer.Wait();
EXPECT_EQ(blob_url, child->current_frame_host()->GetLastCommittedURL());
}
// We expect the frame to have committed in an opaque origin which contains
// the same precursor information - none.
url::Origin blob_origin = child->current_origin();
EXPECT_TRUE(blob_origin.opaque());
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque(),
blob_origin.GetTupleOrPrecursorTupleIfOpaque());
// Navigate the frame away to any web URL.
{
GURL url(embedded_test_server()->GetURL("/title2.html"));
TestFrameNavigationObserver observer(child);
EXPECT_TRUE(ExecJs(child, JsReplace("window.location = $1", url)));
observer.Wait();
EXPECT_EQ(url, child->current_frame_host()->GetLastCommittedURL());
}
EXPECT_FALSE(child->current_origin().opaque());
EXPECT_TRUE(shell()->web_contents()->GetController().CanGoBack());
EXPECT_EQ(3, shell()->web_contents()->GetController().GetEntryCount());
EXPECT_EQ(
2, shell()->web_contents()->GetController().GetLastCommittedEntryIndex());
// Verify the blob URL still exists in the main frame, which keeps it alive
// allowing a session history navigation back to succeed.
EXPECT_EQ(blob_url, GURL(EvalJs(root, "blob_url;").ExtractString()));
// Now navigate back in session history. It should successfully go back to
// the blob: URL.
{
TestFrameNavigationObserver observer(child);
shell()->web_contents()->GetController().GoBack();
observer.Wait();
}
EXPECT_EQ(blob_url, child->current_frame_host()->GetLastCommittedURL());
EXPECT_TRUE(child->current_origin().opaque());
EXPECT_EQ(blob_origin, child->current_origin());
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque(),
child->current_origin().GetTupleOrPrecursorTupleIfOpaque());
}
// Test to verify that about:blank iframe, which is a child of a sandboxed
// iframe is not considered same origin, but precursor information is preserved
// in its origin.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
AboutBlankSubframeInSandboxedFrame) {
// Start off by navigating to a page with sandboxed iframe, which allows
// script execution.
GURL main_url(
embedded_test_server()->GetURL("/sandboxed_main_frame_script.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(1UL, root->child_count());
FrameTreeNode* child = root->child_at(0);
// Navigate the frame to data: URL to cause it to have an opaque origin that
// is derived from the |main_url| origin.
GURL data_url("data:text/html,<html><body>foo</body></html>");
{
TestFrameNavigationObserver observer(child);
EXPECT_TRUE(ExecJs(root, JsReplace("frames[0].location = $1", data_url)));
observer.Wait();
EXPECT_EQ(data_url, child->current_frame_host()->GetLastCommittedURL());
}
// Add an about:blank iframe to the data: frame, which should not inherit the
// origin, but should preserve the precursor information.
{
EXPECT_TRUE(ExecJs(child,
"var f = document.createElement('iframe');"
"document.body.appendChild(f);"));
}
EXPECT_EQ(1UL, child->child_count());
FrameTreeNode* grandchild = child->child_at(0);
EXPECT_TRUE(grandchild->current_origin().opaque());
EXPECT_EQ(GURL(url::kAboutBlankURL),
grandchild->current_frame_host()->GetLastCommittedURL());
// The origin of the data: document should have precursor information matching
// the main frame origin.
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque(),
child->current_origin().GetTupleOrPrecursorTupleIfOpaque());
// The same should hold also for the about:blank subframe of the data: frame.
EXPECT_EQ(root->current_origin().GetTupleOrPrecursorTupleIfOpaque(),
grandchild->current_origin().GetTupleOrPrecursorTupleIfOpaque());
// The about:blank document should not be able to access its parent, as they
// are considered cross origin due to the sandbox flags on the parent.
EXPECT_FALSE(ExecJs(grandchild, "window.parent.foo = 'bar';"));
EXPECT_NE(child->current_origin(), grandchild->current_origin());
}
// Ensure that a popup opened from a sandboxed main frame inherits sandbox flags
// from its opener.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
SandboxFlagsSetForNewWindow) {
GURL main_url(
embedded_test_server()->GetURL("/sandboxed_main_frame_script.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Open a new window from the main frame.
GURL popup_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
Shell* new_shell = OpenPopup(root->current_frame_host(), popup_url, "");
EXPECT_TRUE(new_shell);
WebContents* new_contents = new_shell->web_contents();
// Check that the new window's sandbox flags correctly reflect the opener's
// flags. Main frame sets allow-popups, allow-pointer-lock and allow-scripts.
FrameTreeNode* popup_root =
static_cast<WebContentsImpl*>(new_contents)->GetPrimaryFrameTree().root();
network::mojom::WebSandboxFlags main_frame_sandbox_flags =
root->current_frame_host()->active_sandbox_flags();
EXPECT_EQ(
network::mojom::WebSandboxFlags::kAll &
~network::mojom::WebSandboxFlags::kAutomaticFeatures &
~network::mojom::WebSandboxFlags::kPointerLock &
~network::mojom::WebSandboxFlags::kPopups &
~network::mojom::WebSandboxFlags::kScripts &
~network::mojom::WebSandboxFlags::kTopNavigationToCustomProtocols,
main_frame_sandbox_flags);
EXPECT_EQ(main_frame_sandbox_flags,
popup_root->effective_frame_policy().sandbox_flags);
EXPECT_EQ(main_frame_sandbox_flags, popup_root->active_sandbox_flags());
EXPECT_EQ(main_frame_sandbox_flags,
popup_root->current_frame_host()->active_sandbox_flags());
}
// Tests that the user activation bits get cleared when a cross-site document is
// installed in the frame.
IN_PROC_BROWSER_TEST_F(CrossProcessFrameTreeBrowserTest,
ClearUserActivationForNewDocument) {
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Set the user activation bits.
root->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
EXPECT_TRUE(root->HasStickyUserActivation());
EXPECT_TRUE(root->HasTransientUserActivation());
// Install a new cross-site document to check the clearing of user activation
// bits.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), cross_site_url));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
}
// FrameTreeBrowserTest variant where we isolate http://*.is, Iceland's top
// level domain. This is an analogue to isolating extensions, which we can use
// inside content_browsertests, where extensions don't exist. Iceland, like an
// extension process, is a special place with magical powers; we want to protect
// it from outsiders.
class IsolateIcelandFrameTreeBrowserTest : public ContentBrowserTest {
public:
IsolateIcelandFrameTreeBrowserTest() = default;
IsolateIcelandFrameTreeBrowserTest(
const IsolateIcelandFrameTreeBrowserTest&) = delete;
IsolateIcelandFrameTreeBrowserTest& operator=(
const IsolateIcelandFrameTreeBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
// Blink suppresses navigations to blob URLs of origins different from the
// frame initiating the navigation. We disable those checks for this test,
// to test what happens in a compromise scenario.
command_line->AppendSwitch(switches::kDisableWebSecurity);
// ProcessSwitchForIsolatedBlob test below requires that one of URLs used in
// the test (blob:http://b.is/) belongs to an isolated origin.
command_line->AppendSwitchASCII(switches::kIsolateOrigins, "http://b.is/");
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
};
// Regression test for https://crbug.com/644966
IN_PROC_BROWSER_TEST_F(IsolateIcelandFrameTreeBrowserTest,
ProcessSwitchForIsolatedBlob) {
// Set up an iframe.
WebContents* contents = shell()->web_contents();
FrameTreeNode* root =
static_cast<WebContentsImpl*>(contents)->GetPrimaryFrameTree().root();
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// The navigation targets an invalid blob url; that's intentional to trigger
// an error response. The response should commit in a process dedicated to
// http://b.is or error pages, depending on policy.
EXPECT_EQ(
"done",
EvalJs(
root,
"new Promise((resolve) => {"
" var iframe_element = document.getElementsByTagName('iframe')[0];"
" iframe_element.onload = () => resolve('done');"
" iframe_element.src = 'blob:http://b.is/';"
"});"));
EXPECT_TRUE(WaitForLoadStop(contents));
// Make sure we did a process transfer back to "b.is".
const std::string kExpectedSiteURL =
AreDefaultSiteInstancesEnabled()
? SiteInstanceImpl::GetDefaultSiteURL().spec()
: "http://a.com/";
const std::string kExpectedSubframeSiteURL =
SiteIsolationPolicy::IsErrorPageIsolationEnabled(/*in_main_frame*/ false)
? "chrome-error://chromewebdata/"
: "http://b.is/";
EXPECT_EQ(base::StringPrintf(" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
"Where A = %s\n"
" B = %s",
kExpectedSiteURL.c_str(),
kExpectedSubframeSiteURL.c_str()),
DepictFrameTree(*root));
}
class FrameTreeAnonymousIframeBrowserTest : public FrameTreeBrowserTest {
public:
FrameTreeAnonymousIframeBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kEnableBlinkTestFeatures);
}
};
// Tests the mojo propagation of the 'anonymous' attribute to the browser.
IN_PROC_BROWSER_TEST_F(FrameTreeAnonymousIframeBrowserTest,
AttributeIsPropagatedToBrowser) {
GURL main_url(embedded_test_server()->GetURL("/hello.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Not setting the attribute => the iframe is not anonymous.
EXPECT_TRUE(ExecJs(root,
"var f = document.createElement('iframe');"
"document.body.appendChild(f);"));
EXPECT_EQ(1U, root->child_count());
EXPECT_FALSE(root->child_at(0)->anonymous());
EXPECT_EQ(false, EvalJs(root->child_at(0)->current_frame_host(),
"window.isAnonymouslyFramed"));
// Setting the attribute on the iframe element makes the iframe anonymous.
EXPECT_TRUE(ExecJs(root,
"var d = document.createElement('div');"
"d.innerHTML = '<iframe anonymous></iframe>';"
"document.body.appendChild(d);"));
EXPECT_EQ(2U, root->child_count());
EXPECT_TRUE(root->child_at(1)->anonymous());
EXPECT_EQ(true, EvalJs(root->child_at(1)->current_frame_host(),
"window.isAnonymouslyFramed"));
// Setting the attribute via javascript works.
EXPECT_TRUE(ExecJs(root,
"var g = document.createElement('iframe');"
"g.anonymous = true;"
"document.body.appendChild(g);"));
EXPECT_EQ(3U, root->child_count());
EXPECT_TRUE(root->child_at(2)->anonymous());
EXPECT_EQ(true, EvalJs(root->child_at(2)->current_frame_host(),
"window.isAnonymouslyFramed"));
EXPECT_TRUE(ExecJs(root, "g.anonymous = false;"));
EXPECT_FALSE(root->child_at(2)->anonymous());
EXPECT_EQ(true, EvalJs(root->child_at(2)->current_frame_host(),
"window.isAnonymouslyFramed"));
EXPECT_TRUE(ExecJs(root, "g.anonymous = true;"));
EXPECT_TRUE(root->child_at(2)->anonymous());
EXPECT_EQ(true, EvalJs(root->child_at(2)->current_frame_host(),
"window.isAnonymouslyFramed"));
}
// This is fenced frames test class differs on from FencedFrameTreeBrowserTest,
// by testing MPArch fenced frames exclusively (no ShadowDOM types), through the
// use of FencedFrameTestHelper.
class MPArchFencedFramesFrameTreeBrowserTest : public FrameTreeBrowserTest {
public:
MPArchFencedFramesFrameTreeBrowserTest() = default;
MPArchFencedFramesFrameTreeBrowserTest(
const MPArchFencedFramesFrameTreeBrowserTest&) = delete;
MPArchFencedFramesFrameTreeBrowserTest& operator=(
const MPArchFencedFramesFrameTreeBrowserTest&) = delete;
~MPArchFencedFramesFrameTreeBrowserTest() override = default;
content::test::FencedFrameTestHelper& fenced_frame_test_helper() {
return fenced_frame_helper_;
}
RenderFrameHostImpl* current_frame_host() {
return static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
}
private:
content::test::FencedFrameTestHelper fenced_frame_helper_;
};
} // namespace content