| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/fenced_frame/fenced_frame.h" |
| |
| #include <memory> |
| #include <string> |
| #include <tuple> |
| |
| #include "base/containers/contains.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "build/buildflag.h" |
| #include "components/network_session_configurator/common/network_switches.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "content/browser/attribution_reporting/attribution_manager.h" |
| #include "content/browser/attribution_reporting/attribution_os_level_manager.h" |
| #include "content/browser/attribution_reporting/attribution_test_utils.h" |
| #include "content/browser/attribution_reporting/test/mock_content_browser_client.h" |
| #include "content/browser/back_forward_cache_browsertest.h" |
| #include "content/browser/fenced_frame/fenced_frame_reporter.h" |
| #include "content/browser/private_aggregation/private_aggregation_manager.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_proxy_host.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/features.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/frame_type.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.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_content_browser_client.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/fenced_frame_test_util.h" |
| #include "content/public/test/mock_web_contents_observer.h" |
| #include "content/public/test/navigation_handle_observer.h" |
| #include "content/public/test/resource_load_observer.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/url_loader_monitor.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/shell/browser/shell_browser_context.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "content/test/fenced_frame_test_utils.h" |
| #include "mojo/public/cpp/test_support/test_utils.h" |
| #include "net/base/features.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/connection_tracker.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/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/cors/cors.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest-spi.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h" |
| #include "third_party/blink/public/common/frame/fenced_frame_sandbox_flags.h" |
| #include "third_party/blink/public/mojom/fenced_frame/fenced_frame.mojom.h" |
| #include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h" |
| #include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| namespace cors = network::cors::header_names; |
| |
| constexpr char kAddIframeScript[] = R"({ |
| (()=>{ |
| return new Promise((resolve) => { |
| const frame = document.createElement('iframe'); |
| frame.addEventListener('load', () => {resolve();}); |
| frame.src = $1; |
| document.body.appendChild(frame); |
| }); |
| })(); |
| })"; |
| |
| constexpr char kReportingURL[] = "/_report_event_server.html"; |
| |
| GURL GenerateAndVerifyPendingMappedURN( |
| FencedFrameURLMapping* fenced_frame_url_mapping) { |
| std::optional<GURL> pending_urn = |
| fenced_frame_url_mapping->GeneratePendingMappedURN(); |
| EXPECT_TRUE(pending_urn.has_value()); |
| EXPECT_TRUE(pending_urn->is_valid()); |
| |
| return pending_urn.value(); |
| } |
| |
| } // namespace |
| |
| class FencedFrameBrowserTestBase : public ContentBrowserTest { |
| public: |
| using ServerType = net::EmbeddedTestServer::Type; |
| FencedFrameBrowserTestBase() : https_server_(ServerType::TYPE_HTTPS) { |
| fenced_frame_test_helper_ = std::make_unique<test::FencedFrameTestHelper>(); |
| } |
| |
| // Defines the skeleton of set up method. |
| void SetUpOnMainThread() final { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| https_server()->ServeFilesFromSourceDirectory(GetTestDataFilePath()); |
| https_server()->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); |
| SetupCrossSiteRedirector(https_server()); |
| net::test_server::RegisterDefaultHandlers(https_server()); |
| |
| AdditionalSetup(); |
| AssertServerStart(); |
| } |
| |
| WebContentsImpl* web_contents() { |
| return static_cast<WebContentsImpl*>(shell()->web_contents()); |
| } |
| |
| RenderFrameHostImpl* primary_main_frame_host() { |
| return web_contents()->GetPrimaryMainFrame(); |
| } |
| |
| test::FencedFrameTestHelper& fenced_frame_test_helper() { |
| return *fenced_frame_test_helper_.get(); |
| } |
| |
| net::EmbeddedTestServer* https_server() { return &https_server_; } |
| |
| private: |
| // Some test cases require server starting after performing other setups, mark |
| // this virtual so that concrete test classes can override it with an empty |
| // implementation. |
| virtual void AssertServerStart() { ASSERT_TRUE(https_server()->Start()); } |
| |
| // Concrete test classes can override this to implement custom setups. |
| virtual void AdditionalSetup() {} |
| |
| // This is a unique ptr because in some test cases we don't want to use it, |
| // and it automatically enables MPArch fenced frames when created. |
| std::unique_ptr<test::FencedFrameTestHelper> fenced_frame_test_helper_; |
| net::EmbeddedTestServer https_server_; |
| }; |
| |
| class FencedFrameMPArchBrowserTest : public FencedFrameBrowserTestBase { |
| protected: |
| FencedFrameMPArchBrowserTest() = default; |
| |
| // TODO(crbug.com/40285326): This fails with the field trial testing config. |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| FencedFrameBrowserTestBase::SetUpCommandLine(command_line); |
| command_line->AppendSwitch("disable-field-trial-config"); |
| } |
| |
| base::HistogramTester histogram_tester_; |
| |
| private: |
| // Server must start after ControllableHttpResponse object being constructed. |
| void AssertServerStart() override {} |
| }; |
| |
| // This is a test class for tests that need to use IsolateAllSiteForTesting() |
| // and that will be testing process assignments. It is important that |
| // IsolateAllSiteForTesting is enabled early in these cases, otherwise the |
| // tests can end up with a main frame where |
| // AreOriginKeyedProcessesEnabledByDefault() was false when the main frame was |
| // created (and this is stored in the main frame's BrowsingInstance), and then |
| // AreOriginKeyedProcessesEnabledByDefault() later returns true due to |
| // IsolateAllSiteForTesting() turning on site-per-process. This sequence can |
| // lead to inconsistent SiteInfo settings. |
| class FencedFrameMPArchBrowserTest_IsolateAllSites |
| : public FencedFrameMPArchBrowserTest { |
| protected: |
| FencedFrameMPArchBrowserTest_IsolateAllSites() = default; |
| |
| // TODO(crbug.com/40285326): This fails with the field trial testing config. |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| FencedFrameMPArchBrowserTest::SetUpCommandLine(command_line); |
| IsolateAllSitesForTesting(command_line); |
| } |
| }; |
| |
| // Tests that the renderer can create a <fencedframe> that results in a |
| // browser-side content::FencedFrame also being created. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| CreateFromScriptAndDestroy) { |
| base::HistogramTester histogram_tester; |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("c.test", "/title1.html"))); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| main_url)); |
| FrameTreeNode* fenced_frame_root_node = fenced_frame_rfh->frame_tree_node(); |
| EXPECT_TRUE(fenced_frame_root_node->render_manager() |
| ->GetProxyToOuterDelegate() |
| ->is_render_frame_proxy_live()); |
| |
| // Test `RenderFrameHostImpl::IsInPrimaryMainFrame`. |
| EXPECT_TRUE(primary_rfh->IsInPrimaryMainFrame()); |
| EXPECT_FALSE(fenced_frame_rfh->IsInPrimaryMainFrame()); |
| |
| // Test `FrameTreeNode::IsFencedFrameRoot()`. |
| EXPECT_FALSE( |
| web_contents()->GetPrimaryFrameTree().root()->IsFencedFrameRoot()); |
| EXPECT_FALSE(primary_rfh->child_at(0)->IsFencedFrameRoot()); |
| EXPECT_TRUE(fenced_frame_root_node->IsFencedFrameRoot()); |
| |
| // Test `FrameTreeNode::IsInFencedFrameTree()`. |
| EXPECT_FALSE( |
| web_contents()->GetPrimaryFrameTree().root()->IsInFencedFrameTree()); |
| EXPECT_FALSE(primary_rfh->child_at(0)->IsInFencedFrameTree()); |
| EXPECT_TRUE(fenced_frame_root_node->IsInFencedFrameTree()); |
| |
| EXPECT_TRUE(ExecJs(primary_rfh.get(), |
| "const ff = document.querySelector('fencedframe');\ |
| ff.remove();")); |
| ASSERT_TRUE(fenced_frame_rfh.WaitUntilRenderFrameDeleted()); |
| |
| EXPECT_TRUE(primary_rfh->GetFencedFrames().empty()); |
| EXPECT_TRUE(fenced_frame_rfh.IsDestroyed()); |
| histogram_tester.ExpectTotalCount( |
| "Ads.InterestGroup.Auction.AdNavigationStarted", 0); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, CreateFromParser) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL top_level_url = |
| https_server()->GetURL("c.test", "/fenced_frames/basic.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), top_level_url)); |
| |
| // The fenced frame is set-up synchronously, so it should exist immediately. |
| RenderFrameHostImplWrapper dummy_child_frame( |
| primary_main_frame_host()->child_at(0)->current_frame_host()); |
| EXPECT_NE(dummy_child_frame->inner_tree_main_frame_tree_node_id(), |
| FrameTreeNode::kFrameTreeNodeInvalidId); |
| FrameTreeNode* inner_frame_tree_node = FrameTreeNode::GloballyFindByID( |
| dummy_child_frame->inner_tree_main_frame_tree_node_id()); |
| EXPECT_TRUE(inner_frame_tree_node); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, Navigation) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("c.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // WebContentsObservers should not be notified of commits happening |
| // in the non-primary navigation controller. |
| testing::NiceMock<MockWebContentsObserver> web_contents_observer( |
| web_contents()); |
| EXPECT_CALL(web_contents_observer, NavigationEntryCommitted(testing::_)) |
| .Times(0); |
| EXPECT_CALL(web_contents_observer, NavigationEntryChanged(testing::_)) |
| .Times(0); |
| |
| RenderFrameHostImpl* primary_rfh = primary_main_frame_host(); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| RenderFrameHost* fenced_frame_rfh = |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh, |
| fenced_frame_url); |
| |
| // Test that a fenced frame navigation does not impact the primary main |
| // frame... |
| EXPECT_EQ(main_url, primary_rfh->GetLastCommittedURL()); |
| // ... but should target the correct frame. |
| EXPECT_EQ(fenced_frame_url, fenced_frame_rfh->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(fenced_frame_url), |
| fenced_frame_rfh->GetLastCommittedOrigin()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, AboutBlankNavigation) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* primary_rfh = primary_main_frame_host(); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh, fenced_frame_url); |
| |
| std::vector<FencedFrame*> fenced_frames = primary_rfh->GetFencedFrames(); |
| ASSERT_EQ(1ul, fenced_frames.size()); |
| FencedFrame* fenced_frame = fenced_frames.back(); |
| |
| // Expect the origin is correct. |
| EXPECT_EQ(url::Origin::Create(fenced_frame_url), |
| EvalJs(fenced_frame->GetInnerRoot(), "self.origin;")); |
| |
| // Assigning the location from the parent cause the SiteInstance |
| // to be calculated incorrectly and crash. see https://crbug.com/1268238. |
| // We can't use `NavigateFrameInFencedFrameTree` because that navigates |
| // from the inner frame tree and we want the navigation to occur from |
| // the outer frame tree. |
| TestFrameNavigationObserver observer(fenced_frame->GetInnerRoot()); |
| EXPECT_TRUE(ExecJs(primary_rfh, |
| "document.querySelector('fencedframe').config = new " |
| "FencedFrameConfig('about:blank');")); |
| observer.Wait(); |
| |
| EXPECT_FALSE(fenced_frame->GetInnerRoot()->IsErrorDocument()); |
| EXPECT_EQ("null", EvalJs(fenced_frame->GetInnerRoot(), "self.origin;")); |
| EXPECT_EQ("about:blank", |
| EvalJs(fenced_frame->GetInnerRoot(), "window.location.href")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| SettingNullConfigNavigatesToAboutBlank) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* primary_rfh = primary_main_frame_host(); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh, fenced_frame_url); |
| |
| std::vector<FencedFrame*> fenced_frames = primary_rfh->GetFencedFrames(); |
| ASSERT_EQ(1ul, fenced_frames.size()); |
| FencedFrame* fenced_frame = fenced_frames.back(); |
| |
| // Expect the origin is correct. |
| EXPECT_EQ(url::Origin::Create(fenced_frame_url), |
| EvalJs(fenced_frame->GetInnerRoot(), "self.origin;")); |
| |
| TestFrameNavigationObserver observer(fenced_frame->GetInnerRoot()); |
| EXPECT_TRUE(ExecJs(primary_rfh, |
| "document.querySelector('fencedframe').config = null;")); |
| observer.Wait(); |
| |
| EXPECT_FALSE(fenced_frame->GetInnerRoot()->IsErrorDocument()); |
| EXPECT_EQ("null", EvalJs(fenced_frame->GetInnerRoot(), "self.origin;")); |
| EXPECT_EQ("about:blank", |
| EvalJs(fenced_frame->GetInnerRoot(), "window.location.href")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, FrameIteration) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("c.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| fenced_frame_url)); |
| |
| // Test that the outer => inner delegate mechanism works correctly. |
| EXPECT_THAT(CollectAllRenderFrameHosts(primary_rfh.get()), |
| testing::ElementsAre(primary_rfh.get(), fenced_frame_rfh.get())); |
| |
| // Test that the inner => outer delegate mechanism works correctly. |
| EXPECT_EQ(nullptr, fenced_frame_rfh->GetParent()); |
| EXPECT_EQ(fenced_frame_rfh->GetParentOrOuterDocument(), primary_rfh.get()); |
| EXPECT_EQ(fenced_frame_rfh->GetOutermostMainFrame(), primary_rfh.get()); |
| EXPECT_EQ(fenced_frame_rfh->GetParentOrOuterDocumentOrEmbedder(), |
| primary_rfh.get()); |
| EXPECT_EQ(fenced_frame_rfh->GetOutermostMainFrameOrEmbedder(), |
| primary_rfh.get()); |
| |
| // WebContentsImpl::ForEachFrameTree should include fenced frames. |
| bool visited_fenced_frame_frame_tree = false; |
| web_contents()->ForEachFrameTree( |
| base::BindLambdaForTesting([&](FrameTree& frame_tree) { |
| if (&frame_tree == fenced_frame_rfh->frame_tree()) { |
| visited_fenced_frame_frame_tree = true; |
| } |
| })); |
| EXPECT_TRUE(visited_fenced_frame_frame_tree); |
| } |
| |
| namespace { |
| |
| // Intercepts calls to RenderFramHostImpl's CreateFencedFrame mojo method, and |
| // connects a NavigationDelayer which delays the FencedFrameOwnerHost's |
| // Navigate mojo method. |
| class NavigationDelayerInterceptor |
| : public blink::mojom::LocalFrameHostInterceptorForTesting { |
| public: |
| explicit NavigationDelayerInterceptor(RenderFrameHostImpl* render_frame_host, |
| base::TimeDelta duration) |
| : render_frame_host_(render_frame_host), |
| duration_(duration), |
| impl_(render_frame_host_->local_frame_host_receiver_for_testing() |
| .SwapImplForTesting(this)) {} |
| |
| ~NavigationDelayerInterceptor() override = default; |
| |
| blink::mojom::LocalFrameHost* GetForwardingInterface() override { |
| return impl_; |
| } |
| |
| void CreateFencedFrame( |
| mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost> |
| pending_receiver, |
| blink::mojom::RemoteFrameInterfacesFromRendererPtr |
| remote_frame_interfaces, |
| const blink::RemoteFrameToken& frame_token, |
| const base::UnguessableToken& devtools_frame_token) override { |
| mojo::PendingAssociatedRemote<blink::mojom::FencedFrameOwnerHost> |
| original_remote; |
| |
| GetForwardingInterface()->CreateFencedFrame( |
| original_remote.InitWithNewEndpointAndPassReceiver(), |
| std::move(remote_frame_interfaces), frame_token, devtools_frame_token); |
| std::vector<FencedFrame*> fenced_frames = |
| render_frame_host_->GetFencedFrames(); |
| ASSERT_FALSE(fenced_frames.empty()); |
| navigate_interceptor_ = std::make_unique<NavigationDelayer>( |
| std::move(original_remote), std::move(pending_receiver), |
| fenced_frames.back(), duration_); |
| } |
| |
| private: |
| class NavigationDelayer : public blink::mojom::FencedFrameOwnerHost { |
| public: |
| explicit NavigationDelayer( |
| mojo::PendingAssociatedRemote<blink::mojom::FencedFrameOwnerHost> |
| original_remote, |
| mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost> |
| receiver, |
| FencedFrame* fenced_frame, |
| base::TimeDelta duration) |
| : original_remote_(std::move(original_remote)), |
| fenced_frame_(fenced_frame), |
| duration_(duration) { |
| receiver_.Bind(std::move(receiver)); |
| } |
| |
| ~NavigationDelayer() override = default; |
| |
| void Navigate(const GURL& url, |
| base::TimeTicks navigation_start_time, |
| const std::optional<std::u16string>& |
| embedder_shared_storage_context) override { |
| base::PlatformThread::Sleep(duration_); |
| fenced_frame_->Navigate(url, navigation_start_time, |
| embedder_shared_storage_context); |
| } |
| |
| void DidChangeFramePolicy(const blink::FramePolicy& frame_policy) override { |
| fenced_frame_->DidChangeFramePolicy(frame_policy); |
| } |
| |
| private: |
| mojo::AssociatedRemote<blink::mojom::FencedFrameOwnerHost> original_remote_; |
| mojo::AssociatedReceiver<blink::mojom::FencedFrameOwnerHost> receiver_{ |
| this}; |
| raw_ptr<FencedFrame> fenced_frame_; |
| const base::TimeDelta duration_; |
| }; |
| |
| raw_ptr<RenderFrameHostImpl> render_frame_host_; |
| std::unique_ptr<NavigationDelayer> navigate_interceptor_; |
| const base::TimeDelta duration_; |
| raw_ptr<blink::mojom::LocalFrameHost> impl_; |
| }; |
| |
| } // namespace |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, NavigationStartTime) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* primary_rfh = primary_main_frame_host(); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| constexpr char kAddFencedFrameScript[] = R"({ |
| const fenced_frame = document.createElement('fencedframe'); |
| fenced_frame.config = new FencedFrameConfig($1); |
| document.body.appendChild(fenced_frame); |
| })"; |
| |
| const int delay_in_milliseconds = 1000; |
| |
| // The UI thread of the browser process will sleep |delay_in_milliseconds| |
| // just before handing FencedFrameOwnerHost's Navigate mojo method. |
| NavigationDelayerInterceptor interceptor( |
| primary_rfh, base::Milliseconds(delay_in_milliseconds)); |
| |
| EXPECT_TRUE( |
| ExecJs(primary_rfh, JsReplace(kAddFencedFrameScript, fenced_frame_url))); |
| std::vector<FencedFrame*> fenced_frames = primary_rfh->GetFencedFrames(); |
| ASSERT_EQ(1U, fenced_frames.size()); |
| FencedFrame* fenced_frame = fenced_frames[0]; |
| WaitForLoadStop(web_contents()); |
| |
| // The duration between navigationStart (measured in the renderer process) and |
| // requestStart (measured in the browser process due to PlzNavigate) must be |
| // greater than or equal to |delay_in_milliseconds|. |
| EXPECT_GE(EvalJs(fenced_frame->GetInnerRoot(), |
| "performance.timing.requestStart - " |
| "performance.timing.navigationStart") |
| .ExtractInt(), |
| delay_in_milliseconds); |
| } |
| |
| // Test that ensures we can post from an cross origin iframe into the |
| // fenced frame root. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, CrossOriginMessagePost) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| const GURL cross_origin_iframe_url = |
| https_server()->GetURL("b.com", "/fenced_frames/title1.html"); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("c.test", "/title1.html"))); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| main_url)); |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh.get(), |
| JsReplace(kAddIframeScript, cross_origin_iframe_url))); |
| |
| RenderFrameHostImpl* iframe = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(fenced_frame_rfh.get(), 0)); |
| |
| EXPECT_TRUE( |
| EvalJs(iframe->GetParent(), R"(window.addEventListener('message', (e) => { |
| e.source.postMessage('echo ' + e.data, "*"); |
| }, false); true)") |
| .ExtractBool()); |
| EXPECT_EQ("echo test", EvalJs(iframe, R"((async() => { |
| let promise = new Promise(function(resolve, reject) { |
| window.addEventListener('message', (e) => { |
| resolve(e.data) |
| }, false); |
| window.parent.postMessage('test', "*"); |
| }); |
| let result = await promise; |
| return result; |
| })())")); |
| } |
| |
| // Test that when the documents inside fenced frame tree are loading, |
| // WebContentsObserver::DocumentOnLoadCompletedInPrimaryMainFrame is not invoked |
| // for fenced frames as it is only invoked for primary main frames. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| DocumentOnLoadCompletedInPrimaryMainFrame) { |
| ASSERT_TRUE(https_server()->Start()); |
| // Initialize a MockWebContentsObserver to ensure that |
| // DocumentOnLoadCompletedInPrimaryMainFrame is only invoked for primary main |
| // RenderFrameHosts. |
| testing::NiceMock<MockWebContentsObserver> web_contents_observer( |
| web_contents()); |
| |
| // Navigate to an initial primary page. This should result in invoking |
| // DocumentOnLoadCompletedInPrimaryMainFrame once. |
| EXPECT_CALL(web_contents_observer, |
| DocumentOnLoadCompletedInPrimaryMainFrame()) |
| .Times(1); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("c.test", "/title1.html"))); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| |
| // Once the fenced frame complets loading, it shouldn't result in |
| // invoking DocumentOnLoadCompletedInPrimaryMainFrame. |
| EXPECT_CALL(web_contents_observer, |
| DocumentOnLoadCompletedInPrimaryMainFrame()) |
| .Times(0); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| RenderFrameHostImplWrapper inner_fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| fenced_frame_url)); |
| FrameTreeNode* fenced_frame_root_node = |
| inner_fenced_frame_rfh->frame_tree_node(); |
| EXPECT_FALSE(fenced_frame_root_node->IsLoading()); |
| } |
| |
| // Test that when the documents inside the fenced frame tree are loading, |
| // WebContentsObserver::PrimaryMainDocumentElementAvailable is not invoked for |
| // fenced frames as it is only invoked for primary main frames. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| PrimaryMainDocumentElementAvailable) { |
| ASSERT_TRUE(https_server()->Start()); |
| // Initialize a MockWebContentsObserver to ensure that |
| // PrimaryMainDocumentElementAvailable is only invoked for primary main |
| // RenderFrameHosts. |
| testing::NiceMock<MockWebContentsObserver> web_contents_observer( |
| web_contents()); |
| testing::InSequence s; |
| |
| // Navigate to an initial primary page. This should result in invoking |
| // PrimaryMainDocumentElementAvailable once. |
| EXPECT_CALL(web_contents_observer, PrimaryMainDocumentElementAvailable()) |
| .Times(1); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("c.test", "/title1.html"))); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| |
| // Once the fenced frame completes loading, it shouldn't result in |
| // invoking PrimaryMainDocumentElementAvailable. |
| EXPECT_CALL(web_contents_observer, PrimaryMainDocumentElementAvailable()) |
| .Times(0); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| RenderFrameHostImplWrapper inner_fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| fenced_frame_url)); |
| FrameTreeNode* fenced_frame_root_node = |
| inner_fenced_frame_rfh->frame_tree_node(); |
| EXPECT_FALSE(fenced_frame_root_node->IsLoading()); |
| } |
| |
| // Test that a fenced-frame does not perform any of the Android main-frame |
| // viewport behaviors like zoom-out-to-fit-content or parsing the viewport |
| // <meta>. |
| // Flaky on Mac https://crbug.com/1349900 |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_ViewportSettings DISABLED_ViewportSettings |
| #else |
| #define MAYBE_ViewportSettings ViewportSettings |
| #endif |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, MAYBE_ViewportSettings) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL top_level_url = |
| https_server()->GetURL("c.test", "/fenced_frames/viewport.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), top_level_url)); |
| |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| std::vector<FencedFrame*> fenced_frames = primary_rfh->GetFencedFrames(); |
| ASSERT_EQ(1ul, fenced_frames.size()); |
| FencedFrame* fenced_frame = fenced_frames.back(); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // Ensure various dimensions and properties in the fenced frame |
| // match the dimensions of the <fencedframe> in the parent and do |
| // not take into account the <meta name="viewport"> in the |
| // fenced-frame page. |
| EXPECT_EQ( |
| EvalJs(fenced_frame->GetInnerRoot(), "window.innerWidth").ExtractInt(), |
| 314); |
| EXPECT_EQ( |
| EvalJs(fenced_frame->GetInnerRoot(), "window.innerHeight").ExtractInt(), |
| 271); |
| EXPECT_EQ(EvalJs(fenced_frame->GetInnerRoot(), |
| "document.documentElement.clientWidth") |
| .ExtractInt(), |
| 314); |
| EXPECT_EQ(EvalJs(fenced_frame->GetInnerRoot(), "window.visualViewport.scale") |
| .ExtractDouble(), |
| 1.0); |
| } |
| |
| // Test that fenced frames use the primary main frame's UKM source id during |
| // navigation. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, GetPageUkmSourceId) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("c.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| NavigationHandleObserver handle_observer(web_contents(), fenced_frame_url); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| ASSERT_TRUE(fenced_frame_rfh); |
| |
| ukm::SourceId nav_request_id = handle_observer.next_page_ukm_source_id(); |
| // Should have the same page UKM ID in navigation as page post commit, and as |
| // the primary main frame. |
| EXPECT_EQ(primary_main_frame_host()->GetPageUkmSourceId(), nav_request_id); |
| EXPECT_EQ(fenced_frame_rfh->GetPageUkmSourceId(), nav_request_id); |
| } |
| |
| // Test that iframes that nested within fenced frames use the primary main |
| // frame's UKM source id during navigation. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| GetPageUkmSourceId_NestedFrame) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("c.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| ASSERT_TRUE(fenced_frame_rfh); |
| |
| const GURL iframe_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| NavigationHandleObserver handle_observer(web_contents(), iframe_url); |
| EXPECT_TRUE( |
| ExecJs(fenced_frame_rfh.get(), JsReplace(kAddIframeScript, iframe_url))); |
| |
| RenderFrameHostImpl* iframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(fenced_frame_rfh.get(), 0)); |
| ukm::SourceId nav_request_id = handle_observer.next_page_ukm_source_id(); |
| // Should have the same page UKM ID in navigation as page post commit, and as |
| // the primary main frame. |
| EXPECT_EQ(primary_main_frame_host()->GetPageUkmSourceId(), nav_request_id); |
| EXPECT_EQ(fenced_frame_rfh->GetPageUkmSourceId(), nav_request_id); |
| EXPECT_EQ(iframe_rfh->GetPageUkmSourceId(), nav_request_id); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| DocumentUKMSourceIdShouldNotBeAssociatedWithURL) { |
| ukm::TestAutoSetUkmRecorder recorder; |
| |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| ukm::SourceId fenced_frame_document_ukm_source_id = ukm::kInvalidSourceId; |
| DidFinishNavigationObserver observer( |
| web_contents(), |
| base::BindLambdaForTesting([&fenced_frame_document_ukm_source_id]( |
| NavigationHandle* navigation_handle) { |
| if (navigation_handle->GetNavigatingFrameType() != |
| FrameType::kFencedFrameRoot) |
| return; |
| NavigationRequest* request = NavigationRequest::From(navigation_handle); |
| fenced_frame_document_ukm_source_id = |
| request->commit_params().document_ukm_source_id; |
| })); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| ASSERT_TRUE(fenced_frame_rfh); |
| ASSERT_NE(ukm::kInvalidSourceId, fenced_frame_document_ukm_source_id); |
| EXPECT_EQ(nullptr, |
| recorder.GetSourceForSourceId(fenced_frame_document_ukm_source_id)); |
| } |
| |
| // Test that FrameTree::CollectNodesForIsLoading doesn't include inner |
| // WebContents nodes. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, NodesForIsLoading) { |
| ASSERT_TRUE(https_server()->Start()); |
| GURL url_a(https_server()->GetURL("c.test", "/page_with_iframe.html")); |
| GURL url_b(https_server()->GetURL("c.test", "/title1.html")); |
| GURL fenced_frame_url( |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html")); |
| |
| // 1. Navigate to an initial primary page. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| FrameTree& primary_frame_tree = web_contents()->GetPrimaryFrameTree(); |
| |
| // 2. Create a fenced frame embedded inside primary page. |
| RenderFrameHostImplWrapper outer_fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| fenced_frame_url)); |
| |
| // 3. Create a inner WebContents and attach it to the main contents. Navigate |
| // the inner web contents to an initial page. |
| WebContentsImpl* inner_contents = |
| static_cast<WebContentsImpl*>(CreateAndAttachInnerContents( |
| primary_rfh.get()->child_at(0)->current_frame_host())); |
| ASSERT_TRUE(NavigateToURLFromRenderer(inner_contents, url_b)); |
| |
| RenderFrameHostImpl* inner_contents_rfh = |
| inner_contents->GetPrimaryMainFrame(); |
| FrameTree& inner_contents_primary_frame_tree = |
| inner_contents->GetPrimaryFrameTree(); |
| ASSERT_TRUE(inner_contents_rfh); |
| |
| // 4. Create a fenced frame embedded inside inner WebContents. |
| RenderFrameHostImplWrapper inner_fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(inner_contents_rfh, |
| fenced_frame_url)); |
| |
| // 5. FrameTree::CollectNodesForIsLoading should only include primary_rfh and |
| // outer_fenced_frame_rfh when checked against outer delegate FrameTree. |
| std::vector<RenderFrameHostImpl*> outer_web_contents_frames; |
| for (auto* ftn : |
| web_contents()->GetPrimaryFrameTree().CollectNodesForIsLoading()) { |
| outer_web_contents_frames.push_back(ftn->current_frame_host()); |
| } |
| EXPECT_EQ(outer_web_contents_frames.size(), 2u); |
| EXPECT_THAT(outer_web_contents_frames, |
| testing::UnorderedElementsAre(primary_rfh.get(), |
| outer_fenced_frame_rfh.get())); |
| |
| // 6. FrameTree::CollectNodesForIsLoading should only include |
| // inner_contents_rfh and inner_fenced_frame_rfh when checked against inner |
| // delegate FrameTree. |
| std::vector<RenderFrameHostImpl*> inner_web_contents_frames; |
| for (auto* ftn : |
| inner_contents->GetPrimaryFrameTree().CollectNodesForIsLoading()) { |
| inner_web_contents_frames.push_back(ftn->current_frame_host()); |
| } |
| EXPECT_EQ(inner_web_contents_frames.size(), 2u); |
| EXPECT_THAT(inner_web_contents_frames, |
| testing::UnorderedElementsAre(inner_contents_rfh, |
| inner_fenced_frame_rfh.get())); |
| |
| // 7. Check that FrameTree::LoadingTree returns the correct FrameTree for both |
| // outer and inner WebContents frame trees. |
| EXPECT_NE(primary_frame_tree.LoadingTree(), |
| inner_contents_primary_frame_tree.LoadingTree()); |
| EXPECT_EQ(primary_frame_tree.LoadingTree(), &primary_frame_tree); |
| EXPECT_EQ(inner_contents_primary_frame_tree.LoadingTree(), |
| &inner_contents_primary_frame_tree); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| NoErrorPageOnEmptyFrameHttpError) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL kInitialUrl(https_server()->GetURL("c.test", "/title1.html")); |
| const GURL kEmpty404Url( |
| https_server()->GetURL("c.test", "/fenced_frames/empty404.html")); |
| |
| // Load an initial page. |
| EXPECT_TRUE(NavigateToURL(shell(), kInitialUrl)); |
| RenderFrameHostImplWrapper initial_rfh(primary_main_frame_host()); |
| |
| // Add a fenced frame empty page with 404 status. |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(initial_rfh.get(), |
| kEmpty404Url)); |
| ASSERT_TRUE(fenced_frame_rfh); |
| |
| // Confirm no error page was generated in its place. |
| std::string contents = |
| EvalJs(fenced_frame_rfh.get(), "document.body.textContent;") |
| .ExtractString(); |
| EXPECT_EQ(contents, std::string()); |
| } |
| |
| // Test that when the documents inside the fenced frame tree are loading, then |
| // `WebContents::IsLoading`, `FrameTree::IsLoadingIncludingInnerFrameTrees`, and |
| // `FrameTreeNode::IsLoading` should return true. Primary |
| // `FrameTree::IsLoadingIncludingInnerFrameTrees` value should reflect the |
| // loading state of descendant fenced frames. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, IsLoading) { |
| // Create a HTTP response to control fenced frame navigation. |
| net::test_server::ControllableHttpResponse fenced_frame_response( |
| https_server(), "/fenced_frames/title2.html"); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Navigate to primary url. |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("c.test", "/title1.html"))); |
| RenderFrameHostImpl* fenced_frame_parent_rfh = primary_main_frame_host(); |
| |
| // Create a fenced frame for fenced_frame_url. |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title2.html"); |
| fenced_frame_test_helper().CreateFencedFrameAsync(fenced_frame_parent_rfh, |
| fenced_frame_url); |
| |
| std::vector<FencedFrame*> fenced_frames = |
| fenced_frame_parent_rfh->GetFencedFrames(); |
| EXPECT_EQ(fenced_frames.size(), 1ul); |
| FencedFrame* fenced_frame = fenced_frames.back(); |
| |
| RenderFrameHostImplWrapper inner_fenced_frame_rfh( |
| fenced_frame->GetInnerRoot()); |
| FrameTreeNode* fenced_frame_root_node = |
| inner_fenced_frame_rfh->frame_tree_node(); |
| FrameTree& fenced_frame_tree = fenced_frame_root_node->frame_tree(); |
| |
| // All WebContents::IsLoading, FrameTree::IsLoadingIncludingInnerFrameTrees, |
| // and FrameTreeNode::IsLoading should return true when the fenced frame is |
| // loading along with primary FrameTree::IsLoadingIncludingInnerFrameTrees as |
| // we check for inner frame trees loading state. |
| EXPECT_TRUE(web_contents()->IsLoading()); |
| EXPECT_TRUE(primary_main_frame_host() |
| ->frame_tree() |
| ->IsLoadingIncludingInnerFrameTrees()); |
| EXPECT_TRUE(fenced_frame_root_node->IsLoading()); |
| EXPECT_TRUE(fenced_frame_tree.IsLoadingIncludingInnerFrameTrees()); |
| |
| // Complete the fenced frame response and finish fenced frame navigation. |
| fenced_frame_response.WaitForRequest(); |
| fenced_frame_response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Supports-Loading-Mode: fenced-frame\r\n" |
| "\r\n"); |
| fenced_frame_response.Done(); |
| |
| // Check that all the above loading states should return false once the fenced |
| // frame stops loading. |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_FALSE(web_contents()->IsLoading()); |
| EXPECT_FALSE(primary_main_frame_host() |
| ->frame_tree() |
| ->IsLoadingIncludingInnerFrameTrees()); |
| EXPECT_FALSE(fenced_frame_root_node->IsLoading()); |
| EXPECT_FALSE(fenced_frame_tree.IsLoadingIncludingInnerFrameTrees()); |
| } |
| |
| // Test that when the documents inside the fenced frame tree are loading, |
| // WebContentsObserver::DidStartLoading is fired and when document stops loading |
| // WebContentsObserver::DidStopLoading is fired. In this test primary page |
| // completed loading before fenced frame starts loading and we test the loading |
| // state in the end when both primary page and fenced frame completed loading. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| DidStartAndDidStopLoading) { |
| // Create a HTTP response to control fenced frame navigation. |
| net::test_server::ControllableHttpResponse fenced_frame_response( |
| https_server(), "/fenced_frames/title2.html"); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Navigate to primary url. |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("c.test", "/title1.html"))); |
| EXPECT_FALSE(web_contents()->IsLoading()); |
| RenderFrameHostImpl* fenced_frame_parent_rfh = primary_main_frame_host(); |
| EXPECT_EQ(fenced_frame_parent_rfh->GetFencedFrames().size(), 0ul); |
| |
| // Initialize a MockWebContentsObserver and ensure that |
| // DidStartLoading and DidStopLoading are invoked. |
| testing::NiceMock<MockWebContentsObserver> web_contents_observer( |
| web_contents()); |
| testing::InSequence s; |
| |
| // Create a fenced frame for fenced_frame_url. This will result in invoking |
| // DidStartLoading callback once. |
| EXPECT_CALL(web_contents_observer, DidStartLoading()).Times(1); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title2.html"); |
| fenced_frame_test_helper().CreateFencedFrameAsync(fenced_frame_parent_rfh, |
| fenced_frame_url); |
| std::vector<FencedFrame*> fenced_frames = |
| fenced_frame_parent_rfh->GetFencedFrames(); |
| EXPECT_EQ(fenced_frame_parent_rfh->GetFencedFrames().size(), 1ul); |
| EXPECT_TRUE(web_contents()->IsLoading()); |
| |
| // Complete the fenced frame response and finish fenced frame navigation. |
| fenced_frame_response.WaitForRequest(); |
| fenced_frame_response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Supports-Loading-Mode: fenced-frame\r\n" |
| "\r\n"); |
| fenced_frame_response.Done(); |
| |
| // Once the fenced frame stops loading, this should result in invoking |
| // the DidStopLoading callback once. |
| EXPECT_CALL(web_contents_observer, DidStopLoading()).Times(1); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| } |
| |
| // Ensure that WebContentsObserver::LoadProgressChanged is not invoked when |
| // there is a change in load state of fenced frame as LoadProgressChanged is |
| // attributed to only primary main frame load progress change. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, LoadProgressChanged) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| const GURL main_url = https_server()->GetURL("c.test", "/title1.html"); |
| |
| // Navigate to primary url. |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| |
| // Initialize a MockWebContentsObserver and ensure that LoadProgressChanged is |
| // not invoked for fenced frames. |
| testing::NiceMock<MockWebContentsObserver> web_contents_observer( |
| web_contents()); |
| |
| // Create a fenced frame for fenced_frame_url. This shouldn't call |
| // LoadProgressChanged. |
| EXPECT_CALL(web_contents_observer, LoadProgressChanged(testing::_)).Times(0); |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| fenced_frame_url); |
| } |
| |
| // Tests that NavigationHandle::GetNavigatingFrameType() returns the correct |
| // type. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| NavigationHandleFrameType) { |
| ASSERT_TRUE(https_server()->Start()); |
| { |
| DidFinishNavigationObserver observer( |
| web_contents(), |
| base::BindLambdaForTesting([](NavigationHandle* navigation_handle) { |
| EXPECT_TRUE(navigation_handle->IsInPrimaryMainFrame()); |
| DCHECK_EQ(navigation_handle->GetNavigatingFrameType(), |
| FrameType::kPrimaryMainFrame); |
| })); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), https_server()->GetURL("c.test", "/title1.html"))); |
| } |
| |
| { |
| DidFinishNavigationObserver observer( |
| web_contents(), |
| base::BindLambdaForTesting([](NavigationHandle* navigation_handle) { |
| EXPECT_FALSE(navigation_handle->IsInMainFrame()); |
| DCHECK_EQ(navigation_handle->GetNavigatingFrameType(), |
| FrameType::kSubframe); |
| })); |
| EXPECT_TRUE( |
| ExecJs(primary_main_frame_host(), |
| JsReplace(kAddIframeScript, |
| https_server()->GetURL("c.test", "/empty.html")))); |
| } |
| { |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| DidFinishNavigationObserver observer( |
| web_contents(), |
| base::BindLambdaForTesting([](NavigationHandle* navigation_handle) { |
| EXPECT_TRUE( |
| navigation_handle->GetRenderFrameHost()->IsFencedFrameRoot()); |
| DCHECK_EQ(navigation_handle->GetNavigatingFrameType(), |
| FrameType::kFencedFrameRoot); |
| })); |
| fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| fenced_frame_rfh.get(), fenced_frame_url); |
| } |
| } |
| |
| // Tests that an unload/beforeunload event handler won't be set from |
| // fenced frames. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, UnloadHandler) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("c.test", "/title1.html"))); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| main_url)); |
| |
| const char kConsolePattern[] = |
| "unload/beforeunload handlers are prohibited in fenced frames."; |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern(kConsolePattern); |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh.get(), |
| "window.addEventListener('beforeunload', (e) => {});")); |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_EQ(1u, console_observer.messages().size()); |
| } |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern(kConsolePattern); |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh.get(), |
| "window.addEventListener('unload', (e) => {});")); |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_EQ(1u, console_observer.messages().size()); |
| } |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern(kConsolePattern); |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh.get(), |
| "window.onbeforeunload = function(e){};")); |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_EQ(1u, console_observer.messages().size()); |
| } |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern(kConsolePattern); |
| EXPECT_TRUE( |
| ExecJs(fenced_frame_rfh.get(), "window.onunload = function(e){};")); |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_EQ(1u, console_observer.messages().size()); |
| } |
| } |
| |
| // Tests that an input event targeted to a fenced frame correctly |
| // triggers a user interaction notification for WebContentsObservers. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| UserInteractionForFencedFrame) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("c.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| fenced_frame_url)); |
| |
| ::testing::NiceMock<MockWebContentsObserver> web_contents_observer( |
| web_contents()); |
| EXPECT_CALL(web_contents_observer, DidGetUserInteraction(testing::_)) |
| .Times(1); |
| |
| // Target an event to the fenced frame's RenderWidgetHostView. |
| blink::WebMouseEvent mouse_event( |
| blink::WebInputEvent::Type::kMouseDown, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| mouse_event.button = blink::WebPointerProperties::Button::kLeft; |
| mouse_event.SetPositionInWidget(5, 5); |
| fenced_frame_rfh->GetRenderWidgetHost()->ForwardMouseEvent(mouse_event); |
| } |
| |
| // Test that WebContents::GetFocusedFrame includes results from a fenced |
| // frame's frame tree. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| FocusedFrameInFencedFrame) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL url = https_server()->GetURL("c.test", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), url)); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| RenderFrameHostImplWrapper fenced_frame_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| |
| // NavigateToURL sets initial focus, which is why GetFocusedFrame doesn't |
| // start as null. |
| EXPECT_EQ(web_contents()->GetFocusedFrame(), primary_main_frame_host()); |
| |
| FrameFocusedObserver focus_observer(fenced_frame_rfh.get()); |
| // ExecJs runs with a user gesture which is needed for the fenced frame to be |
| // allowed to take focus. |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh.get(), "window.focus()")); |
| focus_observer.Wait(); |
| EXPECT_EQ(web_contents()->GetFocusedFrame(), fenced_frame_rfh.get()); |
| } |
| |
| // Test that the initial navigation in a fenced frame, which navigates from the |
| // initial empty document, is not classified as a client redirect. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| InitialNavigationIsNotClientRedirect) { |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL url = https_server()->GetURL("c.test", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), url)); |
| |
| FrameNavigateParamsCapturer capturer(web_contents()); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url); |
| capturer.Wait(); |
| EXPECT_EQ(capturer.urls()[0], fenced_frame_url); |
| |
| ASSERT_EQ(1U, capturer.transitions().size()); |
| // The transition used for the initial navigation in the fenced frame is not |
| // classified as a client-side redirect. |
| EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs( |
| capturer.transitions()[0], |
| ui::PageTransitionFromInt(ui::PAGE_TRANSITION_AUTO_SUBFRAME))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest_IsolateAllSites, |
| ProcessAllocationWithFullSiteIsolation) { |
| ASSERT_TRUE(https_server()->Start()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL same_site_fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| const GURL cross_site_fenced_frame_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Empty fenced frame document should have a different site instance, but |
| // should be in the same process as embedder. |
| RenderFrameHost* fenced_frame_rfh = |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| GURL()); |
| EXPECT_NE(fenced_frame_rfh->GetSiteInstance(), |
| primary_main_frame_host()->GetSiteInstance()); |
| EXPECT_FALSE(fenced_frame_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| primary_main_frame_host()->GetSiteInstance())); |
| EXPECT_EQ(fenced_frame_rfh->GetProcess(), |
| primary_main_frame_host()->GetProcess()); |
| |
| // Same-site fenced frame document should be in the same process as embedder. |
| fenced_frame_rfh = fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| fenced_frame_rfh, same_site_fenced_frame_url); |
| EXPECT_NE(fenced_frame_rfh->GetSiteInstance(), |
| primary_main_frame_host()->GetSiteInstance()); |
| EXPECT_FALSE(fenced_frame_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| primary_main_frame_host()->GetSiteInstance())); |
| EXPECT_EQ(fenced_frame_rfh->GetProcess(), |
| primary_main_frame_host()->GetProcess()); |
| |
| // Cross-site fenced frame document should be in a different process from its |
| // embedder. |
| fenced_frame_rfh = fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| fenced_frame_rfh, cross_site_fenced_frame_url); |
| EXPECT_NE(fenced_frame_rfh->GetProcess(), |
| primary_main_frame_host()->GetProcess()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest_IsolateAllSites, |
| CrossSiteFencedFramesShareProcess) { |
| ASSERT_TRUE(https_server()->Start()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL same_site_fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| const GURL cross_site_fenced_frame_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Two fenced frames that are same-site with each other, and cross-site with |
| // the embedder should be in the same process. This happens due to the |
| // subframe process reuse policy which also applies to fenced frames (the |
| // second fenced frame will try to reuse an existing process that is locked to |
| // the same site). |
| RenderFrameHost* ff_rfh_1 = fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), cross_site_fenced_frame_url); |
| RenderFrameHost* ff_rfh_2 = fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), cross_site_fenced_frame_url); |
| EXPECT_NE(ff_rfh_1->GetSiteInstance(), ff_rfh_2->GetSiteInstance()); |
| EXPECT_NE(ff_rfh_1->GetProcess(), primary_main_frame_host()->GetProcess()); |
| EXPECT_EQ(ff_rfh_1->GetProcess(), ff_rfh_2->GetProcess()); |
| |
| // The cross-site fenced frame should be moved to the same process as embedder |
| // when navigated to same-site (similar to before). |
| ff_rfh_2 = fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| ff_rfh_2, same_site_fenced_frame_url); |
| EXPECT_EQ(ff_rfh_2->GetProcess(), primary_main_frame_host()->GetProcess()); |
| } |
| |
| // Tests to ensure that the owner forced sandbox flags are set when a fenced |
| // frame is created, and are kept after the fenced frame is navigated |
| // to a page with a CSP sandbox header. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| EnsureSandboxFlagsEnforced) { |
| ASSERT_TRUE(https_server()->Start()); |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); |
| |
| RenderFrameHostImplWrapper ff_rfh( |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), |
| fenced_frame_url)); |
| |
| EXPECT_TRUE(ff_rfh->IsSandboxed(blink::kFencedFrameForcedSandboxFlags)); |
| |
| GURL new_fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/sandbox_flags.html"); |
| RenderFrameHostImplWrapper new_fenced_frame_rfh( |
| fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| ff_rfh.get(), new_fenced_frame_url)); |
| |
| EXPECT_TRUE(!new_fenced_frame_rfh->IsErrorDocument()); |
| EXPECT_TRUE( |
| new_fenced_frame_rfh->IsSandboxed(blink::kFencedFrameForcedSandboxFlags)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| CreateFencedFrameWhileInBackForwardCache) { |
| if (!BackForwardCache::IsBackForwardCacheFeatureEnabled()) { |
| LOG(ERROR) << "BackForwardCache must be enabled for this test."; |
| return; |
| } |
| |
| ASSERT_TRUE(https_server()->Start()); |
| ASSERT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("a.test", "/title1.html"))); |
| RenderFrameHostWrapper primary_rfh(primary_main_frame_host()); |
| ASSERT_TRUE( |
| ExecJs(primary_rfh.get(), |
| JsReplace(kAddIframeScript, |
| https_server()->GetURL("c.test", "/title1.html")))); |
| RenderFrameHostWrapper iframe( |
| static_cast<RenderFrameHostImpl*>(ChildFrameAt(primary_rfh.get(), 0))); |
| |
| ASSERT_TRUE( |
| NavigateToURL(shell(), https_server()->GetURL("b.test", "/title1.html"))); |
| ASSERT_EQ(primary_rfh->GetLifecycleState(), |
| RenderFrameHost::LifecycleState::kInBackForwardCache); |
| ASSERT_EQ(iframe->GetLifecycleState(), |
| RenderFrameHost::LifecycleState::kInBackForwardCache); |
| |
| mojo::PendingAssociatedRemote<blink::mojom::FencedFrameOwnerHost> remote; |
| mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost> receiver; |
| receiver = remote.InitWithNewEndpointAndPassReceiver(); |
| |
| auto remote_frame_interfaces = |
| blink::mojom::RemoteFrameInterfacesFromRenderer::New(); |
| remote_frame_interfaces->frame_host_receiver = |
| mojo::AssociatedRemote<blink::mojom::RemoteFrameHost>() |
| .BindNewEndpointAndPassDedicatedReceiver(); |
| mojo::AssociatedRemote<blink::mojom::RemoteFrame> frame; |
| std::ignore = frame.BindNewEndpointAndPassDedicatedReceiver(); |
| remote_frame_interfaces->frame = frame.Unbind(); |
| |
| static_cast<RenderFrameHostImpl*>(iframe.get()) |
| ->CreateFencedFrame( |
| std::move(receiver), std::move(remote_frame_interfaces), |
| blink::RemoteFrameToken(), base::UnguessableToken::Create()); |
| EXPECT_TRUE(primary_rfh.WaitUntilRenderFrameDeleted()); |
| EXPECT_TRUE(iframe.IsRenderFrameDeleted()); |
| } |
| |
| // Verify preload from a link element works in fenced frame. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, LinkPreload) { |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Navigate to a page that contains a fenced frame. |
| const GURL main_url = https_server()->GetURL( |
| "a.test", "/cross_site_iframe_factory.html?a.test(a.test{fenced})"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Get fenced frame render frame host. |
| RenderFrameHostImpl* fenced_frame_rfh = |
| primary_main_frame_host()->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| // Set up URLLoaderMonitor. |
| std::string relative_url = "/title1.html"; |
| const GURL preload_url = https_server()->GetURL("a.test", relative_url); |
| URLLoaderMonitor monitor({preload_url}); |
| |
| // Navigate fenced frame to a page with a link element that does a preload. |
| TestFrameNavigationObserver observer(fenced_frame_rfh); |
| EXPECT_TRUE( |
| ExecJs(primary_main_frame_host(), |
| JsReplace( |
| R"(document.querySelector('fencedframe').config |
| = new FencedFrameConfig($1);)", |
| https_server()->GetURL( |
| "a.test", "/fenced_frames/link_rel_preload.html")))); |
| observer.WaitForCommit(); |
| |
| // The preload request is received. It has script resource type. |
| monitor.WaitForUrl(preload_url); |
| std::optional<network::ResourceRequest> request = |
| monitor.GetRequestInfo(preload_url); |
| EXPECT_EQ(request->resource_type, |
| static_cast<int>(blink::mojom::ResourceType::kScript)); |
| } |
| |
| // Verify preload from a link element is disabled after fenced frame network |
| // cutoff. |
| IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, |
| NetworkCutoffDisablesLinkPreload) { |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Navigate to a page that contains a fenced frame. |
| const GURL main_url = https_server()->GetURL( |
| "a.test", "/cross_site_iframe_factory.html?a.test(a.test{fenced})"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Get fenced frame render frame host. |
| RenderFrameHostImpl* fenced_frame_rfh = |
| primary_main_frame_host()->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| // Set up URLLoaderMonitor. |
| std::string relative_url = "/title1.html"; |
| const GURL preload_url = https_server()->GetURL("a.test", relative_url); |
| URLLoaderMonitor monitor({preload_url}); |
| |
| // Navigate fenced frame to a page that disables network access, then adds a |
| // link element that does a preload. |
| TestFrameNavigationObserver observer(fenced_frame_rfh); |
| EXPECT_TRUE( |
| ExecJs(primary_main_frame_host(), |
| JsReplace( |
| R"(document.querySelector('fencedframe').config |
| = new FencedFrameConfig($1);)", |
| https_server()->GetURL( |
| "a.test", |
| "/fenced_frames/link_rel_preload_disable_network.html")))); |
| observer.WaitForCommit(); |
| |
| // The preload request is blocked with code `ERR_NETWORK_ACCESS_REVOKED`. |
| monitor.WaitForUrl(preload_url); |
| EXPECT_EQ(monitor.WaitForRequestCompletion(preload_url).error_code, |
| net::ERR_NETWORK_ACCESS_REVOKED); |
| } |
| |
| class FencedFrameWithSiteIsolationDisabledBrowserTest |
| : public FencedFrameMPArchBrowserTest, |
| public testing::WithParamInterface<std::tuple<bool, bool>> { |
| public: |
| FencedFrameWithSiteIsolationDisabledBrowserTest() { |
| std::vector<base::test::FeatureRef> enabled_features; |
| std::vector<base::test::FeatureRef> disabled_features; |
| |
| if (std::get<0>(GetParam())) { |
| enabled_features.push_back( |
| features::kProcessSharingWithDefaultSiteInstances); |
| disabled_features.push_back( |
| features::kProcessSharingWithStrictSiteInstances); |
| } else { |
| enabled_features.push_back( |
| features::kProcessSharingWithStrictSiteInstances); |
| disabled_features.push_back( |
| features::kProcessSharingWithDefaultSiteInstances); |
| } |
| |
| if (std::get<1>(GetParam())) { |
| enabled_features.push_back(features::kIsolateFencedFrames); |
| } else { |
| disabled_features.push_back(features::kIsolateFencedFrames); |
| } |
| |
| feature_list_.InitWithFeatures(enabled_features, disabled_features); |
| } |
| |
| ~FencedFrameWithSiteIsolationDisabledBrowserTest() override = default; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| FencedFrameMPArchBrowserTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch(switches::kDisableSiteIsolation); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| FencedFrameWithSiteIsolationDisabledBrowserTest, |
| testing::Combine(testing::Bool(), testing::Bool()), |
| [](const testing::TestParamInfo<std::tuple<bool, bool>>& info) { |
| return base::StringPrintf("%s_%s", |
| std::get<0>(info.param) ? "DefaultSiteInstances" |
| : "StrictSiteInstances", |
| std::get<1>(info.param) |
| ? "IsolatedFencedFrames" |
| : "UnisolatedFencedFrames"); |
| }); |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameWithSiteIsolationDisabledBrowserTest, |
| ProcessAllocationWithSiteIsolationDisabled) { |
| ASSERT_TRUE(https_server()->Start()); |
| if (AreAllSitesIsolatedForTesting()) { |
| LOG(ERROR) << "Site isolation should be disabled for this test."; |
| return; |
| } |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL same_site_fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| const GURL cross_site_fenced_frame_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Empty fenced frame document should be in the same process as embedder. |
| RenderFrameHost* fenced_frame_rfh = |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| GURL()); |
| EXPECT_NE(fenced_frame_rfh->GetSiteInstance(), |
| primary_main_frame_host()->GetSiteInstance()); |
| EXPECT_EQ(fenced_frame_rfh->GetProcess(), |
| primary_main_frame_host()->GetProcess()); |
| |
| // Same-site fenced frame document should be in the same process as embedder. |
| fenced_frame_rfh = fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| fenced_frame_rfh, same_site_fenced_frame_url); |
| EXPECT_EQ(fenced_frame_rfh->GetProcess(), |
| primary_main_frame_host()->GetProcess()); |
| |
| // Cross-site fenced frame document should be in the same process as the |
| // embedder (with site isolation disabled). |
| fenced_frame_rfh = fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| fenced_frame_rfh, cross_site_fenced_frame_url); |
| EXPECT_EQ(fenced_frame_rfh->GetProcess(), |
| primary_main_frame_host()->GetProcess()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameWithSiteIsolationDisabledBrowserTest, |
| ProcessAllocationWithDynamicIsolatedOrigin) { |
| ASSERT_TRUE(https_server()->Start()); |
| if (AreAllSitesIsolatedForTesting()) { |
| LOG(ERROR) << "Site isolation should be disabled for this test."; |
| return; |
| } |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL isolated_cross_site_fenced_frame_url = |
| https_server()->GetURL("isolated.b.test", "/fenced_frames/title1.html"); |
| const GURL cross_site_fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Start isolating "isolated.b.test". |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| policy->AddFutureIsolatedOrigins( |
| {url::Origin::Create(isolated_cross_site_fenced_frame_url)}, |
| ChildProcessSecurityPolicy::IsolatedOriginSource::TEST); |
| |
| RenderFrameHost* ff_rfh_1 = fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), cross_site_fenced_frame_url); |
| RenderFrameHost* ff_rfh_2 = fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), isolated_cross_site_fenced_frame_url); |
| |
| // The c.test fenced frame should share a process with the embedder, but |
| // the isolated.b.test fenced frame should be in a different process. |
| EXPECT_EQ(ff_rfh_1->GetProcess(), primary_main_frame_host()->GetProcess()); |
| EXPECT_NE(ff_rfh_2->GetProcess(), ff_rfh_1->GetProcess()); |
| |
| // When we navigate the second fenced frame to c.test, it should now share |
| // its process with the embedder. |
| ff_rfh_2 = fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| ff_rfh_2, cross_site_fenced_frame_url); |
| EXPECT_EQ(ff_rfh_2->GetProcess(), primary_main_frame_host()->GetProcess()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameWithSiteIsolationDisabledBrowserTest, |
| ProcessAllocationWhenRootIsIsolated) { |
| ASSERT_TRUE(https_server()->Start()); |
| if (AreAllSitesIsolatedForTesting()) { |
| LOG(ERROR) << "Site isolation should be disabled for this test."; |
| return; |
| } |
| |
| const GURL isolated_url = |
| https_server()->GetURL("isolated.b.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| const GURL iframe_url = https_server()->GetURL("a.test", "/title1.html"); |
| |
| // Start isolating "isolated.b.test". |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| policy->AddFutureIsolatedOrigins( |
| {url::Origin::Create(isolated_url)}, |
| ChildProcessSecurityPolicy::IsolatedOriginSource::TEST); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), isolated_url)); |
| RenderFrameHost* ff_rfh = fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), GURL()); |
| EXPECT_EQ(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| ff_rfh = fenced_frame_test_helper().NavigateFrameInFencedFrameTree( |
| ff_rfh, fenced_frame_url); |
| EXPECT_NE(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| |
| EXPECT_TRUE(ExecJs(primary_main_frame_host(), |
| JsReplace(kAddIframeScript, iframe_url))); |
| RenderFrameHostImpl* iframe = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(primary_main_frame_host(), 1)); |
| ASSERT_TRUE(iframe && iframe->GetParent()->IsInPrimaryMainFrame()); |
| EXPECT_EQ(iframe->GetProcess(), ff_rfh->GetProcess()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameWithSiteIsolationDisabledBrowserTest, |
| ProcessAllocationForNestedFencedFrame) { |
| ASSERT_TRUE(https_server()->Start()); |
| if (AreAllSitesIsolatedForTesting()) { |
| LOG(ERROR) << "Site isolation should be disabled for this test."; |
| return; |
| } |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHost* ff_rfh = fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), fenced_frame_url); |
| EXPECT_EQ(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| RenderFrameHost* nested_ff_rfh = |
| fenced_frame_test_helper().CreateFencedFrame(ff_rfh, fenced_frame_url); |
| EXPECT_EQ(ff_rfh->GetProcess(), nested_ff_rfh->GetProcess()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameWithSiteIsolationDisabledBrowserTest, |
| ProcessAllocationForFencedFrameInIsolatedPopup) { |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| const GURL popup_url = |
| https_server()->GetURL("isolated.c.test", "/title2.html"); |
| |
| // Start isolating "isolated.c.test". |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| policy->AddFutureIsolatedOrigins( |
| {url::Origin::Create(popup_url)}, |
| ChildProcessSecurityPolicy::IsolatedOriginSource::TEST); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE( |
| ExecJs(primary_main_frame_host(), "popup = window.open('about:blank');")); |
| Shell* popup = new_shell_observer.GetShell(); |
| EXPECT_TRUE(NavigateToURLFromRenderer(popup, popup_url)); |
| RenderFrameHost* popup_rfh = popup->web_contents()->GetPrimaryMainFrame(); |
| ASSERT_EQ( |
| primary_main_frame_host()->GetSiteInstance()->GetBrowsingInstanceId(), |
| popup_rfh->GetSiteInstance()->GetBrowsingInstanceId()); |
| |
| RenderFrameHost* ff_rfh = |
| fenced_frame_test_helper().CreateFencedFrame(popup_rfh, fenced_frame_url); |
| ASSERT_EQ(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| } |
| |
| class FencedFrameIsolatedSandboxedIframesBrowserTest |
| : public FencedFrameMPArchBrowserTest_IsolateAllSites, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| FencedFrameIsolatedSandboxedIframesBrowserTest() { |
| if (GetParam()) { |
| // Run test with both isolation features enabled. |
| feature_list_.InitWithFeatures({blink::features::kIsolateSandboxedIframes, |
| features::kIsolateFencedFrames}, |
| {}); |
| } else { |
| // Run test with only isolated sandboxed iframes enabled. |
| feature_list_.InitWithFeatures( |
| {blink::features::kIsolateSandboxedIframes}, |
| {features::kIsolateFencedFrames}); |
| } |
| } |
| ~FencedFrameIsolatedSandboxedIframesBrowserTest() override = default; |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // This is a basic test to make sure the kIsolateSandboxedIframes (OOPSIF) load |
| // properly with FencedFrames, both when FencedFrames isolation mode is off and |
| // on. The OOPSIF frame is sandboxed due to a CSP sandbox header delivered with |
| // the page loaded into the FencedFrame. The FencedFrame element doesn't support |
| // the 'sandbox' attribute directly, nor can it be loaded inside an OOPSIF since |
| // OOPSIFs by definition disallow same-origin, whereas the FencedFrame element |
| // will only load inside a sandbox if allow-same-origin is specified on the |
| // sandbox. See kFencedFrameMandatoryUnsandboxedFlags. |
| IN_PROC_BROWSER_TEST_P(FencedFrameIsolatedSandboxedIframesBrowserTest, |
| CSP_Mainframe) { |
| bool testing_with_isolate_fenced_frames = GetParam(); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/sandbox_flags.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* ff_rfh = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| EXPECT_TRUE(ff_rfh->IsSandboxed(blink::kFencedFrameForcedSandboxFlags)); |
| |
| EXPECT_NE(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| // This next result may seem weird, but the is_fenced() bit only gets |
| // set if SiteIsolationPolicy::IsProcessIsolationForFencedFramesEnabled() |
| // returns true in SiteInstanceImpl::CreateForFencedFrame(). |
| // The bit is picked up from the fenced frame's BrowsingInstance's |
| // IsolationContext when the SiteInfo is created. |
| EXPECT_EQ(testing_with_isolate_fenced_frames, |
| ff_rfh->GetSiteInstance()->GetSiteInfo().is_fenced()); |
| EXPECT_TRUE(ff_rfh->GetSiteInstance()->GetSiteInfo().is_sandboxed()); |
| EXPECT_NE( |
| primary_main_frame_host()->GetSiteInstance()->GetBrowsingInstanceId(), |
| ff_rfh->GetSiteInstance()->GetBrowsingInstanceId()); |
| } |
| |
| // Similar to CSP_Mainframe, but in this test OOPSIF doesn't isolate the fenced |
| // frames, while kIsolateFencedFrames does. |
| IN_PROC_BROWSER_TEST_P(FencedFrameIsolatedSandboxedIframesBrowserTest, |
| Non_CSP_Mainframe) { |
| bool testing_with_isolate_fenced_frames = GetParam(); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* ff_rfh = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| EXPECT_TRUE(ff_rfh->IsSandboxed(blink::kFencedFrameForcedSandboxFlags)); |
| |
| EXPECT_EQ(testing_with_isolate_fenced_frames, |
| ff_rfh->GetSiteInstance()->GetSiteInfo().is_fenced()); |
| if (testing_with_isolate_fenced_frames) { |
| EXPECT_NE(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| } else { |
| EXPECT_EQ(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| } |
| EXPECT_FALSE(ff_rfh->GetSiteInstance()->GetSiteInfo().is_sandboxed()); |
| EXPECT_NE( |
| primary_main_frame_host()->GetSiteInstance()->GetBrowsingInstanceId(), |
| ff_rfh->GetSiteInstance()->GetBrowsingInstanceId()); |
| } |
| |
| // A test to confirm that a FencedFrame fails to create inside a CSP sandbox |
| // frame without allow-same-origin. This test should fail regardless of the |
| // state of kIsolateSandboxedIframes or kIsolateFencedFrames. |
| IN_PROC_BROWSER_TEST_P(FencedFrameIsolatedSandboxedIframesBrowserTest, |
| NoFencedFramesInIsolatedSandboxedIframes) { |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Load CSP sandboxed frame as mainframe. |
| const GURL main_url = |
| https_server()->GetURL("a.test", "/fenced_frames/sandbox_flags.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| EXPECT_TRUE(primary_main_frame_host() |
| ->GetSiteInstance() |
| ->GetSiteInfo() |
| .is_sandboxed()); |
| |
| // Try to load FencedFrame inside the CSP sandboxed frame. |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| // Extracted from CreateFencedFrame, which doesn't expect to fail. |
| constexpr char kAddFencedFrameScript[] = R"({ |
| const fenced_frame = document.createElement('fencedframe'); |
| document.body.appendChild(fenced_frame); |
| })"; |
| size_t previous_fenced_frame_count = |
| primary_main_frame_host()->GetFencedFrames().size(); |
| EXPECT_EQ(0U, previous_fenced_frame_count); |
| // The following attempt to create a fenced frame is expected to fail since |
| // it would otherwise be contained in a sandbox that doesn't have the |
| // allow-same-origin attribute. See kFencedFrameMandatoryUnsandboxedFlags. |
| EXPECT_FALSE(ExecJs(primary_main_frame_host(), kAddFencedFrameScript, |
| EvalJsOptions::EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| EXPECT_EQ(previous_fenced_frame_count, |
| primary_main_frame_host()->GetFencedFrames().size()); |
| } |
| |
| class FencedFrameProcessIsolationBrowserTest |
| : public FencedFrameMPArchBrowserTest { |
| public: |
| FencedFrameProcessIsolationBrowserTest() { |
| feature_list_.InitWithFeatures({features::kIsolateFencedFrames}, {}); |
| } |
| ~FencedFrameProcessIsolationBrowserTest() override = default; |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameProcessIsolationBrowserTest, BasicTest) { |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* ff_rfh = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| EXPECT_TRUE(ff_rfh->GetSiteInstance()->GetSiteInfo().is_fenced()); |
| EXPECT_NE(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| EXPECT_NE( |
| primary_main_frame_host()->GetSiteInstance()->GetBrowsingInstanceId(), |
| ff_rfh->GetSiteInstance()->GetBrowsingInstanceId()); |
| } |
| |
| // Tests that fenced frames that are same-origin with each other are put in |
| // the same process. |
| IN_PROC_BROWSER_TEST_F(FencedFrameProcessIsolationBrowserTest, |
| SameOriginFencedFramesArePutInTheSameProcess) { |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* ff_rfh_1 = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| RenderFrameHostImpl* ff_rfh_2 = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| |
| EXPECT_NE(ff_rfh_1->GetProcess(), primary_main_frame_host()->GetProcess()); |
| EXPECT_NE(ff_rfh_1->GetSiteInstance(), ff_rfh_2->GetSiteInstance()); |
| EXPECT_EQ(ff_rfh_1->GetProcess(), ff_rfh_2->GetProcess()); |
| } |
| |
| // Tests that fenced frames that are cross-origin with each other are put in |
| // different processes. |
| IN_PROC_BROWSER_TEST_F(FencedFrameProcessIsolationBrowserTest, |
| CrossOriginFencedFramesArePutInDifferentProcesses) { |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL ff_url_1 = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| const GURL ff_url_2 = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* ff_rfh_1 = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| ff_url_1)); |
| RenderFrameHostImpl* ff_rfh_2 = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| ff_url_2)); |
| |
| EXPECT_NE(ff_rfh_1->GetProcess(), primary_main_frame_host()->GetProcess()); |
| EXPECT_NE(ff_rfh_2->GetProcess(), primary_main_frame_host()->GetProcess()); |
| EXPECT_NE(ff_rfh_1->GetProcess(), ff_rfh_2->GetProcess()); |
| } |
| |
| // Tests that a subframe inside a primary page is allocated to a separate |
| // process from a subframe inside a fenced frame. |
| IN_PROC_BROWSER_TEST_F(FencedFrameProcessIsolationBrowserTest, |
| SubframeIsolation) { |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL ff_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| const GURL subframe_url = https_server()->GetURL("c.test", "/title2.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* ff_rfh = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| ff_url)); |
| |
| // Add iframe in primary main frame. |
| EXPECT_TRUE(ExecJs(primary_main_frame_host(), |
| JsReplace(kAddIframeScript, subframe_url))); |
| RenderFrameHost* primary_subframe = |
| ChildFrameAt(primary_main_frame_host(), 0); |
| |
| // Add iframe in fenced frame. |
| EXPECT_TRUE(ExecJs(ff_rfh, JsReplace(kAddIframeScript, subframe_url))); |
| RenderFrameHost* ff_subframe = ChildFrameAt(ff_rfh, 0); |
| |
| // Both subframes should be in separate processes (despite being same-site). |
| EXPECT_NE(primary_subframe->GetProcess(), ff_subframe->GetProcess()); |
| EXPECT_NE(primary_subframe->GetSiteInstance(), |
| ff_subframe->GetSiteInstance()); |
| EXPECT_FALSE(static_cast<RenderFrameHostImpl*>(primary_subframe) |
| ->GetSiteInstance() |
| ->GetSiteInfo() |
| .is_fenced()); |
| EXPECT_TRUE(static_cast<RenderFrameHostImpl*>(ff_subframe) |
| ->GetSiteInstance() |
| ->GetSiteInfo() |
| .is_fenced()); |
| } |
| |
| // Tests process assignment in the following scenario: |
| // a.com |
| // <fencedframe src=a.com> |
| // <iframe src=a.com> |
| // <fencedframe src=a.com> |
| IN_PROC_BROWSER_TEST_F(FencedFrameProcessIsolationBrowserTest, |
| NestedFencedFrames) { |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL subframe_url = https_server()->GetURL("a.test", "/title2.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Create outer fenced frame and add same-origin subframe. |
| RenderFrameHostImpl* outer_ff_rfh = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(), |
| fenced_frame_url)); |
| EXPECT_TRUE(ExecJs(outer_ff_rfh, JsReplace(kAddIframeScript, subframe_url))); |
| RenderFrameHost* outer_ff_subframe = ChildFrameAt(outer_ff_rfh, 0); |
| |
| // Create nested fenced frame. |
| RenderFrameHostImpl* inner_ff_rfh = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(outer_ff_subframe, |
| fenced_frame_url)); |
| |
| EXPECT_EQ(outer_ff_rfh->GetSiteInstance(), |
| outer_ff_subframe->GetSiteInstance()); |
| EXPECT_NE(outer_ff_subframe->GetSiteInstance(), |
| inner_ff_rfh->GetSiteInstance()); |
| |
| // All frames will share the same process (except the primary main frame). |
| EXPECT_NE(primary_main_frame_host()->GetProcess(), |
| outer_ff_rfh->GetProcess()); |
| EXPECT_EQ(outer_ff_rfh->GetProcess(), outer_ff_subframe->GetProcess()); |
| EXPECT_EQ(outer_ff_subframe->GetProcess(), inner_ff_rfh->GetProcess()); |
| } |
| |
| // Tests that error pages inside fenced frames are process-isolated from the |
| // embedding page. |
| IN_PROC_BROWSER_TEST_F(FencedFrameProcessIsolationBrowserTest, ErrorPage) { |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(AreAllSitesIsolatedForTesting()); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/title2.html"); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Loading the fenced frame should fail due to the absence of a |
| // "Supports-Loading-Mode" header. |
| RenderFrameHostImpl* ff_rfh = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), fenced_frame_url, net::ERR_ABORTED)); |
| ASSERT_NE(ff_rfh, nullptr); |
| EXPECT_TRUE(ff_rfh->IsErrorDocument()); |
| |
| const SiteInfo& ff_site_info = ff_rfh->GetSiteInstance()->GetSiteInfo(); |
| EXPECT_TRUE(ff_site_info.is_error_page()); |
| EXPECT_TRUE(ff_site_info.is_fenced()); |
| |
| EXPECT_NE(ff_rfh->GetProcess(), primary_main_frame_host()->GetProcess()); |
| |
| // Create another fenced frame that loads an error page. |
| RenderFrameHostImpl* ff_rfh_2 = static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), fenced_frame_url, net::ERR_ABORTED)); |
| ASSERT_NE(ff_rfh_2, nullptr); |
| // Both fenced frame error pages should share a process. |
| EXPECT_EQ(ff_rfh_2->GetProcess(), ff_rfh->GetProcess()); |
| } |
| |
| namespace { |
| |
| enum class FrameTypeWithOrigin { |
| kSameOriginIframe, |
| kCrossOriginIframe, |
| kSameOriginFencedFrame, |
| kCrossOriginFencedFrame, |
| }; |
| |
| const std::vector<FrameTypeWithOrigin> kTestParameters[] = { |
| {}, |
| |
| {FrameTypeWithOrigin::kSameOriginIframe}, |
| {FrameTypeWithOrigin::kCrossOriginIframe}, |
| {FrameTypeWithOrigin::kSameOriginIframe, |
| FrameTypeWithOrigin::kSameOriginIframe}, |
| {FrameTypeWithOrigin::kSameOriginIframe, |
| FrameTypeWithOrigin::kCrossOriginIframe}, |
| {FrameTypeWithOrigin::kCrossOriginIframe, |
| FrameTypeWithOrigin::kSameOriginIframe}, |
| {FrameTypeWithOrigin::kCrossOriginIframe, |
| FrameTypeWithOrigin::kCrossOriginIframe}, |
| |
| {FrameTypeWithOrigin::kSameOriginFencedFrame}, |
| {FrameTypeWithOrigin::kCrossOriginFencedFrame}, |
| {FrameTypeWithOrigin::kSameOriginFencedFrame, |
| FrameTypeWithOrigin::kSameOriginIframe}, |
| {FrameTypeWithOrigin::kSameOriginFencedFrame, |
| FrameTypeWithOrigin::kCrossOriginIframe}, |
| {FrameTypeWithOrigin::kCrossOriginFencedFrame, |
| FrameTypeWithOrigin::kSameOriginIframe}, |
| {FrameTypeWithOrigin::kCrossOriginFencedFrame, |
| FrameTypeWithOrigin::kCrossOriginIframe}}; |
| |
| static std::string TestParamToString( |
| ::testing::TestParamInfo<std::vector<FrameTypeWithOrigin>> param_info) { |
| std::string out = "Top_"; |
| for (const auto& frame_type : param_info.param) { |
| switch (frame_type) { |
| case FrameTypeWithOrigin::kSameOriginIframe: |
| out += "SameI_"; |
| break; |
| case FrameTypeWithOrigin::kCrossOriginIframe: |
| out += "CrossI_"; |
| break; |
| case FrameTypeWithOrigin::kSameOriginFencedFrame: |
| out += "SameF_"; |
| break; |
| case FrameTypeWithOrigin::kCrossOriginFencedFrame: |
| out += "CrossF_"; |
| break; |
| } |
| } |
| return out; |
| } |
| |
| const char* kSameOriginHostName = "a.test"; |
| const char* kCrossOriginHostName = "b.test"; |
| |
| const char* GetHostNameForFrameType(FrameTypeWithOrigin type) { |
| switch (type) { |
| case FrameTypeWithOrigin::kSameOriginIframe: |
| return kSameOriginHostName; |
| case FrameTypeWithOrigin::kCrossOriginIframe: |
| return kCrossOriginHostName; |
| case FrameTypeWithOrigin::kSameOriginFencedFrame: |
| return kSameOriginHostName; |
| case FrameTypeWithOrigin::kCrossOriginFencedFrame: |
| return kCrossOriginHostName; |
| } |
| } |
| |
| bool IsFencedFrameType(FrameTypeWithOrigin type) { |
| switch (type) { |
| case FrameTypeWithOrigin::kSameOriginIframe: |
| return false; |
| case FrameTypeWithOrigin::kCrossOriginIframe: |
| return false; |
| case FrameTypeWithOrigin::kSameOriginFencedFrame: |
| return true; |
| case FrameTypeWithOrigin::kCrossOriginFencedFrame: |
| return true; |
| } |
| } |
| |
| } // namespace |
| |
| class FencedFrameNestedFrameBrowserTest |
| : public FencedFrameBrowserTestBase, |
| public testing::WithParamInterface<std::vector<FrameTypeWithOrigin>> { |
| protected: |
| FencedFrameNestedFrameBrowserTest() = default; |
| |
| RenderFrameHostImpl* LoadNestedFrame() { |
| const GURL main_url = |
| https_server()->GetURL(kSameOriginHostName, "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* frame = static_cast<RenderFrameHostImpl*>( |
| web_contents()->GetPrimaryMainFrame()); |
| int depth = 0; |
| for (const auto& type : GetParam()) { |
| ++depth; |
| frame = CreateFrame(frame, type, depth); |
| } |
| return frame; |
| } |
| |
| bool IsInFencedFrameTest() { |
| for (const auto& type : GetParam()) { |
| if (IsFencedFrameType(type)) |
| return true; |
| } |
| return false; |
| } |
| |
| private: |
| RenderFrameHostImpl* CreateFrame(RenderFrameHostImpl* parent, |
| FrameTypeWithOrigin type, |
| int depth) { |
| const GURL url = https_server()->GetURL( |
| GetHostNameForFrameType(type), |
| "/fenced_frames/title1.html?depth=" + base::NumberToString(depth)); |
| |
| if (IsFencedFrameType(type)) { |
| return static_cast<RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame(parent, url)); |
| } |
| EXPECT_TRUE(ExecJs(parent, JsReplace(kAddIframeScript, url))); |
| |
| return static_cast<RenderFrameHostImpl*>(ChildFrameAt(parent, 0)); |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameNestedFrameBrowserTest, |
| IsNestedWithinFencedFrame) { |
| RenderFrameHostImpl* rfh = LoadNestedFrame(); |
| EXPECT_EQ(IsInFencedFrameTest(), rfh->IsNestedWithinFencedFrame()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(FencedFrameNestedFrameBrowserTest, |
| FencedFrameNestedFrameBrowserTest, |
| testing::ValuesIn(kTestParameters), |
| TestParamToString); |
| |
| namespace { |
| |
| static std::string ModeTestParamToString( |
| ::testing::TestParamInfo< |
| std::tuple<blink::FencedFrame::DeprecatedFencedFrameMode, |
| blink::FencedFrame::DeprecatedFencedFrameMode>> param_info) { |
| std::string out = "ParentMode"; |
| switch (std::get<0>(param_info.param)) { |
| case blink::FencedFrame::DeprecatedFencedFrameMode::kDefault: |
| out += "Default"; |
| break; |
| case blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds: |
| out += "OpaqueAds"; |
| break; |
| } |
| |
| out += "_ChildMode"; |
| |
| switch (std::get<1>(param_info.param)) { |
| case blink::FencedFrame::DeprecatedFencedFrameMode::kDefault: |
| out += "Default"; |
| break; |
| case blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds: |
| out += "OpaqueAds"; |
| break; |
| } |
| |
| return out; |
| } |
| |
| } // namespace |
| |
| class FencedFrameNestedModesTest |
| : public FencedFrameBrowserTestBase, |
| public testing::WithParamInterface< |
| std::tuple<blink::FencedFrame::DeprecatedFencedFrameMode, |
| blink::FencedFrame::DeprecatedFencedFrameMode>> { |
| protected: |
| FencedFrameNestedModesTest() { |
| // TODO(domfarolino): Maybe we don't need this? |
| feature_list_.InitWithFeaturesAndParameters( |
| {{blink::features::kFencedFrames, {}}, |
| {features::kPrivacySandboxAdsAPIsOverride, {}}}, |
| {/* disabled_features */}); |
| } |
| |
| blink::FencedFrame::DeprecatedFencedFrameMode GetParentMode() { |
| return std::get<0>(GetParam()); |
| } |
| blink::FencedFrame::DeprecatedFencedFrameMode GetChildMode() { |
| return std::get<1>(GetParam()); |
| } |
| |
| std::string GetParentModeStr() { |
| return ModeToString(std::get<0>(GetParam())); |
| } |
| std::string GetChildModeStr() { |
| return ModeToString(std::get<1>(GetParam())); |
| } |
| |
| base::HistogramTester histogram_tester_; |
| |
| private: |
| std::string ModeToString(blink::FencedFrame::DeprecatedFencedFrameMode mode) { |
| switch (mode) { |
| case blink::FencedFrame::DeprecatedFencedFrameMode::kDefault: |
| return "default"; |
| case blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds: |
| return "opaque-ads"; |
| } |
| |
| NOTREACHED_IN_MIGRATION(); |
| return ""; |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // This test runs the following steps: |
| // 1.) Creates a "parent" fenced frame with a particular mode |
| // 2.) Creates a nested/child fenced frame with a particular mode |
| // 3.) Asserts that creation of the child fenced frame either failed or |
| // succeeded depending on its mode. |
| IN_PROC_BROWSER_TEST_P(FencedFrameNestedModesTest, NestedModes) { |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| constexpr char kAddFencedFrameScript[] = R"({ |
| const fenced_frame = document.createElement('fencedframe'); |
| fenced_frame.config = new FencedFrameConfig($2); |
| fenced_frame.mode = $1; |
| document.body.appendChild(fenced_frame); |
| })"; |
| |
| // Because FencedFrameTestHelper::CreateFencedFrame doesn't yet do an |
| // embedder-initiated navigation for default mode, we have to manually |
| // perform the navigation in JS. |
| if (GetParentMode() == |
| blink::FencedFrame::DeprecatedFencedFrameMode::kDefault) { |
| EXPECT_TRUE(ExecJs( |
| primary_main_frame_host(), |
| JsReplace(kAddFencedFrameScript, GetParentMode(), fenced_frame_url))); |
| } else { |
| std::ignore = fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), fenced_frame_url, net::OK, GetParentMode()); |
| } |
| |
| // Wait for page to load in order to have it know it's in a secure context. |
| WaitForLoadStop(web_contents()); |
| |
| // Get the fenced frame's RFH. |
| ASSERT_EQ(1u, primary_main_frame_host()->child_count()); |
| RenderFrameHostImpl* parent_fenced_frame_rfh = |
| primary_main_frame_host()->child_at(0)->current_frame_host(); |
| int inner_node_id = |
| parent_fenced_frame_rfh->inner_tree_main_frame_tree_node_id(); |
| parent_fenced_frame_rfh = |
| FrameTreeNode::GloballyFindByID(inner_node_id)->current_frame_host(); |
| ASSERT_TRUE(parent_fenced_frame_rfh); |
| |
| // 2.) Attempt to create the child fenced frame. |
| if (GetChildMode() == |
| blink::FencedFrame::DeprecatedFencedFrameMode::kDefault) { |
| EXPECT_TRUE(ExecJs( |
| parent_fenced_frame_rfh, |
| JsReplace(kAddFencedFrameScript, GetChildMode(), fenced_frame_url))); |
| } else { |
| std::ignore = fenced_frame_test_helper().CreateFencedFrame( |
| parent_fenced_frame_rfh, fenced_frame_url, net::OK, GetChildMode(), |
| /*wait_for_load=*/false); |
| } |
| |
| // 3.) Assert that the child fenced frame was created or not created depending |
| // on the test parameters. |
| content::FetchHistogramsFromChildProcesses(); |
| // Child fenced frame creation should have succeeded unconditionally. |
| EXPECT_EQ(1u, parent_fenced_frame_rfh->child_count()); |
| if (GetParentMode() != GetChildMode()) { |
| // Child fenced frame navigation should have failed based on its mode. |
| histogram_tester_.ExpectTotalCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, 2); |
| histogram_tester_.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kIncompatibleMode, 1); |
| } else { |
| // Child fenced frame navigation should have succeeded because its mode is |
| // the same as its parent. |
| histogram_tester_.ExpectTotalCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, 2); |
| if (GetChildMode() == |
| blink::FencedFrame::DeprecatedFencedFrameMode::kDefault) { |
| histogram_tester_.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kSuccessDefault, 2); |
| } else { |
| histogram_tester_.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kSuccessOpaque, 2); |
| } |
| } |
| } |
| |
| class FledgeFencedFrameOriginContentBrowserClient |
| : public ContentBrowserTestContentBrowserClient { |
| public: |
| explicit FledgeFencedFrameOriginContentBrowserClient() = default; |
| |
| FledgeFencedFrameOriginContentBrowserClient( |
| const FledgeFencedFrameOriginContentBrowserClient&) = delete; |
| FledgeFencedFrameOriginContentBrowserClient& operator=( |
| const FledgeFencedFrameOriginContentBrowserClient&) = delete; |
| |
| // ContentBrowserClient overrides: |
| // This is needed so that the interest group related APIs can run without |
| // failing with the result AuctionResult::kSellerRejected. |
| bool IsInterestGroupAPIAllowed( |
| content::RenderFrameHost* render_frame_host, |
| ContentBrowserClient::InterestGroupApiOperation operation, |
| const url::Origin& top_frame_origin, |
| const url::Origin& api_origin) override { |
| return true; |
| } |
| |
| bool IsPrivacySandboxReportingDestinationAttested( |
| content::BrowserContext* browser_context, |
| const url::Origin& destination_origin, |
| content::PrivacySandboxInvokingAPI invoking_api, |
| bool post_impression_reporting) override { |
| return true; |
| } |
| |
| bool AreDeprecatedAutomaticBeaconCredentialsAllowed( |
| content::BrowserContext* browser_context, |
| const GURL& destination_url, |
| const url::Origin& top_frame_origin) override { |
| return allow_automatic_beacon_credentials_; |
| } |
| |
| void SetAllowAutomaticBeaconCredentials(bool allowed) { |
| allow_automatic_beacon_credentials_ = allowed; |
| } |
| |
| private: |
| bool allow_automatic_beacon_credentials_ = true; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| FencedFrameNestedModesTest, |
| FencedFrameNestedModesTest, |
| testing::Combine( |
| /*parent mode=*/testing::Values( |
| blink::FencedFrame::DeprecatedFencedFrameMode::kDefault, |
| blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds), |
| /*child mode=*/ |
| testing::Values( |
| blink::FencedFrame::DeprecatedFencedFrameMode::kDefault, |
| blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds)), |
| ModeTestParamToString); |
| |
| // TODO(domfarolino): Rename this. |
| class FencedFrameParameterizedBrowserTest : public FencedFrameBrowserTestBase { |
| public: |
| FencedFrameParameterizedBrowserTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| {{blink::features::kFencedFrames, {}}, |
| {net::features::kThirdPartyStoragePartitioning, {}}, |
| {features::kPrivacySandboxAdsAPIsOverride, {}}, |
| {blink::features::kInterestGroupStorage, {}}, |
| {blink::features::kAdInterestGroupAPI, {}}, |
| {blink::features::kParakeet, {}}, |
| {blink::features::kFledge, {}}, |
| {blink::features::kAllowURNsInIframes, {}}, |
| {blink::features::kDisplayWarningDeprecateURNIframesUseFencedFrames, |
| {}}, |
| {blink::features::kBiddingAndScoringDebugReportingAPI, {}}, |
| {features::kBackForwardCache, {}}, |
| // This feature allows `runAdAuction()`'s promise to resolve to a |
| // `FencedFrameConfig` object upon developer request. |
| {blink::features::kFencedFramesAPIChanges, {}}, |
| {blink::features::kFencedFramesM120FeaturesPart1, {}}, |
| {blink::features::kFencedFramesAutomaticBeaconCredentials, {}}, |
| {blink::features::kFencedFramesM120FeaturesPart2, {}}, |
| {blink::features::kFencedFramesReportingAttestationsChanges, {}}, |
| {blink::features::kFencedFramesLocalUnpartitionedDataAccess, {}}, |
| {blink::features:: |
| kFencedFramesCrossOriginEventReportingUnlabeledTraffic, |
| {}}, |
| {blink::features::kFencedFramesReportEventHeaderChanges, {}}}, |
| {/* disabled_features */}); |
| } |
| |
| // 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, |
| 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->IsNestedWithinFencedFrame()); |
| 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->IsNestedWithinFencedFrame()); |
| |
| 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, 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.config = new FencedFrameConfig($1);", url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| nested_fenced_frame, navigate_script); |
| } |
| |
| // 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(base::Contains(cookie_headers_map_, file_name)); |
| 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(base::Contains(sec_fetch_dest_headers_map_, file_name)); |
| 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(); |
| } |
| |
| void SetAllowAutomaticBeaconCredentials(bool allowed) { |
| content_browser_client_->SetAllowAutomaticBeaconCredentials(allowed); |
| } |
| |
| void VerifyFencedFrameNetworkStatus(ToRenderFrameHost frame, |
| DisableUntrustedNetworkStatus status) { |
| std::optional<FencedFrameProperties> props = |
| static_cast<RenderFrameHostImpl*>(frame.render_frame_host()) |
| ->frame_tree_node() |
| ->GetFencedFrameProperties(); |
| CHECK(props.has_value()); |
| |
| bool expected_current_frame_status = false; |
| bool expected_nested_frame_status = false; |
| |
| if (status == DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete) { |
| expected_current_frame_status = true; |
| } else if (status == DisableUntrustedNetworkStatus:: |
| kCurrentAndDescendantFrameTreesComplete) { |
| expected_current_frame_status = true; |
| expected_nested_frame_status = true; |
| } |
| |
| EXPECT_EQ(props->HasDisabledNetworkForCurrentFrameTree(), |
| expected_current_frame_status); |
| EXPECT_EQ(props->HasDisabledNetworkForCurrentAndDescendantFrameTrees(), |
| expected_nested_frame_status); |
| } |
| |
| ~FencedFrameParameterizedBrowserTest() 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()); |
| } |
| } |
| |
| private: |
| void AdditionalSetup() override { |
| https_server()->RegisterRequestMonitor(base::BindRepeating( |
| &FencedFrameParameterizedBrowserTest::ObserveRequestHeaders, |
| base::Unretained(this))); |
| content_browser_client_ = |
| std::make_unique<FledgeFencedFrameOriginContentBrowserClient>(); |
| } |
| |
| 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_); |
| std::unique_ptr<FledgeFencedFrameOriginContentBrowserClient> |
| content_browser_client_; |
| }; |
| |
| // Tests that the fenced frame gets navigated to an actual url given a urn:uuid. |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| CheckFencedFrameNavigationWithUUID) { |
| base::HistogramTester histogram_tester; |
| 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(); |
| histogram_tester.ExpectTotalCount( |
| "Navigation.BrowserMappedUrnUuidInIframeOrFencedFrame", 0); |
| |
| { |
| 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)); |
| |
| 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(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url); |
| |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid); |
| |
| WebContentsConsoleObserver console_error_observer(shell()->web_contents()); |
| auto error_filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kError; |
| }; |
| console_error_observer.SetFilter(base::BindRepeating(error_filter)); |
| console_error_observer.SetPattern("Supports-Loading-Mode*"); |
| |
| { |
| TestFrameNavigationObserver navigation_observer(fenced_frame_root_node); |
| WebContentsConsoleObserver console_observer(web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == |
| blink::mojom::ConsoleMessageLevel::kWarning; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern( |
| "Protected Audience/selectURL will deprecate supporting iframes to " |
| "render the winning ad*"); |
| EXPECT_TRUE(ExecJs(root, navigate_urn_script)); |
| navigation_observer.WaitForCommit(); |
| // No console warning is emitted for urn::uuid navigation in fenced frames. |
| EXPECT_TRUE(console_observer.messages().empty()); |
| } |
| |
| content::FetchHistogramsFromChildProcesses(); |
| histogram_tester.ExpectTotalCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, 1); |
| // Fenced frame creation succeeded (opaque ads mode) |
| histogram_tester.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kSuccessOpaque, 1); |
| // Fenced frame navigation succeeded, no response header opted-in error |
| histogram_tester.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kResponseHeaderNotOptIn, 0); |
| histogram_tester.ExpectBucketCount( |
| "Navigation.BrowserMappedUrnUuidInIframeOrFencedFrame", 0, 1); |
| 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()); |
| // Fenced frame navigation with opt-in 'Supports-Loading-Mode: fenced-frame' |
| // should not emit console errors. |
| EXPECT_TRUE(console_error_observer.messages().empty()); |
| |
| // 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_F(FencedFrameParameterizedBrowserTest, |
| SharedStorageMetadataInNestedFencedFrame) { |
| 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(); |
| |
| FencedFrameURLMapping& url_mapping1 = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| auto urn_uuid1 = GenerateAndVerifyPendingMappedURN(&url_mapping1); |
| const GURL mapped_url1 = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| SimulateSharedStorageURNMappingComplete( |
| url_mapping1, urn_uuid1, mapped_url1, |
| /*shared_storage_site=*/ |
| net::SchemefulSite::Deserialize("https://foo.com"), |
| /*budget_to_charge=*/2.0); |
| |
| EXPECT_TRUE(ExecJs(root, |
| "var f1 = document.createElement('fencedframe');" |
| "document.body.appendChild(f1);")); |
| |
| EXPECT_EQ(1U, root->child_count()); |
| FrameTreeNode* fenced_frame_root_node1 = |
| GetFencedFrameRootNode(root->child_at(0)); |
| |
| TestFrameNavigationObserver observer1( |
| fenced_frame_root_node1->current_frame_host()); |
| std::string navigate_urn_script1 = |
| JsReplace("f1.config = new FencedFrameConfig($1);", urn_uuid1); |
| EXPECT_TRUE(ExecJs(root, navigate_urn_script1)); |
| observer1.Wait(); |
| |
| FencedFrameURLMapping& url_mapping2 = |
| fenced_frame_root_node1->current_frame_host() |
| ->GetPage() |
| .fenced_frame_urls_map(); |
| auto urn_uuid2 = GenerateAndVerifyPendingMappedURN(&url_mapping2); |
| const GURL mapped_url2 = |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html"); |
| SimulateSharedStorageURNMappingComplete( |
| url_mapping2, urn_uuid2, mapped_url2, |
| /*shared_storage_site=*/ |
| net::SchemefulSite::Deserialize("https://bar.com"), |
| /*budget_to_charge=*/3.0); |
| |
| EXPECT_TRUE(ExecJs(fenced_frame_root_node1, |
| "var f2 = document.createElement('fencedframe');" |
| "document.body.appendChild(f2);")); |
| |
| EXPECT_EQ(1U, fenced_frame_root_node1->child_count()); |
| FrameTreeNode* fenced_frame_root_node2 = |
| GetFencedFrameRootNode(fenced_frame_root_node1->child_at(0)); |
| |
| TestFrameNavigationObserver observer2( |
| fenced_frame_root_node2->current_frame_host()); |
| std::string navigate_urn_script2 = |
| JsReplace("f2.config = new FencedFrameConfig($1);", urn_uuid2); |
| EXPECT_TRUE(ExecJs(fenced_frame_root_node1, navigate_urn_script2)); |
| observer2.Wait(); |
| |
| auto metadata = fenced_frame_root_node2->FindSharedStorageBudgetMetadata(); |
| |
| EXPECT_EQ(metadata.size(), 2u); |
| |
| EXPECT_EQ(metadata[0]->site, |
| net::SchemefulSite::Deserialize("https://bar.com")); |
| EXPECT_DOUBLE_EQ(metadata[0]->budget_to_charge, 3.0); |
| |
| EXPECT_EQ(metadata[1]->site, |
| net::SchemefulSite::Deserialize("https://foo.com")); |
| EXPECT_DOUBLE_EQ(metadata[1]->budget_to_charge, 2.0); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| 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');" |
| "document.body.appendChild(f1);")); |
| |
| EXPECT_TRUE(ExecJs(root, |
| "var f2 = document.createElement('fencedframe');" |
| "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(); |
| auto urn_uuid = GenerateAndVerifyPendingMappedURN(&url_mapping); |
| const GURL mapped_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| SimulateSharedStorageURNMappingComplete( |
| url_mapping, urn_uuid, mapped_url, |
| /*shared_storage_site=*/ |
| net::SchemefulSite::Deserialize("https://bar.com"), |
| /*budget_to_charge=*/2.0); |
| |
| { |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node1->current_frame_host()); |
| std::string navigate_urn_script = |
| JsReplace("f1.config = new FencedFrameConfig($1);", urn_uuid); |
| EXPECT_TRUE(ExecJs(root, navigate_urn_script)); |
| observer.Wait(); |
| } |
| |
| { |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node2->current_frame_host()); |
| std::string navigate_urn_script = |
| JsReplace("f2.config = new FencedFrameConfig($1);", urn_uuid); |
| EXPECT_TRUE(ExecJs(root, navigate_urn_script)); |
| observer.Wait(); |
| } |
| |
| EXPECT_EQ(fenced_frame_root_node1->FindSharedStorageBudgetMetadata().size(), |
| 1u); |
| |
| 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_F( |
| FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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(); |
| FencedFrameURLMappingTestPeer url_mapping_test_peer(&url_mapping); |
| |
| auto urn_uuid = GenerateAndVerifyPendingMappedURN(&url_mapping); |
| const GURL mapped_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid); |
| |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node->current_frame_host()); |
| |
| EXPECT_TRUE(ExecJs(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_test_peer.HasObserver(urn_uuid, request)); |
| |
| auto budget_metadata = |
| fenced_frame_root_node->FindSharedStorageBudgetMetadata(); |
| EXPECT_EQ(budget_metadata.size(), 0u); |
| |
| // Trigger the mapping to resume the deferred navigation. |
| SimulateSharedStorageURNMappingComplete( |
| url_mapping, urn_uuid, mapped_url, |
| /*shared_storage_site=*/ |
| net::SchemefulSite::Deserialize("https://bar.com"), |
| /*budget_to_charge=*/2.0); |
| |
| EXPECT_FALSE(url_mapping_test_peer.HasObserver(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_EQ(budget_metadata.size(), 1u); |
| EXPECT_EQ(budget_metadata[0]->site, |
| net::SchemefulSite::Deserialize("https://bar.com")); |
| EXPECT_DOUBLE_EQ(budget_metadata[0]->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_F( |
| FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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(); |
| FencedFrameURLMappingTestPeer url_mapping_test_peer(&url_mapping); |
| |
| auto urn_uuid = GenerateAndVerifyPendingMappedURN(&url_mapping); |
| const GURL mapped_url = |
| https_server()->GetURL("a.test", "/fenced_frames/nonexistent-url.html"); |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid); |
| |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node->current_frame_host()); |
| |
| EXPECT_TRUE(ExecJs(root, navigate_urn_script)); |
| |
| // After the previous ExecJs, 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_test_peer.HasObserver(urn_uuid, request)); |
| |
| // Trigger the mapping to resume the deferred navigation. |
| SimulateSharedStorageURNMappingComplete( |
| url_mapping, urn_uuid, mapped_url, |
| /*shared_storage_site=*/ |
| net::SchemefulSite::Deserialize("https://bar.com"), |
| /*budget_to_charge=*/2.0); |
| |
| EXPECT_FALSE(url_mapping_test_peer.HasObserver(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); |
| |
| // Despite the error, the budget metadata should be valid. |
| auto metadata = fenced_frame_root_node->FindSharedStorageBudgetMetadata(); |
| EXPECT_EQ(metadata.size(), 1u); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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(); |
| FencedFrameURLMappingTestPeer url_mapping_test_peer(&url_mapping); |
| |
| auto urn_uuid = GenerateAndVerifyPendingMappedURN(&url_mapping); |
| const GURL mapped_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid); |
| |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node->current_frame_host()); |
| |
| EXPECT_TRUE(ExecJs(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_test_peer.HasObserver(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_TRUE(ExecJs(root, JsReplace("f.config = new FencedFrameConfig($1);", |
| new_url.spec()))); |
| |
| EXPECT_FALSE(url_mapping_test_peer.HasObserver(urn_uuid, request)); |
| |
| observer.Wait(); |
| |
| EXPECT_EQ( |
| new_url, |
| fenced_frame_root_node->current_frame_host()->GetLastCommittedURL()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url); |
| |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, 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_F(FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url); |
| |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, 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()); |
| std::optional<net::CookiePartitionKey> partition_key = |
| net::CookiePartitionKey::FromNetworkIsolationKey( |
| isolation_info.network_isolation_key(), |
| isolation_info.site_for_cookies(), net::SchemefulSite(https_url), |
| isolation_info.IsMainFrameRequest()); |
| 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(cookie.PartitionKey()->IsThirdParty(), |
| partition_key->IsThirdParty()); |
| 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;")); |
| } |
| |
| // Similar to `CheckPartitionedCookiesWithNonce`, but this test set up consists |
| // of three layers nested frames, from top to bottom: |
| // - A fenced frame loads an origin of "a.test". |
| // - An urn iframe loads an origin of "a.test". |
| // - An iframe loads origin of "a.test". |
| // Both the nested urn iframe in the middle and the iframe in the bottom should |
| // be able to access the same cookies as the top-level fenced frame because they |
| // operate on the same partition nonce. |
| // TODO(crbug.com/40060657): Once navigation support for urn::uuid in iframes is |
| // deprecated, this test should be removed. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| CheckPartitionedCookiesWithNonceShouldTraverseFrameTree) { |
| // 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');" |
| "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(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url); |
| |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, 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()); |
| std::optional<net::CookiePartitionKey> partition_key = |
| net::CookiePartitionKey::FromNetworkIsolationKey( |
| isolation_info.network_isolation_key(), |
| isolation_info.site_for_cookies(), net::SchemefulSite(https_url), |
| isolation_info.IsMainFrameRequest()); |
| 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(cookie.PartitionKey()->IsThirdParty(), |
| partition_key->IsThirdParty()); |
| EXPECT_EQ("3", cookie.Value()); |
| } |
| |
| // Run the same test for an urn iframe inside the fenced frame. It should be |
| // able to access the same cookies because urn iframe nested in a fenced |
| // frame should operate on the partition nonce from the fenced frame. |
| GURL iframe_url( |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html")); |
| |
| // Generate urn uuid. |
| FencedFrameURLMapping& iframe_url_mapping = |
| fenced_frame_root_node->current_frame_host() |
| ->GetPage() |
| .fenced_frame_urls_map(); |
| auto iframe_urn_uuid = |
| test::AddAndVerifyFencedFrameURL(&iframe_url_mapping, iframe_url); |
| |
| EXPECT_EQ(0U, fenced_frame_root_node->child_count()); |
| FrameTreeNode* iframe_node = |
| AddIframeInFencedFrame(fenced_frame_root_node, 0); |
| NavigateIframeInFencedFrame(fenced_frame_root_node->child_at(0), |
| iframe_urn_uuid); |
| 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;")); |
| |
| // Add another iframe under the nested urn iframe. The iframe should be able |
| // to access the same cookies as the top-level fenced frame because they |
| // operate on the same partition nonce. |
| GURL bottom_iframe_url( |
| https_server()->GetURL("a.test", "/fenced_frames/title0.html")); |
| |
| EXPECT_EQ(0U, iframe_node->child_count()); |
| FrameTreeNode* bottom_iframe_node = AddIframeInFencedFrame(iframe_node, 0); |
| NavigateIframeInFencedFrame(iframe_node->child_at(0), bottom_iframe_url); |
| |
| EXPECT_EQ( |
| bottom_iframe_url, |
| iframe_node->child_at(0)->current_frame_host()->GetLastCommittedURL()); |
| EXPECT_EQ( |
| url::Origin::Create(bottom_iframe_url), |
| iframe_node->child_at(0)->current_frame_host()->GetLastCommittedOrigin()); |
| EXPECT_EQ("B=3; C=3", EvalJs(bottom_iframe_node->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_F(FencedFrameParameterizedBrowserTest, |
| 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.config = new FencedFrameConfig($1);", |
| fenced_frame_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, 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_F(FencedFrameParameterizedBrowserTest, |
| 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.config = new FencedFrameConfig($1);", fenced_frame_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(fenced_frame, |
| 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()); |
| std::optional<base::UnguessableToken> fenced_frame_nonce = |
| fenced_frame->GetFencedFrameNonce(); |
| 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()->GetStorageKey().nonce().has_value()); |
| EXPECT_EQ( |
| fenced_frame_nonce.value(), |
| fenced_frame->current_frame_host()->GetStorageKey().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()->GetStorageKey().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()->GetStorageKey().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()); |
| std::optional<base::UnguessableToken> nested_iframe_nonce = |
| fenced_frame->child_at(0)->GetFencedFrameNonce(); |
| 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() |
| ->GetStorageKey() |
| .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() |
| ->GetStorageKey() |
| .nonce() |
| .value()); |
| |
| // Add a nested fenced frame. |
| auto* nested_fenced_frame = AddNestedFencedFrame(fenced_frame, 1); |
| GetFencedFrameRootNode(fenced_frame->child_at(1)); |
| std::optional<base::UnguessableToken> nested_fframe_nonce = |
| nested_fenced_frame->GetFencedFrameNonce(); |
| 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")); |
| std::optional<base::UnguessableToken> new_fenced_frame_nonce = |
| fenced_frame->GetFencedFrameNonce(); |
| EXPECT_NE(std::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_F(FencedFrameParameterizedBrowserTest, |
| 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.config = new FencedFrameConfig($1);", fenced_frame_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(fenced_frame, |
| navigate_script); |
| } |
| |
| // There should be a nonce in the StorageKey. |
| EXPECT_TRUE( |
| fenced_frame->current_frame_host()->GetStorageKey().nonce().has_value()); |
| |
| std::optional<base::UnguessableToken> fenced_frame_nonce = |
| fenced_frame->GetFencedFrameNonce(); |
| EXPECT_TRUE(fenced_frame_nonce.has_value()); |
| EXPECT_EQ( |
| fenced_frame_nonce.value(), |
| fenced_frame->current_frame_host()->GetStorageKey().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()->GetStorageKey().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()->GetStorageKey().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_F(FencedFrameParameterizedBrowserTest, |
| CheckFencedFrameNotNavigatedWithoutOptIn) { |
| base::HistogramTester histogram_tester; |
| 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)); |
| |
| GURL https_url(https_server()->GetURL("a.test", "/title1.html")); |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url); |
| |
| WebContentsConsoleObserver console_observer(shell()->web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kError; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern("Supports-Loading-Mode*"); |
| |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, navigate_urn_script, |
| net::ERR_BLOCKED_BY_RESPONSE); |
| |
| EXPECT_FALSE(console_observer.messages().empty()); |
| EXPECT_EQ( |
| console_observer.GetMessageAt(0), |
| "Supports-Loading-Mode HTTP response header 'fenced-frame' is required " |
| "to load the fenced frame root and its nested iframes."); |
| content::FetchHistogramsFromChildProcesses(); |
| histogram_tester.ExpectTotalCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, 2); |
| // Fenced frame creation succeeded (opaque ads mode) |
| histogram_tester.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kSuccessOpaque, 1); |
| // Fenced frame navigation failed (Supports-Loading-Mode response header |
| // 'fenced-frame' not opted-in) |
| histogram_tester.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kResponseHeaderNotOptIn, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| CheckNestedIframeNotNavigatedWithoutOptIn) { |
| base::HistogramTester histogram_tester; |
| 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.config = new FencedFrameConfig($1);", fenced_frame_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, navigate_script); |
| } |
| |
| WebContentsConsoleObserver console_observer(shell()->web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kError; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern("Supports-Loading-Mode*"); |
| |
| // 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); |
| |
| EXPECT_FALSE(console_observer.messages().empty()); |
| EXPECT_EQ( |
| console_observer.GetMessageAt(0), |
| "Supports-Loading-Mode HTTP response header 'fenced-frame' is required " |
| "to load the fenced frame root and its nested iframes."); |
| content::FetchHistogramsFromChildProcesses(); |
| histogram_tester.ExpectTotalCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, 2); |
| // Fenced frame creation succeeded (default mode) |
| histogram_tester.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kSuccessDefault, 1); |
| // Fenced frame navigation failed (Supports-Loading-Mode response header |
| // 'fenced-frame' not opted-in) |
| histogram_tester.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kResponseHeaderNotOptIn, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| 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.config = new FencedFrameConfig($1);", |
| fenced_frame_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, 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_F(FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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. |
| // |
| // Since this was a navigation toward an opaque URL initiated from the |
| // embedder, the navigation must use and commit FencedFrameProperties with |
| // an opaque URL. |
| 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(); |
| auto urn_uuid = |
| test::AddAndVerifyFencedFrameURL(&url_mapping, fenced_frame_url); |
| |
| std::string navigate_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, navigate_script); |
| |
| // The mapped url of the fenced frame properties should be opaque to the |
| // embedder. |
| EXPECT_FALSE(fenced_frame_root_node->GetFencedFrameProperties() |
| ->mapped_url() |
| ->GetValueForEntity(FencedFrameEntity::kEmbedder) |
| .has_value()); |
| |
| // Navigate the fenced frame again, but toward a non-opaque URL. Since this |
| // is initiated from the embedder, the new document must commit with |
| // FencedFrameProperties with a transparent URL. |
| GURL second_url( |
| https_server()->GetURL("a.test", "/fenced_frames/title0.html")); |
| std::string second_navigate_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", second_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, second_navigate_script); |
| // The mapped url of the fenced frame properties should be visible to the |
| // embedder. |
| EXPECT_TRUE(fenced_frame_root_node->GetFencedFrameProperties() |
| ->mapped_url() |
| ->GetValueForEntity(FencedFrameEntity::kEmbedder) |
| .has_value()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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. The navigation must use and commit FencedFrameProperties with an |
| // opaque URL. |
| 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(); |
| auto urn_uuid = |
| test::AddAndVerifyFencedFrameURL(&url_mapping, fenced_frame_url); |
| |
| std::string navigate_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, navigate_script); |
| |
| EXPECT_FALSE(fenced_frame_root_node->GetFencedFrameProperties() |
| ->mapped_url() |
| ->GetValueForEntity(FencedFrameEntity::kEmbedder) |
| .has_value()); |
| |
| // Navigate the fenced frame again, but toward a non-opaque URL and the |
| // navigation is cancelled. The navigation is not committed and therefore |
| // the FencedFrameProperties do not change. |
| GURL second_url(https_server()->GetURL("a.test", "/nocontent")); |
| std::string second_navigate_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", second_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, 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 the same FencedFrameProperties. |
| 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_FALSE(fenced_frame_root_node->GetFencedFrameProperties() |
| ->mapped_url() |
| ->GetValueForEntity(FencedFrameEntity::kEmbedder) |
| .has_value()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| RTCPeerConnectionDisabled) { |
| GURL main_url(https_server()->GetURL("a.test", "/fenced_frames/title1.html")); |
| GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHost* primary_rfh = primary_main_frame_host(); |
| RenderFrameHost* fenced_frame_host = |
| fenced_frame_test_helper().CreateFencedFrame(primary_rfh, |
| fenced_frame_url); |
| |
| // Copied from https://webrtc.org/getting-started/peer-connections. |
| // The contents of the configuration object doesn't matter here, |
| // because construction should fail before the information becomes |
| // relevant. |
| auto result = EvalJs(fenced_frame_host, R"( |
| const configuration = { |
| 'iceServers': [{'urls': 'stun:stun.example.com:19302'}] |
| }; |
| const peerConnection = new RTCPeerConnection(configuration); |
| )"); |
| |
| EXPECT_THAT( |
| result.error, |
| testing::HasSubstr("Failed to construct 'RTCPeerConnection': " |
| "RTCPeerConnection is not allowed in fenced frames.")); |
| } |
| |
| namespace { |
| class InsecureContentTestContentBrowserClient |
| : public ContentBrowserTestContentBrowserClient { |
| public: |
| 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; |
| } |
| }; |
| } // namespace |
| |
| class FencedFrameIgnoreCertErrors : public FencedFrameParameterizedBrowserTest { |
| public: |
| FencedFrameIgnoreCertErrors() |
| : https_server_mismatched_(ServerType::TYPE_HTTPS) {} |
| |
| protected: |
| // 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 { |
| FencedFrameParameterizedBrowserTest::SetUpCommandLine(command_line); |
| // Browser will ignore certificate errors. |
| command_line->AppendSwitch(switches::kIgnoreCertificateErrors); |
| } |
| |
| void TearDownOnMainThread() override { |
| web_contents_.reset(); |
| FencedFrameParameterizedBrowserTest::TearDownOnMainThread(); |
| } |
| |
| void TearDown() override { |
| GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, |
| browser_context_.release()); |
| FencedFrameParameterizedBrowserTest::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: |
| void AdditionalSetup() 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. |
| base::FilePath path; |
| browser_context_ = CreateTestBrowserContext(); |
| |
| https_server()->RegisterRequestMonitor(base::BindRepeating( |
| &FencedFrameParameterizedBrowserTest::ObserveRequestHeaders, |
| base::Unretained(this))); |
| } |
| |
| net::EmbeddedTestServer https_server_mismatched_; |
| std::unique_ptr<BrowserContext> browser_context_; |
| std::unique_ptr<WebContents> web_contents_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameIgnoreCertErrors, FencedframeHasCertError) { |
| CreateWebContents(); |
| // Allow insecure content. |
| InsecureContentTestContentBrowserClient 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.config = new FencedFrameConfig($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_F(FencedFrameParameterizedBrowserTest, |
| 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.config = new FencedFrameConfig($1);", |
| fenced_frame_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, 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.config = new FencedFrameConfig($1);", new_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, 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_F(FencedFrameParameterizedBrowserTest, |
| 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()); |
| |
| EXPECT_EQ(1, 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.config = new FencedFrameConfig($1);", fenced_frame_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(fenced_frame, |
| navigate_script); |
| |
| GURL fragment_url( |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html#123")); |
| navigate_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", fragment_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(fenced_frame, |
| 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.config = new FencedFrameConfig($1);", cross_site_url.spec()); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(fenced_frame, |
| navigate_script_2); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(fenced_frame, |
| 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_F(FencedFrameParameterizedBrowserTest, |
| 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"); |
| |
| 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()); |
| EXPECT_TRUE(WaitForDOMContentLoaded(fenced_frame->current_frame_host())); |
| |
| // 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()); |
| |
| // Note the last committed url is the latest one (`fenced_frame_url_2`) when |
| // back/forward cache is enabled. However, when back/forward cache is |
| // disabled, it will navigate to `fenced_frame_url_1`. Fenced frames have |
| // their own NavigationController which is not retained when the top-level |
| // page navigates. Therefore going back lands on the initial navigation in |
| // the Fenced Frame. |
| EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount()); |
| |
| if (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()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| ReloadPageWithFencedFrame) { |
| GURL main_url(https_server()->GetURL( |
| "a.test", "/fenced_frames/basic_fenced_frame_src.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_F(FencedFrameParameterizedBrowserTest, |
| NavigateUnfencedTopAndGoBack) { |
| GURL main_url(https_server()->GetURL("a.test", "/fenced_frames/title1.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(); |
| |
| RenderFrameHost* primary_rfh = primary_main_frame_host(); |
| std::ignore = fenced_frame_test_helper().CreateFencedFrame( |
| primary_rfh, fenced_frame_url, net::OK, |
| blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds); |
| |
| 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()); |
| |
| if (BackForwardCache::IsBackForwardCacheFeatureEnabled()) { |
| // When bfcache is enabled, the fenced frame should still be there after we |
| // go back. |
| 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()); |
| } else { |
| // When bfcache is disabled, the fenced frame should no longer exist when we |
| // go back, because it was created programmatically. |
| EXPECT_EQ(0U, root->child_count()); |
| } |
| } |
| |
| // Simulates the crash in crbug.com/1317642 by disabling BFCache and going back |
| // to a page with a fenced frame navigation. This is a regression test |
| // originally for Shadow DOM fenced frames, which no longer exist, but we still |
| // explicitly test this scenario. |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| 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. |
| 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"); |
| |
| 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()); |
| |
| // Fenced frames have their own NavigationController which is not retained |
| // when the top-level page navigates. Therefore going back lands on the |
| // initial fenced frame without any navigation. |
| EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| EXPECT_EQ(1, fenced_frame->navigator().controller().GetEntryCount()); |
| |
| EXPECT_TRUE(!fenced_frame->navigator().controller().GetLastCommittedEntry() || |
| fenced_frame->navigator() |
| .controller() |
| .GetLastCommittedEntry() |
| ->IsInitialEntry()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| 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"); |
| |
| 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(), /* initiator_origin= */ std::nullopt, |
| /* initiator_base_url= */ std::nullopt, |
| ui::PAGE_TRANSITION_RELOAD, false, std::string(), |
| controller.GetBrowserContext(), |
| nullptr /* blob_url_loader_factory */)); |
| NavigationEntryRestoreContextImpl context; |
| restored_entry->SetPageState(blink::PageState::CreateFromURL(main_url), |
| &context); |
| 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(), 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()); |
| |
| // 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_F(FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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.config = new FencedFrameConfig($1);", urn_uuid); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, navigate_urn_script, net::ERR_INVALID_URL); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| 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');" |
| "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(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url); |
| |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($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, navigate_urn_script, expected_net_error_code); |
| |
| if (!test_case.expect_allowed) |
| EXPECT_EQ("fenced-frame-src;", EvalJs(root, "violation")); |
| |
| std::optional<blink::FencedFrame::DeprecatedFencedFrameMode> |
| fenced_frame_mode = |
| fenced_frame_root_node->GetDeprecatedFencedFrameMode(); |
| EXPECT_TRUE(fenced_frame_mode.has_value()); |
| EXPECT_EQ(fenced_frame_mode.value(), |
| blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| 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(crbug.com/40919516): Flaky on Android release bots. |
| #if BUILDFLAG(IS_ANDROID) && defined(NDEBUG) |
| #define MAYBE_FencedAdSizes DISABLED_FencedAdSizes |
| #else |
| #define MAYBE_FencedAdSizes FencedAdSizes |
| #endif |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| 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"); |
| const GURL kUrl2 = |
| https_server()->GetURL("a.test", "/fenced_frames/title0.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), kUrl2)); |
| |
| // 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.id = 'nested_fenced_frame';" |
| "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(); |
| |
| // Navigate the fenced frame, which should force its inner size to the |
| // nearest allowed one. |
| TestFrameNavigationObserver observer(nodeB); |
| fenced_frame_test_helper().NavigateFencedFrameUsingFledge( |
| nodeA->current_frame_host(), kUrl, "nested_fenced_frame"); |
| observer.Wait(); |
| |
| // Check that the outer container size hasn't changed. |
| EXPECT_TRUE(PollUntilEvalToTrue( |
| JsReplace("getComputedStyle(nested_fenced_frame).width == $1 && " |
| "getComputedStyle(nested_fenced_frame).height == $2", |
| frame_width, frame_height), |
| nodeA->current_frame_host())); |
| |
| // Check that the inner size is what we expect. |
| EXPECT_TRUE( |
| PollUntilEvalToTrue(JsReplace("innerWidth == $1 && innerHeight == $2", |
| output_width, output_height), |
| nodeB->current_frame_host())); |
| |
| // 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))); |
| |
| // Force a style recomputation. |
| ASSERT_TRUE(EvalJs(nodeA, "getComputedStyle(nested_fenced_frame).width") |
| .error.empty()); |
| |
| // Check that the inner size hasn't changed. |
| EXPECT_TRUE( |
| PollUntilEvalToTrue(JsReplace("innerWidth == $1 && innerHeight == $2", |
| output_width, output_height), |
| nodeB->current_frame_host())); |
| }; |
| |
| // 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)); |
| } |
| } |
| |
| // 1. creates a default mode fenced frame. |
| // 2. creates an opaque mode urn iframe nested in the fenced frame. |
| // 3. do an `_unfencedTop` navigation from the urn iframe. |
| // |
| // The `_unfencedTop` navigation should succeed. This verifies the fenced frame |
| // properties from the urn iframe are used for checks in |
| // `ValidateUnfencedTopNavigation`. Otherwise, if the fenced frame properties |
| // from the top-level fenced frame are used, a mojo bad message should be |
| // received. |
| // |
| // Note: Outside tests, one common scenairo that results in the same setup is |
| // creating a shared storage urn iframe nested inside a default fenced frame. |
| // |
| // TODO(crbug.com/40060657): Once navigation support for urn::uuid in iframes is |
| // deprecated, this test should be removed. |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| NestedUrnIframeUnderFencedFrameUnfencedTopNavigation) { |
| base::HistogramTester histogram_tester; |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root_rfh = |
| static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root() |
| ->current_frame_host(); |
| |
| // Add fenced frame. |
| EXPECT_TRUE(ExecJs( |
| root_rfh, |
| JsReplace(R"( |
| var f = document.createElement('fencedframe'); |
| f.mode = $1; |
| document.body.appendChild(f); |
| )", |
| blink::FencedFrame::DeprecatedFencedFrameMode::kDefault))); |
| EXPECT_EQ(1U, root_rfh->child_count()); |
| FrameTreeNode* fenced_frame_root_node = |
| GetFencedFrameRootNode(root_rfh->child_at(0)); |
| |
| // Navigate fenced frame. |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title0.html"); |
| |
| std::string navigate_urn_script = |
| JsReplace("f.config = new FencedFrameConfig($1);", fenced_frame_url); |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, navigate_urn_script); |
| EXPECT_EQ( |
| fenced_frame_url, |
| fenced_frame_root_node->current_frame_host()->GetLastCommittedURL()); |
| EXPECT_EQ( |
| url::Origin::Create(fenced_frame_url), |
| fenced_frame_root_node->current_frame_host()->GetLastCommittedOrigin()); |
| |
| // Check histograms. |
| content::FetchHistogramsFromChildProcesses(); |
| histogram_tester.ExpectTotalCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, 1); |
| histogram_tester.ExpectBucketCount( |
| blink::kFencedFrameCreationOrNavigationOutcomeHistogram, |
| blink::FencedFrameCreationOutcome::kSuccessDefault, 1); |
| |
| // Add nested urn iframe. |
| FrameTreeNode* urn_iframe_node = |
| AddIframeInFencedFrame(fenced_frame_root_node, 0); |
| |
| // Generate urn. |
| const GURL urn_iframe_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title0.html"); |
| |
| std::optional<GURL> urn_uuid = |
| fenced_frame_root_node->current_frame_host() |
| ->GetPage() |
| .fenced_frame_urls_map() |
| .AddFencedFrameURLForTesting(urn_iframe_url); |
| EXPECT_TRUE(urn_uuid.has_value()); |
| EXPECT_TRUE(urn_uuid->is_valid()); |
| |
| // Navigate the iframe using the urn. |
| NavigateIframeInFencedFrame(urn_iframe_node, urn_uuid.value()); |
| |
| // Do an `_unfencedTop` navigation from the nested urn iframe. |
| const GURL new_page_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| TestFrameNavigationObserver observer(root); |
| EXPECT_TRUE( |
| ExecJs(urn_iframe_node, |
| JsReplace("window.open($1, '_unfencedTop');", new_page_url))); |
| observer.Wait(); |
| |
| // Expect the `_unfencedTop` navigation to succeed. |
| EXPECT_EQ(new_page_url, root->current_frame_host()->GetLastCommittedURL()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| EmbedderInitiatedNavigationForceNewBrowsingInstance) { |
| base::HistogramTester histogram_tester; |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Create parent fenced frame. |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title0.html"); |
| RenderFrameHost* ff_rfh = fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), fenced_frame_url); |
| |
| // Create nested fenced frame. |
| const GURL nested_fenced_frame_url = |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html"); |
| RenderFrameHost* nested_ff_rfh = fenced_frame_test_helper().CreateFencedFrame( |
| ff_rfh, nested_fenced_frame_url); |
| FrameTreeNode* nested_ff_node = |
| static_cast<RenderFrameHostImpl*>(nested_ff_rfh)->frame_tree_node(); |
| scoped_refptr<SiteInstance> nested_ff_site_instance = |
| nested_ff_rfh->GetSiteInstance(); |
| |
| TestFrameNavigationObserver load_observer(nested_ff_rfh); |
| |
| // Embedder initiates nested fenced frame navigation. |
| const GURL navigate_url = |
| https_server()->GetURL("b.test", "/fenced_frames/basic.html"); |
| EXPECT_TRUE(ExecJs( |
| ff_rfh, JsReplace( |
| R"(document.getElementsByTagName('fencedframe')[0].config = |
| new FencedFrameConfig($1);)", |
| navigate_url))); |
| |
| // Wait for load stops. |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| load_observer.Wait(); |
| EXPECT_EQ(nested_ff_node->current_frame_host()->GetLastCommittedURL(), |
| navigate_url); |
| |
| // An embedder-initiated fenced frame navigation through a fenced frame config |
| // will use a new SiteInstance in a different BrowsingInstance. |
| SiteInstance* post_navigation_site_instance = |
| nested_ff_node->current_frame_host()->GetSiteInstance(); |
| EXPECT_NE(nested_ff_site_instance, post_navigation_site_instance); |
| EXPECT_FALSE(nested_ff_site_instance->IsRelatedSiteInstance( |
| post_navigation_site_instance)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| DisableUntrustedNetworkNestedFrames) { |
| // This test creates the following frame setup: |
| // a.test |
| // └─b.test (fenced) |
| // ├─c.test |
| // │ └─c.test |
| // └─d.test (fenced) |
| // It then calls disableUntrustedNetwork() on b.test, and ensures that network |
| // isn't cut off until d.test's network is revoked. |
| |
| GURL main_url( |
| https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(b.test{" |
| "fenced}(c.test(c.test),d.test{fenced}))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* first_fenced_frame = |
| root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* second_fenced_frame = |
| first_fenced_frame->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| // Call disable untrusted network on the first fenced frame. Make sure it |
| // doesn't resolve. |
| EXPECT_EQ(EvalJs(first_fenced_frame, R"( |
| var ff1_promise_resolved = false; |
| (async () => { |
| let timeout_promise = new Promise( |
| resolve => setTimeout(() => {resolve('timeout')}, 1000)); |
| let disable_network_promise = window.fence.disableUntrustedNetwork().then( |
| () => {ff1_promise_resolved = true;}); |
| return Promise.race([disable_network_promise, timeout_promise]); |
| })(); |
| )"), |
| "timeout"); |
| |
| VerifyFencedFrameNetworkStatus( |
| first_fenced_frame, |
| DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| VerifyFencedFrameNetworkStatus(second_fenced_frame, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| EXPECT_FALSE( |
| EvalJs(first_fenced_frame, "ff1_promise_resolved").ExtractBool()); |
| |
| // Call disable untrusted network on the second fenced frame. This one should |
| // resolve and cause the first fenced frame to have full network cutoff. |
| EXPECT_TRUE(ExecJs(second_fenced_frame, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| |
| VerifyFencedFrameNetworkStatus( |
| first_fenced_frame, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus( |
| second_fenced_frame, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| EXPECT_TRUE(EvalJs(first_fenced_frame, "ff1_promise_resolved").ExtractBool()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| DisableUntrustedNetworkParallelTrees) { |
| // This test creates the following frame setup: |
| // a.test |
| // ├─b.test (fenced) (FF1) |
| // │ └─b.test (fenced) (FF2) |
| // └─c.test (fenced) (FF3) |
| // └─c.test (fenced) (FF4) |
| // It then makes the following calls and checks: |
| // 1. FF1 disableUntrustedNetwork(), no promise resolved. |
| // 2. FF4 disableUntrustedNetwork(), FF4 promise resolved. |
| // 3. FF3 disableUntrustedNetwork(), FF3 promise resolved. |
| // 4. FF2 disableUntrustedNetwork(), FF1 & FF2 promise resolved. |
| |
| GURL main_url( |
| https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(b.test{" |
| "fenced}(b.test{fenced}),c.test{fenced}(c.test{" |
| "fenced}))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* ff1 = root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* ff2 = ff1->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* ff3 = root->GetFencedFrames().at(1)->GetInnerRoot(); |
| RenderFrameHostImpl* ff4 = ff3->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| // Call disable untrusted network on the first fenced frame. Make sure it |
| // doesn't resolve. |
| EXPECT_EQ(EvalJs(ff1, R"( |
| var ff1_promise_resolved = false; |
| (async () => { |
| let timeout_promise = new Promise( |
| resolve => setTimeout(() => {resolve('timeout')}, 1000)); |
| let disable_network_promise = window.fence.disableUntrustedNetwork().then( |
| () => {ff1_promise_resolved = true;}); |
| return Promise.race([disable_network_promise, timeout_promise]); |
| })(); |
| )"), |
| "timeout"); |
| |
| // Only the first fenced frame should have marked its frame tree as disabled. |
| // No frame trees should have full network cutoff. |
| VerifyFencedFrameNetworkStatus( |
| ff1, DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| VerifyFencedFrameNetworkStatus(ff2, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus(ff3, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus(ff4, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| |
| // Call disable untrusted network on the 4th fenced frame. It should resolve. |
| EXPECT_TRUE(ExecJs(ff4, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| |
| // The 4th fenced frame should be fully marked for network cutoff. None of the |
| // other frames should've been affected by this. |
| VerifyFencedFrameNetworkStatus( |
| ff1, DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| VerifyFencedFrameNetworkStatus(ff2, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus(ff3, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus( |
| ff4, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| EXPECT_FALSE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| |
| // Call disable untrusted network on the 3rd fenced frame. It should resolve. |
| EXPECT_TRUE(ExecJs(ff3, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| |
| // The 3rd fenced frame should be fully marked for network cutoff. None of the |
| // other frames should've been affected by this. |
| VerifyFencedFrameNetworkStatus( |
| ff1, DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| VerifyFencedFrameNetworkStatus(ff2, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus( |
| ff3, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus( |
| ff4, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| EXPECT_FALSE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| |
| // Call disable untrusted network on the 2nd fenced frame. It should resolve. |
| EXPECT_TRUE(ExecJs(ff2, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| |
| // The 2nd fenced frame should be fully marked for network cutoff. The 1st |
| // fenced frame should also be fully marked now that its descendant has lost |
| // network access. |
| VerifyFencedFrameNetworkStatus( |
| ff1, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus( |
| ff2, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus( |
| ff3, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus( |
| ff4, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| EXPECT_TRUE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| AddFencedFrameToDisabledNetworkTree) { |
| // This test creates the following frame setup: |
| // a.test |
| // ├─b.test (fenced) FF1 |
| // └─c.test (fenced) FF2 |
| // It then cuts off b.test's network access. After doing that, the test adds a |
| // new child fenced frame and checks that the fenced frame did not navigate |
| // and that recalculating the network revocation status (via c.test having its |
| // network revoked) doesn't change the status of b.test. |
| |
| GURL main_url(https_server()->GetURL( |
| "a.test", |
| "/cross_site_iframe_factory.html?a.test(b.test{fenced},c.test{fenced})")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* ff1 = root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* ff2 = root->GetFencedFrames().at(1)->GetInnerRoot(); |
| |
| // Disable the fenced frame's network. |
| EXPECT_TRUE(ExecJs(ff1, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| VerifyFencedFrameNetworkStatus( |
| ff1, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus(ff2, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| |
| // Create and attempt to navigate a child fenced frame after network cutoff. |
| // The creation should succeed, but the navigation should fail. |
| RenderFrameHostImpl* nested_ff = |
| AddNestedFencedFrame(ff1->frame_tree_node(), 0)->current_frame_host(); |
| GURL fenced_frame_url( |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html")); |
| EXPECT_TRUE(ExecJs(ff1, JsReplace("document.querySelector('fencedframe')." |
| "config = new FencedFrameConfig($1);", |
| fenced_frame_url.spec()))); |
| |
| // Disable the network of an unrelated fenced frame. This will cause the whole |
| // frame tree to be recalculated. |
| EXPECT_TRUE(ExecJs(ff2, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| VerifyFencedFrameNetworkStatus( |
| ff2, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| |
| // The addition of a nested fenced frame that doesn't navigate shouldn't |
| // change the network revocation status of its ancestor. The nested fenced |
| // frame will have been created with its network already being marked as cut |
| // off. |
| VerifyFencedFrameNetworkStatus( |
| ff1, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus( |
| nested_ff, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| AddFencedFrameAfterNetworkCutoff) { |
| // This test creates the following frame setup: |
| // a.test |
| // └─b.test (fenced) |
| // └─c.test (fenced) |
| // It then cuts off b.test's network access. After doing that, the test adds a |
| // new fenced frame as a child of b.test and checks that the fenced frame did |
| // not navigate. It then cuts off c.test's network and checks that b.test has |
| // its network revoked as a result. |
| |
| GURL main_url(https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(" |
| "b.test{fenced}(c.test{fenced}))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* ff1 = root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* ff2 = ff1->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| // Disable the outer fenced frame's network. |
| // Call disable untrusted network on the first fenced frame. Make sure it |
| // doesn't resolve. |
| EXPECT_EQ(EvalJs(ff1, R"( |
| var ff1_promise_resolved = false; |
| (async () => { |
| let timeout_promise = new Promise( |
| resolve => setTimeout(() => {resolve('timeout')}, 1000)); |
| let disable_network_promise = window.fence.disableUntrustedNetwork().then( |
| () => {ff1_promise_resolved = true;}); |
| return Promise.race([disable_network_promise, timeout_promise]); |
| })(); |
| )"), |
| "timeout"); |
| |
| VerifyFencedFrameNetworkStatus( |
| ff1, DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| VerifyFencedFrameNetworkStatus(ff2, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| EXPECT_FALSE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| |
| // Create and attempt to navigate a child fenced frame after network cutoff. |
| // The creation should succeed, but the navigation should fail. |
| RenderFrameHostImpl* nested_ff = |
| AddNestedFencedFrame(ff1->frame_tree_node(), 1)->current_frame_host(); |
| GURL fenced_frame_url( |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html")); |
| EXPECT_TRUE(ExecJs(ff1, JsReplace("document.querySelector('fencedframe')." |
| "config = new FencedFrameConfig($1);", |
| fenced_frame_url.spec()))); |
| |
| // Disable the network of the other nested fenced frame. This will cause the |
| // whole frame tree to be recalculated. |
| EXPECT_TRUE(ExecJs(ff2, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| VerifyFencedFrameNetworkStatus( |
| ff2, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| EXPECT_TRUE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| |
| // The addition of a nested fenced frame that doesn't navigate shouldn't |
| // change the network revocation status of its ancestor. |
| VerifyFencedFrameNetworkStatus( |
| ff1, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus( |
| nested_ff, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| } |
| |
| // Helper class. Immediately run a callback when a navigation starts. |
| class DidStartNavigationCallback final : public WebContentsObserver { |
| public: |
| explicit DidStartNavigationCallback( |
| WebContents* web_contents, |
| base::OnceCallback<void(NavigationHandle*)> callback) |
| : WebContentsObserver(web_contents), callback_(std::move(callback)) {} |
| ~DidStartNavigationCallback() override = default; |
| |
| private: |
| void DidStartNavigation(NavigationHandle* navigation_handle) override { |
| if (callback_) { |
| std::move(callback_).Run(navigation_handle); |
| } |
| } |
| base::OnceCallback<void(NavigationHandle*)> callback_; |
| }; |
| |
| // Test that calling `window.fence.disableUntrustedNetwork` from a fenced frame |
| // that has a nested fenced frame with an ongoing navigation. The promise |
| // returned should not be resolved. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| RevokeNetworkAccessNotResolveWithOngoingNestedFencedFrameNavigation) { |
| // This test creates the following frame setup: |
| // a.test |
| // └─b.test (fenced) |
| // └─c.test (fenced) |
| |
| GURL main_url(https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(" |
| "b.test{fenced}(c.test{fenced}))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* ff1 = root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* ff2 = ff1->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| // Disable nested fenced frame untrusted network access. The nonce should |
| // resolve. |
| EXPECT_TRUE(ExecJs(ff2, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| VerifyFencedFrameNetworkStatus(ff1, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus( |
| ff2, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| |
| // Callback will be invoked after embedder-initiated nested fenced frame |
| // navigation starts. |
| DidStartNavigationCallback callback( |
| web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| // Disable untrusted network for the parent fenced frame. The promise |
| // will not resolve due to the ongoing navigation in the nested fenced |
| // frame. |
| EXPECT_EQ(EvalJs(ff1, R"( |
| (async () => { |
| let timeout_promise = new Promise( |
| resolve => setTimeout(() => {resolve('timeout')}, 1000)); |
| let disable_network_promise = |
| window.fence.disableUntrustedNetwork(); |
| return Promise.race([disable_network_promise, timeout_promise]); |
| })(); |
| )"), |
| "timeout"); |
| |
| // The nonce should be marked as revoked for untrusted network access. |
| VerifyFencedFrameNetworkStatus( |
| ff1, DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| })); |
| |
| // Embedder initiates the navigation of the nested fenced frame. |
| GURL navigate_url( |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html")); |
| EXPECT_TRUE(ExecJs( |
| ff1, JsReplace( |
| R"(document.getElementsByTagName('fencedframe')[0].config = |
| new FencedFrameConfig($1);)", |
| navigate_url))); |
| } |
| |
| // Test that calling `window.fence.disableUntrustedNetwork` from a fenced frame |
| // that has a nested iframe with an ongoing navigation. The promise returned |
| // should not be resolved. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| RevokeNetworkAccessNotResolveWithOngoingNestedIframeNavigation) { |
| // This test creates the following frame setup: |
| // a.test |
| // └─b.test (fenced) |
| // └─c.test |
| |
| GURL main_url(https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(" |
| "b.test{fenced}(c.test))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* fenced_frame_rfh = |
| root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* iframe_rfh = |
| static_cast<RenderFrameHostImpl*>(ChildFrameAt(fenced_frame_rfh, 0)); |
| |
| // Callback will be invoked after embedder-initiated nested iframe navigation |
| // starts. |
| DidStartNavigationCallback callback( |
| web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| // Disable untrusted network for the parent fenced frame. The promise |
| // will not resolve due to the ongoing navigation in the nested iframe. |
| EXPECT_EQ(EvalJs(fenced_frame_rfh, R"( |
| var promise_resolved = false; |
| (async () => { |
| let timeout_promise = new Promise( |
| resolve => setTimeout(() => {resolve('timeout')}, 1000)); |
| let disable_network_promise = |
| window.fence.disableUntrustedNetwork().then( |
| () => {promise_resolved = true;} |
| ); |
| return Promise.race([disable_network_promise, timeout_promise]); |
| })(); |
| )"), |
| "timeout"); |
| |
| // The nonce should be marked as revoked for untrusted network access. |
| VerifyFencedFrameNetworkStatus( |
| fenced_frame_rfh, |
| DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| EXPECT_FALSE( |
| EvalJs(fenced_frame_rfh, "promise_resolved").ExtractBool()); |
| })); |
| |
| GURL navigate_url( |
| https_server()->GetURL("c.test", "/fenced_frames/title1.html")); |
| |
| // Set up navigation and console observers. |
| NavigationHandleObserver handle_observer(web_contents(), navigate_url); |
| TestFrameNavigationObserver load_observer(iframe_rfh); |
| |
| // Embedder initiates the navigation of the nested iframe. |
| EXPECT_TRUE( |
| ExecJs(fenced_frame_rfh, |
| JsReplace("document.getElementsByTagName('iframe')[0].src = $1;", |
| navigate_url))); |
| |
| // Wait for load stops. |
| load_observer.Wait(); |
| EXPECT_FALSE(load_observer.last_navigation_succeeded()); |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_TRUE(handle_observer.is_error()); |
| EXPECT_EQ(handle_observer.net_error_code(), net::ERR_NETWORK_ACCESS_REVOKED); |
| |
| // Once there is no ongoing navigation in nested iframe, the promise should be |
| // resolved. |
| EXPECT_TRUE(EvalJs(fenced_frame_rfh, "promise_resolved").ExtractBool()); |
| VerifyFencedFrameNetworkStatus( |
| fenced_frame_rfh, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| } |
| |
| // This test exercises this scenario: |
| // 1. There are two fenced frames: a child FF nested in a parent FF. |
| // 2. Child FF disables untrusted network. |
| // 3. Parent FF initiates a navigation of child FF to a new config. |
| // 4. Parent FF disables untrusted network immediately after the navigation is |
| // initiated. The promise returned by `window.fence.disableUntrustedNetwork()` |
| // should not resolve. |
| // 5. Attempt at this time to call shared storage get from parent FF should fail |
| // because the network hasn't been disabled yet due to the ongoing navigation. |
| // 6. The in-progress child FF navigation should be aborted. |
| // 7. Call `window.fence.disableUntrustedNetwork()` again for parent FF. This |
| // time the nonce should be resolved and the network is considered revoked. |
| // 8. Access to shared storage get is now allowed. |
| // |
| // Otherwise if the child FF navigation commits, the child FF will get a new |
| // nonce and no longer has untrusted network disabled. Parent FF can then |
| // communicate cross-site data into child via width or height fields, etc. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| ParentChildFencedFramesBothDisableNetworkCancelEmbedderInitiatedNavigation) { |
| // This test creates the following frame setup: |
| // a.test |
| // └─b.test (fenced) |
| // └─c.test (fenced) |
| |
| GURL main_url(https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(" |
| "b.test{fenced}(c.test{fenced}))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* ff1 = root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* ff2 = ff1->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| EXPECT_TRUE(ExecJs(ff1, R"( |
| sharedStorage.set('test', 'apple'); |
| )")); |
| |
| // Disable nested fenced frame untrusted network access. |
| EXPECT_TRUE(ExecJs(ff2, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| VerifyFencedFrameNetworkStatus(ff1, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus( |
| ff2, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| |
| const GURL navigate_url = |
| https_server()->GetURL("b.test", "/fenced_frames/basic.html"); |
| |
| // Set up navigation and console observers. |
| NavigationHandleObserver handle_observer(web_contents(), navigate_url); |
| TestFrameNavigationObserver load_observer(ff2); |
| WebContentsConsoleObserver console_observer(web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kError; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern("*network access has been disabled*"); |
| |
| // Callback will be invoked after embedder-initiated nested fenced frame |
| // navigation starts. |
| DidStartNavigationCallback callback( |
| web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| // Disable untrusted network for the parent fenced frame. The promise |
| // will not resolve due to the ongoing navigation in the nested fenced |
| // frame. |
| EXPECT_EQ(EvalJs(ff1, R"( |
| var ff1_promise_resolved = false; |
| (async () => { |
| let timeout_promise = new Promise( |
| resolve => setTimeout(() => {resolve('timeout')}, 1000)); |
| let disable_network_promise = |
| window.fence.disableUntrustedNetwork().then( |
| () => {ff1_promise_resolved = true;}); |
| return Promise.race([disable_network_promise, timeout_promise]); |
| })(); |
| )"), |
| "timeout"); |
| |
| // The nonce should be marked as revoked for untrusted network access. |
| VerifyFencedFrameNetworkStatus( |
| ff1, DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| EXPECT_FALSE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| |
| // Shared storage get is denied. |
| EvalJsResult get_result = EvalJs(ff1, "sharedStorage.get('test');"); |
| EXPECT_THAT( |
| get_result.error, |
| testing::HasSubstr( |
| "sharedStorage.get() is not allowed in a fenced frame until " |
| "network access for it and all descendent frames has been " |
| "revoked with window.fence.disableUntrustedNetwork()")); |
| })); |
| |
| // Embedder initiates nested fenced frame navigation. |
| EXPECT_TRUE(ExecJs( |
| ff1, JsReplace( |
| R"(document.getElementsByTagName('fencedframe')[0].config = |
| new FencedFrameConfig($1);)", |
| navigate_url))); |
| |
| // Wait for commit. |
| load_observer.WaitForCommit(); |
| |
| // The in-progress embedder initiated navigation is aborted because: |
| // 1. The child fenced frame disables untrusted network access. |
| // 2. The parent fenced frame, which is the navigation initiator, calls |
| // `window.fence.disableUntrustedNetwork` after navigation starts. This call |
| // marks the fenced frame's nonce as revoked for network access, even though |
| // the promise returned by the call does not resolve. |
| EXPECT_TRUE(handle_observer.is_error()); |
| EXPECT_EQ(net::ERR_ABORTED, handle_observer.net_error_code()); |
| |
| // A console error should be shown. |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| EXPECT_EQ(console_observer.messages().size(), 1u); |
| EXPECT_EQ( |
| console_observer.GetMessageAt(0), |
| "Embedder-initiated navigations of fenced frames are not allowed after " |
| "both the embedder and embedded fenced frame network access has been " |
| "disabled."); |
| |
| // The promise returned by the previous `window.fence.disableUntrustedNetwork` |
| // call will be resolved now because the child fenced frame no longer has |
| // ongoing navigations. Then shared storage get is allowed. |
| EXPECT_TRUE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| VerifyFencedFrameNetworkStatus( |
| ff1, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| VerifyFencedFrameNetworkStatus( |
| ff2, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| EXPECT_EQ(EvalJs(ff1, "sharedStorage.get('test');"), "apple"); |
| } |
| |
| // Disable untrusted network in a fenced frame. An ongoing navigation taking |
| // place in the frame itself should not prevent the promise returned by the |
| // `window.fence.disableUntrustedNetwork` call from being resolved. The |
| // navigation should succeed. |
| IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest, |
| DiableNetworkWithOngoingNavigationInTargetFencedFrame) { |
| // This test creates the following frame setup: |
| // a.test |
| // └─b.test (fenced) |
| |
| GURL main_url(https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(" |
| "b.test{fenced})")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* fenced_frame_rfh = |
| root->GetFencedFrames().at(0)->GetInnerRoot(); |
| FrameTreeNode* fenced_frame_node = fenced_frame_rfh->frame_tree_node(); |
| |
| const GURL navigate_url = |
| https_server()->GetURL("b.test", "/fenced_frames/basic.html"); |
| |
| // Set up navigation and console observers. |
| NavigationHandleObserver handle_observer(web_contents(), navigate_url); |
| TestFrameNavigationObserver load_observer(fenced_frame_rfh); |
| WebContentsConsoleObserver console_observer(web_contents()); |
| |
| // Callback will be invoked after embedder-initiated nested fenced frame |
| // navigation starts. |
| DidStartNavigationCallback callback( |
| web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| // Disable untrusted network for the fenced frame. The promise should |
| // resolve. |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| VerifyFencedFrameNetworkStatus( |
| fenced_frame_rfh, DisableUntrustedNetworkStatus:: |
| kCurrentAndDescendantFrameTreesComplete); |
| })); |
| |
| // Initiates fenced frame navigation. |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace( |
| R"(document.getElementsByTagName('fencedframe')[0].config = |
| new FencedFrameConfig($1);)", |
| navigate_url))); |
| |
| // Wait for commit. |
| load_observer.WaitForCommit(); |
| EXPECT_TRUE(load_observer.last_navigation_succeeded()); |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_EQ(handle_observer.last_committed_url(), navigate_url); |
| EXPECT_EQ(net::OK, handle_observer.net_error_code()); |
| |
| // Verify the network status after navigation commits. Because the fenced |
| // frame commits to a new config. The untrusted network access is not |
| // disabled. |
| VerifyFencedFrameNetworkStatus(fenced_frame_node, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| } |
| |
| // This test has a nested iframe in middle of two fenced frames. |
| // 1. Bottom fenced frame disables network. |
| // 2. The top fenced frame initiates the nested iframe navigation. |
| // 3. The top fenced frame disables its network right after the navigation |
| // starts. |
| // 4. The nested iframe navigation should fail. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| TopAndLeafFencedFramesRevokeNetworkNavigateNestedIframe) { |
| // This test creates the following frame setup: |
| // a.test |
| // └─b.test (fenced) |
| // └─c.test |
| // └─d.test (fenced) |
| |
| GURL main_url( |
| https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(b.test{" |
| "fenced}(c.test(d.test{fenced})))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* ff1 = root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* iframe_rfh = |
| static_cast<RenderFrameHostImpl*>(ChildFrameAt(ff1, 0)); |
| RenderFrameHostImpl* ff2 = |
| iframe_rfh->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| // Disable ff2 untrusted network access. |
| EXPECT_TRUE(ExecJs(ff2, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| VerifyFencedFrameNetworkStatus(ff1, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus( |
| ff2, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| |
| // Navigate to a page that contains a fenced frame. |
| const GURL navigate_url = |
| https_server()->GetURL("b.test", "/fenced_frames/nested.html"); |
| |
| // Set up navigation and console observers. |
| NavigationHandleObserver handle_observer(web_contents(), navigate_url); |
| TestFrameNavigationObserver load_observer(iframe_rfh); |
| |
| // Callback will be invoked after embedder-initiated nested iframe navigation |
| // starts. |
| DidStartNavigationCallback callback( |
| web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| // Disable untrusted network for ff1. The promise will not resolve due |
| // to the ongoing navigation in the nested iframe. |
| EXPECT_EQ(EvalJs(ff1, R"( |
| var ff1_promise_resolved = false; |
| (async () => { |
| let timeout_promise = new Promise( |
| resolve => setTimeout(() => {resolve('timeout')}, 1000)); |
| let disable_network_promise = |
| window.fence.disableUntrustedNetwork().then( |
| () => {ff1_promise_resolved = true;}); |
| return Promise.race([disable_network_promise, timeout_promise]); |
| })(); |
| )"), |
| "timeout"); |
| |
| // The nonce should be marked as revoked for untrusted network access. |
| VerifyFencedFrameNetworkStatus( |
| ff1, DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| VerifyFencedFrameNetworkStatus( |
| ff2, DisableUntrustedNetworkStatus:: |
| kCurrentAndDescendantFrameTreesComplete); |
| EXPECT_FALSE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| })); |
| |
| // Embedder initiates nested iframe navigation. |
| EXPECT_TRUE(ExecJs( |
| ff1, JsReplace("document.getElementsByTagName('iframe')[0].src = $1;", |
| navigate_url))); |
| |
| // Wait for load stops. |
| load_observer.Wait(); |
| |
| // The iframe navigation should fail because the root fenced frame nonce is |
| // revoked for untrusted network access. |
| EXPECT_FALSE(load_observer.last_navigation_succeeded()); |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_TRUE(handle_observer.is_error()); |
| EXPECT_EQ(handle_observer.net_error_code(), net::ERR_NETWORK_ACCESS_REVOKED); |
| |
| // Once there is no ongoing navigation in nested iframe, the promise should |
| // be resolved. |
| EXPECT_TRUE(EvalJs(ff1, "ff1_promise_resolved").ExtractBool()); |
| VerifyFencedFrameNetworkStatus( |
| ff1, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| } |
| |
| // This test exercises this scenario: |
| // 1. There are two fenced frames: a child FF nested in a parent FF. |
| // 2. Child FF disables untrusted network. |
| // 3. Parent FF initiates a navigation of child FF to a new config. |
| // 4. The in-progress child FF navigation should succeed. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| OnlyTargetFencedFrameDisablesNetworkDoesNotCancelEmbedderInitiatedNavigation) { |
| // This test creates the following frame setup: |
| // a.test |
| // └─b.test (fenced) |
| // └─c.test (fenced) |
| |
| GURL main_url(https_server()->GetURL("a.test", |
| "/cross_site_iframe_factory.html?a.test(" |
| "b.test{fenced}(c.test{fenced}))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| RenderFrameHostImpl* root = web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* ff1 = root->GetFencedFrames().at(0)->GetInnerRoot(); |
| RenderFrameHostImpl* ff2 = ff1->GetFencedFrames().at(0)->GetInnerRoot(); |
| |
| FrameTreeNode* ff2_node = ff2->frame_tree_node(); |
| scoped_refptr<SiteInstance> nested_ff_site_instance = ff2->GetSiteInstance(); |
| |
| // Disable nested fenced frame untrusted network access. |
| EXPECT_TRUE(ExecJs(ff2, R"( |
| (async () => { |
| return window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| VerifyFencedFrameNetworkStatus(ff1, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus( |
| ff2, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| |
| const GURL navigate_url = |
| https_server()->GetURL("b.test", "/fenced_frames/basic.html"); |
| |
| // Set up navigation and console observers. |
| NavigationHandleObserver handle_observer(web_contents(), navigate_url); |
| TestFrameNavigationObserver load_observer(ff2); |
| WebContentsConsoleObserver console_observer(web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kError; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern("*network access has been disabled*"); |
| |
| // Embedder initiates nested fenced frame navigation. |
| EXPECT_TRUE(ExecJs( |
| ff1, JsReplace( |
| R"(document.getElementsByTagName('fencedframe')[0].config = |
| new FencedFrameConfig($1);)", |
| navigate_url))); |
| |
| // Wait for commit. The in progress embedder initiated navigation is not |
| // cancelled because the parent fenced frame does not have untrusted network |
| // access disabled. |
| load_observer.WaitForCommit(); |
| EXPECT_TRUE(load_observer.last_navigation_succeeded()); |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_EQ(handle_observer.last_committed_url(), navigate_url); |
| EXPECT_EQ(net::OK, handle_observer.net_error_code()); |
| |
| // After the nested fenced frame is navigated to a new config, both fenced |
| // frames do not have network revoked. |
| VerifyFencedFrameNetworkStatus(ff1, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| VerifyFencedFrameNetworkStatus(ff2_node, |
| DisableUntrustedNetworkStatus::kNotStarted); |
| |
| // An embedder-initiated fenced frame navigation through a fenced frame config |
| // will use a new SiteInstance in a different BrowsingInstance. |
| SiteInstance* post_navigation_site_instance = |
| ff2_node->current_frame_host()->GetSiteInstance(); |
| EXPECT_NE(nested_ff_site_instance, post_navigation_site_instance); |
| EXPECT_FALSE(nested_ff_site_instance->IsRelatedSiteInstance( |
| post_navigation_site_instance)); |
| |
| // No console error should be shown. |
| EXPECT_TRUE(console_observer.messages().empty()); |
| } |
| |
| // This test exercises this scenario: |
| // 1. A child urn iframe nested in a parent FF. |
| // 2. Parent FF initiates a navigation of child urn iframe to a new urn. |
| // 3. Parent FF disables untrusted network. |
| // 4. The in-progress child urn iframe navigation should commit an error page. |
| // |
| // Note the navigation commits an error page not because of the check in |
| // `NavigationRequest::IsDisabledEmbedderInitiatedFencedFrameNavigation` which |
| // only applies to fenced frame. It is because of the check in |
| // `CorsURLLoaderFactory::CreateLoaderAndStart` which iterates over all active |
| // requests and commits those matching the nonce whose network is disabled to |
| // an error page. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameParameterizedBrowserTest, |
| ParentFencedFrameDisablesNetworkCancelNestedUrnIframeNavigation) { |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Create parent fenced frame. |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title0.html"); |
| RenderFrameHost* fenced_frame_rfh = |
| fenced_frame_test_helper().CreateFencedFrame( |
| primary_main_frame_host(), fenced_frame_url, net::OK, |
| blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds); |
| |
| // Create nested urn iframe. |
| GURL nested_iframe_url( |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html")); |
| |
| // Create nested iframe. |
| EXPECT_EQ(0U, |
| static_cast<RenderFrameHostImpl*>(fenced_frame_rfh)->child_count()); |
| FrameTreeNode* nested_iframe_node = AddIframeInFencedFrame( |
| static_cast<RenderFrameHostImpl*>(fenced_frame_rfh)->frame_tree_node(), |
| 0); |
| EXPECT_TRUE(nested_iframe_node); |
| |
| // Add the nested iframe url to fenced frame url mapping. |
| FencedFrameURLMapping& url_mapping = |
| static_cast<RenderFrameHostImpl*>(fenced_frame_rfh) |
| ->GetPage() |
| .fenced_frame_urls_map(); |
| auto nested_iframe_urn_uuid = |
| test::AddAndVerifyFencedFrameURL(&url_mapping, nested_iframe_url); |
| |
| // Navigate the nested urn iframe to the urn. |
| NavigateIframeInFencedFrame( |
| static_cast<RenderFrameHostImpl*>(fenced_frame_rfh)->child_at(0), |
| nested_iframe_urn_uuid); |
| EXPECT_EQ(nested_iframe_url, |
| static_cast<RenderFrameHostImpl*>(fenced_frame_rfh) |
| ->child_at(0) |
| ->current_frame_host() |
| ->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(nested_iframe_url), |
| static_cast<RenderFrameHostImpl*>(fenced_frame_rfh) |
| ->child_at(0) |
| ->current_frame_host() |
| ->GetLastCommittedOrigin()); |
| |
| // Add the navigation url to fenced frame url mapping. |
| const GURL navigate_url = |
| https_server()->GetURL("b.test", "/fenced_frames/basic.html"); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, navigate_url); |
| |
| // Set up navigation and console observers. |
| NavigationHandleObserver handle_observer(web_contents(), navigate_url); |
| TestFrameNavigationObserver load_observer(nested_iframe_node); |
| WebContentsConsoleObserver console_observer(web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kError; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern("*network access has been disabled*"); |
| |
| // Callback will be invoked after navigation starts. It disables parent fenced |
| // frame's untrusted network access. |
| DidStartNavigationCallback callback( |
| web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| // Disable untrusted network for fenced frame. The promise will not |
| // resolve due to the ongoing navigation in the nested iframe. |
| EXPECT_EQ(EvalJs(fenced_frame_rfh, R"( |
| var ff1_promise_resolved = false; |
| (async () => { |
| let timeout_promise = new Promise( |
| resolve => setTimeout(() => {resolve('timeout')}, 1000)); |
| let disable_network_promise = |
| window.fence.disableUntrustedNetwork().then( |
| () => {ff1_promise_resolved = true;}); |
| return Promise.race([disable_network_promise, timeout_promise]); |
| })(); |
| )"), |
| "timeout"); |
| |
| // The nonce should be marked as revoked for untrusted network access. |
| VerifyFencedFrameNetworkStatus( |
| fenced_frame_rfh, |
| DisableUntrustedNetworkStatus::kCurrentFrameTreeComplete); |
| EXPECT_FALSE( |
| EvalJs(fenced_frame_rfh, "ff1_promise_resolved").ExtractBool()); |
| })); |
| |
| // Embedder initiates nested urn iframe navigation. |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh, |
| JsReplace("iframe_within_ff.src = $1;", urn_uuid))); |
| |
| // Wait for load stops. |
| load_observer.Wait(); |
| |
| // The in progress embedder initiated navigation commits an error page because |
| // the parent fenced frame disables untrusted network access after the |
| // navigation starts. |
| EXPECT_FALSE(load_observer.last_navigation_succeeded()); |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_TRUE(handle_observer.is_error()); |
| EXPECT_EQ(handle_observer.net_error_code(), net::ERR_NETWORK_ACCESS_REVOKED); |
| |
| // No console error should be shown because the check in |
| // `CorsURLLoaderFactory::CreateLoaderAndStart` does not emit console errors. |
| EXPECT_TRUE(console_observer.messages().empty()); |
| |
| // There is no ongoing navigation in urn iframe, the promise should be |
| // resolved. |
| EXPECT_TRUE(EvalJs(fenced_frame_rfh, "ff1_promise_resolved").ExtractBool()); |
| VerifyFencedFrameNetworkStatus( |
| fenced_frame_rfh, |
| DisableUntrustedNetworkStatus::kCurrentAndDescendantFrameTreesComplete); |
| } |
| |
| class FencedFrameReportEventBrowserTest |
| : public FencedFrameParameterizedBrowserTest { |
| public: |
| // TODO(crbug.com/40053214): Disable window.fence.reportEvent in iframes. |
| // Remove this constructor and `scoped_feature_list_` once FLEDGE stops |
| // supporting iframes. |
| // Mode A/B is disabled to be able to test cross-origin reporting beacons. |
| FencedFrameReportEventBrowserTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| /*enabled_features=*/{{blink::features::kAllowURNsInIframes, {}}}, |
| /*disabled_features=*/{features::kCookieDeprecationFacilitatedTesting}); |
| } |
| |
| // 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 target a nested iframe rather than the |
| // fenced frame root. |
| bool is_target_nested_iframe = false; |
| // 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; |
| // Whether attribution-reporting permission policy is expected to be |
| // allowed. |
| bool expect_attribution_reporting_allowed = true; |
| // Whether the report should disregard the `event` field and instead |
| // send to a custom destination URL. |
| bool use_custom_destination_url = false; |
| |
| struct Event { |
| std::string type; |
| std::string reporting_destination; |
| // Optional `eventData` field for reportEvent. |
| // 1. If this is `std::nullopt`, reportEvent is called without the |
| // `eventData` field. |
| // 2. Otherwise, the event data is the given string appended with the |
| // `navigation_index` of each step. |
| std::optional<std::string> data; |
| bool cross_origin_exposed = false; |
| }; |
| struct Destination { |
| // The origin for the navigation. |
| std::string origin; |
| // The path for the resource to load. |
| std::string path; |
| }; |
| |
| // Specifies the reporting destination, event type and event data for |
| // reportEvent. |
| Event event{"click", "buyer", "click data"}; |
| |
| // The initial navigation destination (may be redirected). |
| Destination destination; |
| // A list of redirects that the navigation should take. The last redirect |
| // destination will be the ultimate destination of the navigation. |
| std::vector<Destination> redirects; |
| |
| // Specify the outcome of reportEvent. |
| enum class Result { |
| kSuccess, |
| kModeNotOpaque, |
| kCrossOrigin, |
| kNoMeta, |
| kNoDestination, |
| kNoReportingURL, |
| kInvalidReportingURL, |
| kExceedMaxEventDataLength, |
| kUntrustedNetworkDisabled, |
| kCrossOriginNoHeader, |
| kCrossOriginModeAB |
| }; |
| |
| // Outcome of reportEvent. |
| Result report_event_result = Result::kSuccess; |
| }; |
| |
| std::string GetErrorPattern(Step::Result result) { |
| switch (result) { |
| case Step::Result::kModeNotOpaque: |
| return "Fenced event reporting is only available in the 'opaque-ads' " |
| "mode."; |
| case Step::Result::kCrossOrigin: |
| return "Fenced event reporting is only available in same-origin " |
| "subframes."; |
| case Step::Result::kNoMeta: |
| return "This frame did not register reporting metadata."; |
| case Step::Result::kNoDestination: |
| return "This frame did not register reporting metadata for " |
| "destination *"; |
| case Step::Result::kNoReportingURL: |
| return "This frame did not register reporting url for destination * " |
| "and event_type *"; |
| case Step::Result::kInvalidReportingURL: |
| return "This frame registered invalid reporting url for destination * " |
| "and event_type *"; |
| case Step::Result::kExceedMaxEventDataLength: |
| return "The data provided to reportEvent() exceeds the maximum length, " |
| "which is 64KB."; |
| case Step::Result::kUntrustedNetworkDisabled: |
| return "Cannot send fenced frame event-level reports after " |
| "calling window.fence.disableUntrustedNetwork()."; |
| case Step::Result::kCrossOriginNoHeader: |
| return "This document is cross-origin to the document that contains " |
| "reporting metadata, but the fenced frame's document was not " |
| "served with the 'Allow-Cross-Origin-Event-Reporting' header."; |
| case Step::Result::kCrossOriginModeAB: |
| return "Cross-origin reporting beacons are not supported with Mode A/B " |
| "Chrome-facilitated testing traffic."; |
| default: |
| return ""; |
| } |
| } |
| |
| std::unique_ptr<net::test_server::BasicHttpResponse> |
| GetResponseWithAccessAllowHeaders( |
| const net::test_server::HttpRequest* request) { |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| |
| response->set_code(net::HTTP_OK); |
| response->AddCustomHeader(cors::kAccessControlAllowMethods, |
| request->method_string); |
| if (base::Contains(request->headers, "Origin")) { |
| response->AddCustomHeader(cors::kAccessControlAllowOrigin, "*"); |
| } |
| if (base::Contains(request->headers, cors::kAccessControlRequestHeaders)) { |
| response->AddCustomHeader( |
| cors::kAccessControlAllowHeaders, |
| request->headers.at(cors::kAccessControlRequestHeaders)); |
| } |
| |
| return response; |
| } |
| |
| scoped_refptr<FencedFrameReporter> CreateFencedFrameReporter() { |
| return FencedFrameReporter::CreateForFledge( |
| web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(), |
| web_contents()->GetBrowserContext(), |
| /*direct_seller_is_seller=*/false, |
| PrivateAggregationManager::GetManager( |
| *web_contents()->GetBrowserContext()), |
| /*main_frame_origin=*/ |
| web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(), |
| /*winner_origin=*/url::Origin::Create(GURL("https://a.test")), |
| /*winner_aggregation_coordinator_origin=*/std::nullopt, |
| /*allowed_reporting_origins=*/ |
| {{url::Origin::Create(https_server()->GetURL("a.test", "/")), |
| url::Origin::Create(https_server()->GetURL("b.test", "/")), |
| url::Origin::Create(https_server()->GetURL("c.test", "/"))}}); |
| } |
| |
| // 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; |
| |
| std::string reporting_origin = "c.test"; |
| // 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) { |
| responses.emplace_back( |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| https_server(), kReportingURL)); |
| if (step.is_target_nested_iframe) { |
| ASSERT_FALSE(step.is_embedder_initiated); |
| ASSERT_FALSE(step.is_opaque); |
| } |
| ASSERT_FALSE(step.destination.origin.empty()); |
| ASSERT_FALSE(step.destination.path.empty()); |
| int redirect_index = 0; |
| for (auto& redirect_destination : step.redirects) { |
| ASSERT_FALSE(base::Contains(paths, redirect_destination.path)); |
| ASSERT_FALSE(redirect_destination.origin.empty()); |
| ASSERT_FALSE(redirect_destination.path.empty()); |
| paths.insert(redirect_destination.path); |
| |
| // Intercept the previous navigation destination in the chain. |
| std::string previous_path = |
| redirect_index ? step.redirects[redirect_index - 1].path |
| : step.destination.path; |
| redirects.emplace_back( |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| https_server(), previous_path)); |
| redirect_index++; |
| } |
| } |
| } |
| // An additional response is used to check any spurious waiting reported |
| // events. |
| responses.emplace_back( |
| std::make_unique<net::test_server::ControllableHttpResponse>( |
| https_server(), kReportingURL)); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Set up the document.cookie. We will later verify that this is not sent |
| // with the reportEvent() beacon. |
| // TODO(crbug.com/40286778): Remove this block after 3PCD. |
| GURL reporting_cookie_url = |
| https_server()->GetURL(reporting_origin, "/hello.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), reporting_cookie_url)); |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| EXPECT_TRUE(ExecJs( |
| root, "document.cookie = 'name=foobarbaz; SameSite=None; Secure';")); |
| |
| // Set up the embedder and a fenced frame. |
| GURL main_url = https_server()->GetURL("a.test", "/hello.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| 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()); |
| |
| // Create a FencedFrameReporter and pass it reporting metadata. |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| GURL reporting_url( |
| https_server()->GetURL("c.test", "/_report_event_server.html")); |
| url::Origin reporting_worklet_origin = |
| url::Origin::Create(GURL(https_server()->GetURL("d.test", "/"))); |
| // Set valid reporting metadata for buyer. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| reporting_worklet_origin, |
| { |
| {"click", reporting_url}, |
| }, |
| /*reporting_ad_macros=*/FencedFrameReporter::ReportingMacros()); |
| // Set empty reporting url for seller. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kSeller, |
| reporting_worklet_origin, {{"click", GURL()}}); |
| // Set no reporting urls for component seller. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kComponentSeller, |
| reporting_worklet_origin, {}); |
| |
| // Get the urn mapping object. |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| |
| // Create a holder for a nested iframe. |
| std::optional<FrameTreeNode*> nested_iframe_node = std::nullopt; |
| |
| 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.destination.origin, |
| step.destination.path); |
| GURL expect_url = navigate_url; |
| if (step.is_opaque) { |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL( |
| &url_mapping, navigate_url, fenced_frame_reporter); |
| navigate_url = urn_uuid; |
| } |
| FrameTreeNode* navigation_target_node = fenced_frame_root_node; |
| |
| // Add a nested iframe inside the fenced frame if necessary (or clear the |
| // handle to it, if the navigation will remove it). |
| if (step.is_target_nested_iframe) { |
| if (!nested_iframe_node) { |
| EXPECT_TRUE( |
| ExecJs(fenced_frame_root_node, |
| "var iframe_within_ff = document.createElement('iframe');" |
| "document.body.appendChild(iframe_within_ff);")); |
| EXPECT_EQ(1U, fenced_frame_root_node->child_count()); |
| nested_iframe_node = fenced_frame_root_node->child_at(0); |
| } |
| navigation_target_node = *nested_iframe_node; |
| } else { |
| nested_iframe_node = std::nullopt; |
| } |
| |
| // Initiate the navigation. |
| TestFrameNavigationObserver target_observer(navigation_target_node); |
| if (step.is_target_nested_iframe) { |
| EXPECT_TRUE( |
| ExecJs(fenced_frame_root_node, |
| JsReplace("iframe_within_ff.src = $1", navigate_url))); |
| } else if (step.is_embedder_initiated) { |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace("fenced_frame.config = new FencedFrameConfig($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_destination : step.redirects) { |
| GURL redirect_url = https_server()->GetURL(redirect_destination.origin, |
| redirect_destination.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. |
| target_observer.WaitForCommit(); |
| EXPECT_EQ( |
| expect_url, |
| navigation_target_node->current_frame_host()->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(expect_url), |
| navigation_target_node->current_frame_host() |
| ->GetLastCommittedOrigin()); |
| navigation_index++; |
| |
| // Monitor the console warnings. |
| WebContentsConsoleObserver console_observer(web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return (message.log_level == |
| blink::mojom::ConsoleMessageLevel::kError) || |
| (message.log_level == |
| blink::mojom::ConsoleMessageLevel::kWarning); |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| if (step.report_event_result != Step::Result::kSuccess) { |
| console_observer.SetPattern(GetErrorPattern(step.report_event_result)); |
| } |
| |
| if (step.report_event_result == Step::Result::kUntrustedNetworkDisabled) { |
| EXPECT_TRUE(ExecJs(navigation_target_node, R"( |
| window.fence.disableUntrustedNetwork(); |
| )")); |
| } |
| |
| // Perform the reportEvent call, with a unique body. |
| if (step.use_custom_destination_url) { |
| // Call reportEvent to a custom `destinationURL`. |
| EXPECT_TRUE(ExecJs( |
| navigation_target_node, |
| JsReplace(R"( |
| window.fence.reportEvent({ |
| destinationURL: $1, |
| crossOriginExposed: $2 |
| }); |
| )", |
| https_server()->GetURL("c.test", kReportingURL).spec(), |
| step.event.cross_origin_exposed))); |
| |
| } else if (!step.event.data) { |
| // Call reportEvent without `eventData` field. |
| EXPECT_TRUE( |
| ExecJs(navigation_target_node, |
| JsReplace(R"( |
| window.fence.reportEvent({ |
| eventType: $1, |
| destination: [$2], |
| crossOriginExposed: $3 |
| }); |
| )", |
| step.event.type, step.event.reporting_destination, |
| step.event.cross_origin_exposed))); |
| } else { |
| // Call reportEvent with `eventData`. |
| EvalJsResult result = |
| EvalJs(navigation_target_node, |
| JsReplace(R"( |
| window.fence.reportEvent({ |
| eventType: $1, |
| eventData: $3 + ' $4', |
| destination: [$2], |
| crossOriginExposed: $5 |
| }); |
| )", |
| step.event.type, step.event.reporting_destination, |
| step.event.data.value(), navigation_index, |
| step.event.cross_origin_exposed)); |
| |
| if (step.report_event_result == |
| Step::Result::kExceedMaxEventDataLength) { |
| // When eventData exceeds the length limit, a security error is thrown |
| // instead of a console error. |
| EXPECT_FALSE(result.error.empty()); |
| EXPECT_THAT( |
| result.error, |
| testing::HasSubstr(GetErrorPattern(step.report_event_result))); |
| continue; |
| } |
| |
| EXPECT_TRUE(result.error.empty()); |
| } |
| |
| // If relevant, check that the event report succeeded. |
| if (step.report_event_result == Step::Result::kSuccess) { |
| auto& response = *responses[response_index]; |
| response.WaitForRequest(); |
| |
| // Verify the request has the correct content. |
| if (step.use_custom_destination_url) { |
| EXPECT_EQ(response.http_request()->method, |
| net::test_server::METHOD_GET); |
| // For custom destination URL reports, the request initiator should |
| // not be the worklet origin. |
| EXPECT_NE(response.http_request()->headers.at("Origin"), |
| reporting_worklet_origin.Serialize()); |
| } else { |
| if (!step.event.data) { |
| EXPECT_TRUE(response.http_request()->content.empty()); |
| } else { |
| EXPECT_EQ(response.http_request()->content, |
| step.event.data.value() + " " + |
| base::NumberToString(navigation_index)); |
| } |
| // For preregistered URL reports, the request initiator should be the |
| // worklet origin. |
| EXPECT_EQ(response.http_request()->headers.at("Origin"), |
| reporting_worklet_origin.Serialize()); |
| } |
| // Verify the request contains the correct referrer. |
| EXPECT_EQ(response.http_request()->headers.at("Referer"), |
| navigation_target_node->current_frame_host() |
| ->GetLastCommittedOrigin() |
| .GetURL()); |
| // Verify the request contains the eligibility header. |
| if (step.expect_attribution_reporting_allowed) { |
| ExpectValidAttributionReportingEligibleHeaderForEventBeacon( |
| response.http_request()->headers.at( |
| "Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| response.http_request()->headers.at( |
| "Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/false); |
| } else { |
| EXPECT_FALSE(base::Contains(response.http_request()->headers, |
| "Attribution-Reporting-Eligible")); |
| EXPECT_FALSE(base::Contains(response.http_request()->headers, |
| "Attribution-Reporting-Support")); |
| } |
| |
| // TODO(crbug.com/40286778): Remove this check after 3PCD. |
| EXPECT_EQ(0U, response.http_request()->headers.count("Cookie")); |
| response.Done(); |
| ++response_index; |
| } else { |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| EXPECT_EQ(console_observer.messages().size(), 1u); |
| } |
| } |
| |
| // Check for any spurious waiting reported events. |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace("fenced_frame.config = new FencedFrameConfig($1)", |
| reporting_url))); |
| auto& response = *responses[response_index]; |
| response.WaitForRequest(); |
| EXPECT_EQ(response.http_request()->content, ""); |
| response.Done(); |
| // Ensures that the config's FencedFrameReporter is deleted on subsequent |
| // navigation. Used to test histograms that are logged in the |
| // FencedFrameReporter's destructor. |
| url_mapping.ClearMapForTesting(); |
| } |
| |
| private: |
| // Server must start after ControllableHttpResponse object being constructed. |
| void AssertServerStart() override {} |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Fenced frame not in opaque-ads mode should fail reportEvent(). |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventNonOpaqueAdsMode) { |
| net::test_server::ControllableHttpResponse response(https_server(), |
| kReportingURL); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Set up the embedder and a default mode 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');" |
| "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()); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kError; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern(GetErrorPattern(Step::Result::kNoMeta)); |
| |
| // Perform the reportEvent call, with a unique body. |
| const char report_event_script[] = R"( |
| window.fence.reportEvent({ |
| eventType: 'click', |
| eventData: 'click 0', |
| destination: ['buyer'], |
| }); |
| )"; |
| EXPECT_TRUE(ExecJs(fenced_frame_root_node, report_event_script)); |
| |
| // Check console warning. |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| EXPECT_EQ(console_observer.messages().size(), 1u); |
| |
| // Check that the request received is from `SendBasicRequest`. This implies |
| // the reporting beacon from `window.fence.reportEvent` was not sent as |
| // expected. |
| fenced_frame_test_helper().SendBasicRequest( |
| web_contents(), https_server()->GetURL("c.test", kReportingURL), |
| "response"); |
| response.WaitForRequest(); |
| EXPECT_TRUE(response.has_received_request()); |
| EXPECT_EQ(response.http_request()->content, "response"); |
| } |
| |
| // The simplest test case: URN navigation into reportEvent. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventEmbedderURNNavigation) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // reportEvent shouldn't work if `window.fence.disableUntrustedNetwork` has been |
| // called in a fenced frame. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventDisableUntrustedNetwork) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kUntrustedNetworkDisabled, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // The `eventData` field of `fence.reportEvent` should be optional. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventWithoutEventData) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .event = {/*type=*/"click", /*reporting_destination=*/"buyer", |
| /*data=*/std::nullopt}, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // The `eventData` field should not exceed the limit of 64KB. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventEventDataExceedsLengthLimit) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .event = {/*type=*/"click", /*reporting_destination=*/"buyer", |
| /*data=*/ |
| std::string(blink::kFencedFrameMaxBeaconLength + 1, '*')}, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kExceedMaxEventDataLength, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // reportEvent shouldn't work if there is no associated reporting metadata with |
| // the reporting destination. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventNoMetadataForReportingDestination) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .event = {/*type=*/"click", |
| /*reporting_destination=*/"component-seller", |
| /*data=*/"click data"}, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kNoDestination, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // reportEvent shouldn't work if there is no associated reporting url with |
| // the event type and the reporting destination. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventNoReportingURLForEventType) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .event = {/*type=*/"invalid-event", /*reporting_destination=*/"buyer", |
| /*data=*/"click data"}, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kNoReportingURL, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // reportEvent shouldn't work if the reporting url is invalid. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventInvalidReportingURL) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .event = {/*type=*/"click", /*reporting_destination=*/"seller", |
| /*data=*/"click data"}, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kInvalidReportingURL, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // reportEvent should work in subframes that are same-origin to the most recent |
| // embedder-initiated committed url in the fenced frame, regardless of the |
| // fenced frame root's current url. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventNestedIframeSameOriginNavigation) { |
| base::HistogramTester histogram_tester; |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| |
| // Navigate the page away so that the FencedFrameReporter destructor runs and |
| // logs the relevant histograms. |
| GURL new_url = https_server()->GetURL("c.test", "/hello.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), new_url)); |
| histogram_tester.ExpectUniqueSample( |
| blink::kFencedFrameBeaconReportingCountUMA, 3, 1); |
| histogram_tester.ExpectUniqueSample( |
| blink::kFencedFrameBeaconReportingCountCrossOriginUMA, 0, 1); |
| } |
| |
| // reportEvent shouldn't work in subframes that are cross-origin to the most |
| // recent embedder-initiated committed url in the fenced frame, regardless of |
| // the fenced frame root's current url. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventNestedIframeCrossOriginNavigation) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Reporting metadata should persist across FF-initiated same-origin |
| // navigations. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventFFSameOriginNavigation) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .destination = {"a.test", "/fenced_frames/title1.html?foo"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Reporting metadata should be dropped upon cross-origin navigations, |
| // but come back upon new URN navigations. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventFFCrossOriginNavigation) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Embedder-initiated URL navigations should always be considered cross-origin. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventEmbedderURLNavigation) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = false, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kNoMeta, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Same-origin redirects in the initial URN navigation shouldn't affect |
| // reporting metadata. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventEmbedderSameOriginRedirect) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/redirect1.html"}, |
| .redirects = |
| { |
| {"a.test", "/fenced_frames/redirect2.html"}, |
| {"a.test", "/fenced_frames/title1.html"}, |
| }, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Cross-origin redirects in the initial URN navigation shouldn't affect |
| // reporting metadata either. The final URL in the redirect chain should be the |
| // one used for subsequent same- or cross- origin checks. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventEmbedderCrossOriginRedirect) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/redirect1.html"}, |
| .redirects = |
| { |
| {"b.test", "/fenced_frames/redirect2.html"}, |
| {"c.test", "/fenced_frames/title1.html"}, |
| }, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .destination = {"c.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Metadata should be preserved as long as the final URL in a FF-initiated |
| // redirect chain is same-origin. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventFFSameOriginInterveningRedirect) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .destination = {"a.test", "/fenced_frames/redirect1.html"}, |
| .redirects = |
| { |
| {"a.test", "/fenced_frames/redirect2.html"}, |
| {"a.test", "/fenced_frames/title1.html?foo"}, |
| }, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Metadata should be preserved as long as the final URL in an FF-initiated |
| // redirect chain is same-origin. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventFFCrossOriginInterveningRedirect) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .destination = {"a.test", "/fenced_frames/redirect1.html"}, |
| .redirects = |
| { |
| {"b.test", "/fenced_frames/redirect2.html"}, |
| {"a.test", "/fenced_frames/title1.html"}, |
| }, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Attribution Reporting headers are not set if attribution-reporting permission |
| // policy is disallowed for the fenced frame. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventAttributionReportingDisallowed) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .expect_attribution_reporting_allowed = false, |
| .destination = |
| {"a.test", |
| "/fenced_frames/attribution_reporting_disallowed.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Attribution Reporting headers are not set if attribution-reporting permission |
| // policy is disallowed for the nested iframe. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventNestedIframeAttributionReportingDisallowed) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .expect_attribution_reporting_allowed = false, |
| .destination = |
| {"a.test", |
| "/fenced_frames/attribution_reporting_disallowed.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Tests for reportEvent to a custom destinationURL: |
| |
| // The simplest test case: URN navigation into reportEvent. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventCustomURLEmbedderURNNavigation) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .use_custom_destination_url = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // reportEvent should work in subframes that are same-origin to the most recent |
| // embedder-initiated committed url in the fenced frame, regardless of the |
| // fenced frame root's current url. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventCustomURLNestedIframeSameOriginNavigation) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .use_custom_destination_url = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .use_custom_destination_url = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .use_custom_destination_url = true, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .use_custom_destination_url = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // reportEvent shouldn't work in subframes that are cross-origin to the most |
| // recent embedder-initiated committed url in the fenced frame, regardless of |
| // the fenced frame root's current url. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventCustomURLNestedIframeCrossOriginNavigation) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .use_custom_destination_url = true, |
| .destination = {"a.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .use_custom_destination_url = true, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .use_custom_destination_url = true, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .use_custom_destination_url = true, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginNoHeader, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Attribution Reporting headers are not set if attribution-reporting permission |
| // policy is disallowed for the fenced frame. |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventBrowserTest, |
| FencedFrameReportEventCustomURLAttributionReportingDisallowed) { |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .expect_attribution_reporting_allowed = false, |
| .use_custom_destination_url = true, |
| .destination = |
| {"a.test", |
| "/fenced_frames/attribution_reporting_disallowed.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| 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/40053214): Disable window.fence.reportEvent in iframes. |
| // Remove this test once the FLEDGE origin trial stops supporting iframes. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| IframeReportingMetadata) { |
| net::test_server::ControllableHttpResponse reporting_response(https_server(), |
| kReportingURL); |
| 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); |
| |
| // Create a FencedFrameReporter and pass it reporting metadata. |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| GURL reporting_url(https_server()->GetURL("c.test", kReportingURL)); |
| // Set valid reporting metadata for buyer. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(GURL()), |
| {{"mouse interaction", reporting_url}, |
| {"click", https_server()->GetURL("c.test", "/title1.html")}}); |
| // Set empty reporting url for seller and component seller. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kSeller, |
| url::Origin::Create(GURL()), {}); |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kComponentSeller, |
| url::Origin::Create(GURL()), {}); |
| |
| GURL https_url( |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html")); |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url, |
| fenced_frame_reporter); |
| |
| TestFencedFrameURLMappingResultObserver mapping_observer; |
| url_mapping.ConvertFencedFrameURNToURL(urn_uuid, &mapping_observer); |
| TestFrameNavigationObserver observer(iframe_node); |
| |
| EXPECT_TRUE(ExecJs(root, JsReplace("f.src = $1;", urn_uuid))); |
| |
| observer.WaitForCommit(); |
| EXPECT_TRUE(mapping_observer.mapping_complete_observed()); |
| EXPECT_EQ(fenced_frame_reporter, mapping_observer.fenced_frame_reporter()); |
| |
| 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))); |
| |
| reporting_response.WaitForRequest(); |
| // Verify the request has the correct content. |
| EXPECT_EQ(reporting_response.http_request()->content, event_data); |
| // Verify the request contains the eligibility header. |
| ExpectValidAttributionReportingEligibleHeaderForEventBeacon( |
| reporting_response.http_request()->headers.at( |
| "Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| reporting_response.http_request()->headers.at( |
| "Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/false); |
| } |
| |
| // The reportEvent beacon is a POST request. Upon receiving a 302 redirect |
| // response, the request is changed to a GET request. In this test case, the |
| // reporting url is same-origin. There are no preflight requests. |
| // 1. A POST request is sent to the reporting destination. |
| // 2. A response with 302 redirect is sent back to the requester. |
| // 3. A GET request is sent to the redirected destination. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| SameOriginReportEventPost302RedirectGet) { |
| net::test_server::ControllableHttpResponse response(https_server(), |
| kReportingURL); |
| net::test_server::ControllableHttpResponse redirect_response( |
| https_server(), "/redirect.html"); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Set up the embedder and a default mode 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');" |
| "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")); |
| |
| // Create a FencedFrameReporter and pass it reporting metadata. |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| GURL reporting_url(https_server()->GetURL("a.test", kReportingURL)); |
| // Set valid reporting metadata for buyer. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(https_url), {{"click", reporting_url}}); |
| |
| // Get the urn mapping object. |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| |
| // Add url and its reporting metadata to fenced frame url mapping. |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url, |
| fenced_frame_reporter); |
| |
| TestFencedFrameURLMappingResultObserver mapping_observer; |
| url_mapping.ConvertFencedFrameURNToURL(urn_uuid, &mapping_observer); |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node->current_frame_host()); |
| |
| // Navigate the fenced frame. |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid))); |
| |
| observer.WaitForCommit(); |
| EXPECT_TRUE(mapping_observer.mapping_complete_observed()); |
| EXPECT_EQ(fenced_frame_reporter, mapping_observer.fenced_frame_reporter()); |
| |
| // Perform the reportEvent call, with a unique body. |
| std::string event_data = "this is a click"; |
| std::string report_event_script = JsReplace(R"( |
| window.fence.reportEvent({ |
| eventType: 'click', |
| eventData: $1, |
| destination: ['buyer'], |
| }); |
| )", |
| event_data); |
| EXPECT_TRUE(ExecJs(fenced_frame_root_node, report_event_script)); |
| |
| { |
| // Verify the reporting request. |
| response.WaitForRequest(); |
| EXPECT_EQ(response.http_request()->content, event_data); |
| EXPECT_EQ(response.http_request()->method, |
| net::test_server::HttpMethod::METHOD_POST); |
| ExpectValidAttributionReportingEligibleHeaderForEventBeacon( |
| response.http_request()->headers.at("Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| response.http_request()->headers.at("Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/false); |
| EXPECT_TRUE( |
| base::Contains(response.http_request()->headers, "Content-Length")); |
| EXPECT_TRUE( |
| base::Contains(response.http_request()->headers, "Content-Type")); |
| EXPECT_TRUE(base::Contains(response.http_request()->headers, "Origin")); |
| |
| // Send 302 redirect response. |
| GURL redirect_url = https_server()->GetURL("a.test", "/redirect.html"); |
| response.Send( |
| /*http_status=*/net::HTTP_FOUND, |
| /*content_type=*/"text/plain;charset=UTF-8", |
| /*content=*/{}, /*cookies=*/{}, /*extra_headers=*/ |
| {base::StrCat({"Location: ", redirect_url.spec()})}); |
| |
| response.Done(); |
| } |
| |
| { |
| // Verify the redirect request is a GET request. |
| redirect_response.WaitForRequest(); |
| EXPECT_EQ(redirect_response.http_request()->method, |
| net::test_server::HttpMethod::METHOD_GET); |
| // Check that POST-specific headers were stripped. |
| EXPECT_FALSE(base::Contains(redirect_response.http_request()->headers, |
| "Content-Length")); |
| EXPECT_FALSE(base::Contains(redirect_response.http_request()->headers, |
| "Content-Type")); |
| EXPECT_FALSE( |
| base::Contains(redirect_response.http_request()->headers, "Origin")); |
| // Check that the content body was stripped. |
| EXPECT_TRUE(redirect_response.http_request()->content.empty()); |
| // These extra request headers were not stripped. |
| ExpectValidAttributionReportingEligibleHeaderForEventBeacon( |
| redirect_response.http_request()->headers.at( |
| "Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| response.http_request()->headers.at("Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/false); |
| } |
| } |
| |
| // The reportEvent beacon is a POST request. Upon receiving a 302 redirect |
| // response, the request is changed to a GET request. In this test case, the |
| // reporting url is cross-origin. There are no preflight requests. |
| // 1. A POST request is sent to the reporting destination. |
| // 2. A response with 302 redirect is sent back to the requester. |
| // 3. A GET request is sent to the redirected destination. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| CrossOriginReportEventPost302RedirectGet) { |
| net::test_server::ControllableHttpResponse reporting_response(https_server(), |
| kReportingURL); |
| net::test_server::ControllableHttpResponse redirect_response( |
| https_server(), "/redirect.html"); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Set up the embedder and a default mode 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');" |
| "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")); |
| |
| // Create a FencedFrameReporter and pass it reporting metadata. |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| GURL reporting_url(https_server()->GetURL("c.test", kReportingURL)); |
| // Set valid reporting metadata for buyer. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(GURL(https_url)), {{"click", reporting_url}}); |
| |
| // Get the urn mapping object. |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| |
| // Add url and its reporting metadata to fenced frame url mapping. |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url, |
| fenced_frame_reporter); |
| |
| TestFencedFrameURLMappingResultObserver mapping_observer; |
| url_mapping.ConvertFencedFrameURNToURL(urn_uuid, &mapping_observer); |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node->current_frame_host()); |
| |
| // Navigate the fenced frame. |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid))); |
| |
| observer.WaitForCommit(); |
| EXPECT_TRUE(mapping_observer.mapping_complete_observed()); |
| EXPECT_EQ(fenced_frame_reporter, mapping_observer.fenced_frame_reporter()); |
| |
| // Perform the reportEvent call, with a unique body. |
| std::string event_data = "this is a click"; |
| std::string report_event_script = JsReplace(R"( |
| window.fence.reportEvent({ |
| eventType: 'click', |
| eventData: $1, |
| destination: ['buyer'], |
| }); |
| )", |
| event_data); |
| EXPECT_TRUE(ExecJs(fenced_frame_root_node, report_event_script)); |
| |
| { |
| reporting_response.WaitForRequest(); |
| EXPECT_EQ(reporting_response.http_request()->method, |
| net::test_server::HttpMethod::METHOD_POST); |
| EXPECT_EQ(reporting_response.http_request()->content, event_data); |
| EXPECT_TRUE(base::Contains(reporting_response.http_request()->headers, |
| "Content-Length")); |
| EXPECT_TRUE(base::Contains(reporting_response.http_request()->headers, |
| "Content-Type")); |
| EXPECT_TRUE( |
| base::Contains(reporting_response.http_request()->headers, "Origin")); |
| ExpectValidAttributionReportingEligibleHeaderForEventBeacon( |
| reporting_response.http_request()->headers.at( |
| "Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| reporting_response.http_request()->headers.at( |
| "Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/false); |
| |
| // Send 302 redirect response, with "Access-Control-Allow-Origin" header. |
| // This header is needed to get the redirect through. |
| GURL redirect_url = https_server()->GetURL("d.test", "/redirect.html"); |
| reporting_response.Send( |
| /*http_status=*/net::HTTP_FOUND, |
| /*content_type=*/"text/plain;charset=UTF-8", |
| /*content=*/{}, /*cookies=*/{}, /*extra_headers=*/ |
| {base::StrCat({cors::kAccessControlAllowOrigin, ": *"}), |
| base::StrCat({"Location: ", redirect_url.spec()})}); |
| reporting_response.Done(); |
| } |
| |
| { |
| // Verify the redirect request is a GET request. |
| redirect_response.WaitForRequest(); |
| EXPECT_EQ(redirect_response.http_request()->method, |
| net::test_server::HttpMethod::METHOD_GET); |
| EXPECT_EQ(redirect_response.http_request()->headers.at("Origin"), "null"); |
| EXPECT_FALSE(base::Contains(redirect_response.http_request()->headers, |
| "Content-Length")); |
| EXPECT_EQ(redirect_response.http_request()->headers.at("Content-Type"), |
| "text/plain;charset=UTF-8"); |
| // Check that the content body was stripped. |
| EXPECT_TRUE(redirect_response.http_request()->content.empty()); |
| // These extra request headers were not stripped. |
| ExpectValidAttributionReportingEligibleHeaderForEventBeacon( |
| redirect_response.http_request()->headers.at( |
| "Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| redirect_response.http_request()->headers.at( |
| "Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/false); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| AttributionNoneSupported_EligibleHeaderNotSet) { |
| MockAttributionReportingContentBrowserClientBase< |
| ContentBrowserTestContentBrowserClient> |
| browser_client; |
| EXPECT_CALL( |
| browser_client, |
| GetAttributionSupport( |
| ContentBrowserClient::AttributionReportingOsApiState::kDisabled, |
| /*client_os_disabled=*/false)) |
| .WillRepeatedly( |
| testing::Return(network::mojom::AttributionSupport::kNone)); |
| ON_CALL(browser_client, IsPrivacySandboxReportingDestinationAttested) |
| .WillByDefault(testing::Return(true)); |
| |
| net::test_server::ControllableHttpResponse response(https_server(), |
| kReportingURL); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| 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');" |
| "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")); |
| |
| // Create a FencedFrameReporter and pass it reporting metadata. |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| GURL reporting_url(https_server()->GetURL("a.test", kReportingURL)); |
| // Set valid reporting metadata for buyer. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(GURL()), {{"click", reporting_url}}); |
| |
| // Get the urn mapping object. |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| |
| // Add url and its reporting metadata to fenced frame url mapping. |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url, |
| fenced_frame_reporter); |
| |
| TestFencedFrameURLMappingResultObserver mapping_observer; |
| url_mapping.ConvertFencedFrameURNToURL(urn_uuid, &mapping_observer); |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node->current_frame_host()); |
| |
| // Navigate the fenced frame. |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid))); |
| |
| observer.WaitForCommit(); |
| EXPECT_TRUE(mapping_observer.mapping_complete_observed()); |
| EXPECT_EQ(fenced_frame_reporter, mapping_observer.fenced_frame_reporter()); |
| |
| // Perform the reportEvent call, with a unique body. |
| std::string event_data = "this is a click"; |
| std::string report_event_script = JsReplace(R"( |
| window.fence.reportEvent({ |
| eventType: 'click', |
| eventData: $1, |
| destination: ['buyer'], |
| }); |
| )", |
| event_data); |
| EXPECT_TRUE(ExecJs(fenced_frame_root_node, report_event_script)); |
| |
| response.WaitForRequest(); |
| EXPECT_EQ(response.http_request()->content, event_data); |
| ExpectEmptyAttributionReportingEligibleHeader( |
| response.http_request()->headers.at("Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| response.http_request()->headers.at("Attribution-Reporting-Support"), |
| /*web_expected=*/false, |
| /*os_expected=*/false); |
| } |
| |
| // This test case covers the crash due to different implementations are used to |
| // get fenced frame properties at renderer side v.s. browser side. The test set |
| // up consists of three layers nested frames, from top to bottom: |
| // - A fenced frame loads an origin of "a.test", with reporting metadata. |
| // - An urn iframe loads an origin of "b.test", with reporting metadata. |
| // - An iframe loads origin of "a.test". |
| // |
| // At the time of crashing, `FrameTreeNode::GetFencedFrameProperties()` has |
| // two different behaviors, controlled by its parameter `source_node`. Plus |
| // feature `kAllowURNsInIframes` is enabled, so urn iframes are allowed. |
| // - When `source_node` is set to `kClosestAncestor`, the fenced frame |
| // properties are obtained by doing a bottom-up traversal from the frame tree |
| // node. |
| // - When it is set to `kFrameTreeRoot`, the fenced frame properties are |
| // obtained directly from the fenced frame tree root node if the node is in a |
| // fenced frame tree. Otherwise it performs a traversal just like the case |
| // above. |
| // |
| // In both cases if there is no fenced frame properties found in the end, the |
| // fenced frame properties of this frame tree node itself is returned. |
| // |
| // Crash happens when calling `reportEvent()` from the bottom iframe. |
| // The renderer gets fenced frame properties with `source_node` set to |
| // `kFrameTreeRoot`. The fenced frame properties are from the top-level fenced |
| // frame. However, at browser side, the fenced frame properties are obtained |
| // with `source_node` set to `kClosestAncestor`. The fenced frame properties are |
| // from the middle urn iframe. |
| // |
| // When reportEvent is called, renderer side checks will pass. But browser side |
| // checks will fail because the mapped url of the fenced frame properties is |
| // "b.test", which is cross-origin with the iframe origin "a.test". This results |
| // in a mojo bad message because browser assumes this error should be caught at |
| // renderer side before it reaches here. |
| // |
| // The solution is to let renderer call the getter with `source_node` |
| // set to `kClosestAncestor`. Now both renderer and browser should get the |
| // fenced frame properties from the nested urn iframe. Then the expected |
| // behavior is that the reportEvent call fails at renderer because there is no |
| // reporting metadata registered. If the nested iframe is navigated to "b.test", |
| // `reportEvent()` should succeed. |
| // |
| // See crbug.com/1470634. |
| // |
| // Note: If the urn iframe in the middle is an ad component, the nested iframe |
| // is not allowed to call `reportEvent()`. |
| // See test `ReportEventNotAllowedInNestedIframeUnderAdComponent` in |
| // `InterestGroupAdComponentAutomaticBeaconBrowserTest`. |
| // |
| // TODO(crbug.com/40060657): Once navigation support for urn::uuid in iframes is |
| // deprecated, this test should be removed. |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| GetFencedFramePropertiesShouldTraverseFrameTree) { |
| net::test_server::ControllableHttpResponse reporting_response(https_server(), |
| kReportingURL); |
| ASSERT_TRUE(https_server()->Start()); |
| 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(); |
| |
| // Top level fenced frame, origin is "a.test". |
| EXPECT_TRUE(ExecJs(root_rfh, |
| "var f = document.createElement('fencedframe');" |
| "document.body.appendChild(f);")); |
| EXPECT_EQ(1U, root_rfh->child_count()); |
| |
| // Add reporting metadata. |
| GURL reporting_url(https_server()->GetURL("a.test", kReportingURL)); |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(GURL()), {{"click", reporting_url}}); |
| GURL fenced_frame_url( |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html")); |
| |
| FencedFrameURLMapping& url_mapping = |
| root_rfh->GetPage().fenced_frame_urls_map(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL( |
| &url_mapping, fenced_frame_url, fenced_frame_reporter); |
| |
| 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()); |
| |
| // Navigate fenced frame. |
| NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad( |
| fenced_frame_root_node, |
| JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid)); |
| EXPECT_EQ( |
| fenced_frame_url, |
| fenced_frame_root_node->current_frame_host()->GetLastCommittedURL()); |
| EXPECT_EQ( |
| url::Origin::Create(fenced_frame_url), |
| fenced_frame_root_node->current_frame_host()->GetLastCommittedOrigin()); |
| |
| // Nested urn iframe, origin is "b.test". |
| GURL nested_iframe_url( |
| https_server()->GetURL("b.test", "/fenced_frames/title1.html")); |
| |
| // Add reporting metadata. |
| GURL nested_iframe_reporting_url( |
| https_server()->GetURL("b.test", kReportingURL)); |
| scoped_refptr<FencedFrameReporter> nested_iframe_reporter = |
| CreateFencedFrameReporter(); |
| nested_iframe_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(GURL()), {{"click", nested_iframe_reporting_url}}); |
| FencedFrameURLMapping& nested_iframe_url_mapping = |
| fenced_frame_root_node->current_frame_host() |
| ->GetPage() |
| .fenced_frame_urls_map(); |
| |
| // Generate urn. |
| auto nested_iframe_urn_uuid = test::AddAndVerifyFencedFrameURL( |
| &nested_iframe_url_mapping, nested_iframe_url, nested_iframe_reporter); |
| |
| EXPECT_EQ(0U, fenced_frame_root_node->child_count()); |
| FrameTreeNode* nested_iframe_node = |
| AddIframeInFencedFrame(fenced_frame_root_node, 0); |
| EXPECT_TRUE(nested_iframe_node); |
| |
| // Navigate the nested urn iframe. |
| NavigateIframeInFencedFrame(fenced_frame_root_node->child_at(0), |
| nested_iframe_urn_uuid); |
| |
| EXPECT_EQ(nested_iframe_url, fenced_frame_root_node->child_at(0) |
| ->current_frame_host() |
| ->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(nested_iframe_url), |
| fenced_frame_root_node->child_at(0) |
| ->current_frame_host() |
| ->GetLastCommittedOrigin()); |
| |
| // Bottom nested iframe, origin is "a.test". |
| GURL bottom_iframe_url( |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html")); |
| |
| EXPECT_EQ(0U, nested_iframe_node->child_count()); |
| FrameTreeNode* bottom_iframe_node = |
| AddIframeInFencedFrame(nested_iframe_node, 0); |
| |
| // Navigate the bottom iframe. |
| NavigateIframeInFencedFrame(nested_iframe_node->child_at(0), |
| bottom_iframe_url); |
| |
| EXPECT_EQ(bottom_iframe_url, nested_iframe_node->child_at(0) |
| ->current_frame_host() |
| ->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(bottom_iframe_url), |
| nested_iframe_node->child_at(0) |
| ->current_frame_host() |
| ->GetLastCommittedOrigin()); |
| |
| // Set up console error observer. |
| WebContentsConsoleObserver console_observer(web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kError; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern( |
| GetErrorPattern(Step::Result::kCrossOriginNoHeader)); |
| |
| // Expect reportEvent to fail because this frame is cross-origin with |
| // the middle urn iframe. |
| std::string event_data = "this is a click"; |
| std::string report_event_script = JsReplace(R"( |
| window.fence.reportEvent({ |
| eventType: 'click', |
| eventData: $1, |
| destination: ['buyer'], |
| }); |
| )", |
| event_data); |
| EXPECT_TRUE(ExecJs(bottom_iframe_node, report_event_script)); |
| |
| // Check console error. |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_EQ(console_observer.messages().size(), 1u); |
| |
| // Navigate the bottom iframe to "b.test". It then becomes same-origin with |
| // its parent urn iframe. |
| NavigateIframeInFencedFrame(nested_iframe_node->child_at(0), |
| nested_iframe_url); |
| |
| EXPECT_TRUE(ExecJs(bottom_iframe_node, report_event_script)); |
| |
| // Now `reportEvent()` should succeed. |
| reporting_response.WaitForRequest(); |
| EXPECT_EQ(reporting_response.http_request()->content, event_data); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| NestedIframeCrossOriginNavigationWithOptIn) { |
| base::HistogramTester histogram_tester; |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", |
| "/set-header" |
| "?Supports-Loading-Mode: fenced-frame" |
| "&Allow-Cross-Origin-Event-Reporting: ?1"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .event = {/*type=*/"click", /*reporting_destination=*/"buyer", |
| /*data=*/"data", /*cross_origin_exposed=*/true}, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| |
| // Navigate the page away so that the FencedFrameReporter destructor runs and |
| // logs the relevant histograms. |
| GURL new_url = https_server()->GetURL("c.test", "/hello.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), new_url)); |
| histogram_tester.ExpectUniqueSample( |
| blink::kFencedFrameBeaconReportingCountUMA, 1, 1); |
| histogram_tester.ExpectUniqueSample( |
| blink::kFencedFrameBeaconReportingCountCrossOriginUMA, 1, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(FencedFrameReportEventBrowserTest, |
| CustomURLNestedIframeCrossOriginNavigationWithOptIn) { |
| base::HistogramTester histogram_tester; |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .use_custom_destination_url = true, |
| .destination = {"a.test", |
| "/set-header" |
| "?Supports-Loading-Mode: fenced-frame" |
| "&Allow-Cross-Origin-Event-Reporting: ?1"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .use_custom_destination_url = true, |
| .event = {/*type=*/"N/a", /*reporting_destination=*/"N/a", |
| /*data=*/"data", /*cross_origin_exposed=*/true}, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| }; |
| RunTest(config); |
| |
| // Navigate the page away so that the FencedFrameReporter destructor runs and |
| // logs the relevant histograms. |
| GURL new_url = https_server()->GetURL("c.test", "/hello.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), new_url)); |
| histogram_tester.ExpectUniqueSample( |
| blink::kFencedFrameBeaconReportingCountUMA, 1, 1); |
| histogram_tester.ExpectUniqueSample( |
| blink::kFencedFrameBeaconReportingCountCrossOriginUMA, 1, 1); |
| } |
| |
| class FencedFrameReportEventAttributionCrossAppWebEnabledBrowserTest |
| : public FencedFrameReportEventBrowserTest { |
| public: |
| FencedFrameReportEventAttributionCrossAppWebEnabledBrowserTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| network::features::kAttributionReportingCrossAppWeb); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventAttributionCrossAppWebEnabledBrowserTest, |
| ReportEventSameOriginSetsSupportHeader) { |
| AttributionOsLevelManager::ScopedApiStateForTesting scoped_api_state_setting( |
| AttributionOsLevelManager::ApiState::kEnabled); |
| |
| net::test_server::ControllableHttpResponse response(https_server(), |
| kReportingURL); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Set up the embedder and a default mode 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');" |
| "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")); |
| |
| // Create a FencedFrameReporter and pass it reporting metadata. |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| GURL reporting_url(https_server()->GetURL("a.test", kReportingURL)); |
| // Set valid reporting metadata for buyer. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(GURL()), {{"click", reporting_url}}); |
| |
| // Get the urn mapping object. |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| |
| // Add url and its reporting metadata to fenced frame url mapping. |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url, |
| fenced_frame_reporter); |
| |
| TestFencedFrameURLMappingResultObserver mapping_observer; |
| url_mapping.ConvertFencedFrameURNToURL(urn_uuid, &mapping_observer); |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node->current_frame_host()); |
| |
| // Navigate the fenced frame. |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid))); |
| |
| observer.WaitForCommit(); |
| EXPECT_TRUE(mapping_observer.mapping_complete_observed()); |
| EXPECT_EQ(fenced_frame_reporter, mapping_observer.fenced_frame_reporter()); |
| |
| // Perform the reportEvent call, with a unique body. |
| std::string event_data = "this is a click"; |
| std::string report_event_script = JsReplace(R"( |
| window.fence.reportEvent({ |
| eventType: 'click', |
| eventData: $1, |
| destination: ['buyer'], |
| }); |
| )", |
| event_data); |
| EXPECT_TRUE(ExecJs(fenced_frame_root_node, report_event_script)); |
| |
| // Verify the request contains the eligibility header. |
| response.WaitForRequest(); |
| EXPECT_EQ(response.http_request()->content, event_data); |
| ExpectValidAttributionReportingEligibleHeaderForEventBeacon( |
| response.http_request()->headers.at("Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| response.http_request()->headers.at("Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/true); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventAttributionCrossAppWebEnabledBrowserTest, |
| ReportEventCrossOriginSetsSupportHeader) { |
| net::test_server::ControllableHttpResponse reporting_response(https_server(), |
| kReportingURL); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Set up the embedder and a default mode 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');" |
| "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")); |
| |
| // Create a FencedFrameReporter and pass it reporting metadata. |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| GURL reporting_url(https_server()->GetURL("c.test", kReportingURL)); |
| // Set valid reporting metadata for buyer. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(GURL()), {{"click", reporting_url}}); |
| |
| // Get the urn mapping object. |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| |
| // Add url and its reporting metadata to fenced frame url mapping. |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, https_url, |
| fenced_frame_reporter); |
| |
| TestFencedFrameURLMappingResultObserver mapping_observer; |
| url_mapping.ConvertFencedFrameURNToURL(urn_uuid, &mapping_observer); |
| TestFrameNavigationObserver observer( |
| fenced_frame_root_node->current_frame_host()); |
| |
| // Navigate the fenced frame. |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace("f.config = new FencedFrameConfig($1);", urn_uuid))); |
| |
| observer.WaitForCommit(); |
| EXPECT_TRUE(mapping_observer.mapping_complete_observed()); |
| EXPECT_EQ(fenced_frame_reporter, mapping_observer.fenced_frame_reporter()); |
| |
| // Perform the reportEvent call, with a unique body. |
| std::string event_data = "this is a click"; |
| std::string report_event_script = JsReplace(R"( |
| window.fence.reportEvent({ |
| eventType: 'click', |
| eventData: $1, |
| destination: ['buyer'], |
| }); |
| )", |
| event_data); |
| EXPECT_TRUE(ExecJs(fenced_frame_root_node, report_event_script)); |
| |
| // Verify the request contains the eligibility header. |
| { |
| reporting_response.WaitForRequest(); |
| EXPECT_EQ(reporting_response.http_request()->content, event_data); |
| ExpectValidAttributionReportingEligibleHeaderForEventBeacon( |
| reporting_response.http_request()->headers.at( |
| "Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| reporting_response.http_request()->headers.at( |
| "Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/false); |
| } |
| } |
| |
| class FencedFrameReportEventFacilitatedTestingEnabledBrowserTest |
| : public FencedFrameReportEventBrowserTest { |
| public: |
| FencedFrameReportEventFacilitatedTestingEnabledBrowserTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| features::kCookieDeprecationFacilitatedTesting); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventFacilitatedTestingEnabledBrowserTest, |
| NestedIframeCrossOriginNavigationWithOptIn) { |
| base::HistogramTester histogram_tester; |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .destination = {"a.test", |
| "/set-header" |
| "?Supports-Loading-Mode: fenced-frame" |
| "&Allow-Cross-Origin-Event-Reporting: ?1"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .event = {/*type=*/"click", /*reporting_destination=*/"buyer", |
| /*data=*/"data", /*cross_origin_exposed=*/true}, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginModeAB, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| FencedFrameReportEventFacilitatedTestingEnabledBrowserTest, |
| CustomURLNestedIframeCrossOriginNavigationWithOptIn) { |
| base::HistogramTester histogram_tester; |
| std::vector<Step> config = { |
| { |
| .is_embedder_initiated = true, |
| .is_opaque = true, |
| .use_custom_destination_url = true, |
| .destination = {"a.test", |
| "/set-header" |
| "?Supports-Loading-Mode: fenced-frame" |
| "&Allow-Cross-Origin-Event-Reporting: ?1"}, |
| .report_event_result = Step::Result::kSuccess, |
| }, |
| { |
| .is_target_nested_iframe = true, |
| .use_custom_destination_url = true, |
| .event = {/*type=*/"N/a", /*reporting_destination=*/"N/a", |
| /*data=*/"data", /*cross_origin_exposed=*/true}, |
| .destination = {"b.test", "/fenced_frames/title1.html"}, |
| .report_event_result = Step::Result::kCrossOriginModeAB, |
| }, |
| }; |
| RunTest(config); |
| } |
| |
| // Parameterized on whether the feature is enabled or not. |
| class UUIDFrameTreeBrowserTest |
| : public FencedFrameBrowserTestBase, |
| public ::testing::WithParamInterface<std::tuple<bool, bool>> { |
| public: |
| UUIDFrameTreeBrowserTest() { |
| scoped_feature_list_.InitWithFeatureStates( |
| {{blink::features::kAllowURNsInIframes, IsAllowURNsInIframesEnabled()}, |
| {blink::features::kDisplayWarningDeprecateURNIframesUseFencedFrames, |
| DisplayWarningDeprecateURNIframesUseFencedFrames()}}); |
| } |
| |
| bool NavigateIframeAndCheckURL(WebContents* web_contents, |
| const std::string& html_id, |
| const GURL& url, |
| const GURL& expected_commit_url) { |
| TestNavigationObserver nav_observer(web_contents); |
| if (!BeginNavigateIframeToURL(web_contents, html_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 base::StringPrintf( |
| "%s_%s", |
| std::get<0>(info.param) ? "AllowURNsInIframes" |
| : "DoNotAllowURNsInIframes", |
| std::get<1>(info.param) |
| ? "DisplayWarningDeprecateURNIframesUseFencedFrames" |
| : "DoNotDisplayWarningDeprecateURNIframesUseFencedFrames"); |
| } |
| |
| bool IsAllowURNsInIframesEnabled() { return std::get<0>(GetParam()); } |
| |
| bool DisplayWarningDeprecateURNIframesUseFencedFrames() { |
| return std::get<1>(GetParam()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(UUIDFrameTreeBrowserTest, |
| CheckIframeNavigationWithUUID) { |
| base::HistogramTester histogram_tester; |
| 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)); |
| histogram_tester.ExpectTotalCount( |
| "Navigation.BrowserMappedUrnUuidInIframeOrFencedFrame", 0); |
| |
| // 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)); |
| histogram_tester.ExpectTotalCount( |
| "Navigation.BrowserMappedUrnUuidInIframeOrFencedFrame", 0); |
| |
| GURL frame_url( |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html")); |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, frame_url); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| auto filter = |
| [](const content::WebContentsConsoleObserver::Message& message) { |
| return message.log_level == blink::mojom::ConsoleMessageLevel::kWarning; |
| }; |
| console_observer.SetFilter(base::BindRepeating(filter)); |
| console_observer.SetPattern( |
| "Protected Audience/selectURL will deprecate supporting iframes to " |
| "render the winning ad*"); |
| |
| if (IsAllowURNsInIframesEnabled()) { |
| // If the feature is enabled, we should navigate to the mapped page. |
| EXPECT_TRUE(NavigateIframeAndCheckURL(web_contents(), "test_iframe", |
| urn_uuid, frame_url)); |
| histogram_tester.ExpectBucketCount( |
| "Navigation.BrowserMappedUrnUuidInIframeOrFencedFrame", 1, 1); |
| // A console warning is emitted during navigation if feature |
| // `kDisplayWarningDeprecateURNIframesUseFencedFrames` is enabled. This will |
| // be removed once navigation support for urn::uuid in iframes is |
| // deprecated. |
| // TODO(crbug.com/40060657) |
| |
| if (DisplayWarningDeprecateURNIframesUseFencedFrames()) { |
| ASSERT_TRUE(console_observer.Wait()); |
| ASSERT_FALSE(console_observer.messages().empty()); |
| EXPECT_EQ( |
| console_observer.GetMessageAt(0), |
| "Protected Audience/selectURL will deprecate supporting iframes to " |
| "render the winning ad/selected URL. " |
| "Please use fenced frames instead. See " |
| "https://developer.chrome.com/en/docs/privacy-sandbox/fenced-frame/" |
| "#examples"); |
| } |
| } else { |
| // If the feature is disabled, navigation should fail. |
| EXPECT_FALSE(NavigateIframeAndCheckURL(web_contents(), "test_iframe", |
| urn_uuid, GURL())); |
| histogram_tester.ExpectBucketCount( |
| "Navigation.BrowserMappedUrnUuidInIframeOrFencedFrame", 1, 0); |
| // No console warning is emitted if the feature is disabled. |
| EXPECT_TRUE(console_observer.messages().empty()); |
| } |
| |
| // 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())); |
| |
| // 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(); |
| auto urn_uuid = test::AddAndVerifyFencedFrameURL(&url_mapping, frame_url); |
| |
| // Top page navigation to a URN should fail regardless of if the feature is |
| // enabled. |
| EXPECT_FALSE(NavigateToURL(shell(), urn_uuid)); |
| } |
| |
| class FencedFrameAutomaticBeaconBrowserTest |
| : public FencedFrameReportEventBrowserTest, |
| public testing::WithParamInterface<const char*> { |
| public: |
| FencedFrameAutomaticBeaconBrowserTest() = default; |
| |
| // An object representing the configuration of the test. First a frame is |
| // navigated to a page. Then, it does a top navigation. |
| struct Config { |
| struct Destination { |
| // The origin for the navigation. |
| std::string origin; |
| // The path for the resource to load. |
| std::string path; |
| }; |
| |
| struct BeaconType { |
| // The name of the event type as passed into |
| // setReportEventDataForAutomaticBeacons(). |
| std::string name; |
| // The mojo value that will be checked for histograms. |
| blink::mojom::AutomaticBeaconType type; |
| }; |
| |
| Destination starting_url; |
| Destination secondary_initiator_url; |
| Destination navigation_url; |
| |
| // Optional message to be sent as part of the payload. |
| // 1. If this is `std::nullopt`, `setReportEventDataForAutomaticBeacons()` |
| // is called without the `eventData` field. |
| // 2. Otherwise, the event data is the given string. |
| std::optional<std::string> message = "data"; |
| |
| // Whether there is a call to `setReportEventDataForAutomaticBeacons()`. |
| bool register_beacon_data = true; |
| |
| // Weather the destinations field is set when calling |
| // `setReportEventDataForAutomaticBeacons()`. |
| bool register_destinations = true; |
| |
| // Whether the initiating frame should have user activation when navigating. |
| bool initiator_has_user_activation = true; |
| |
| // Whether the top-level navigation should target "_blank" instead of |
| // "_unfencedTop"/"_top". |
| bool target_blank_navigation = false; |
| |
| // Whether we expect the beacon to send properly or not. |
| bool expected_success = true; |
| |
| // Whether we expect the beacon to send with data or not. |
| bool expected_data = true; |
| |
| // Whether we expect cookie data to be attached to the beacon. |
| // TODO(crbug.com/40286778): Remove this after 3PCD. |
| bool expected_cookie = true; |
| |
| // Whether a fenced frame should call window.fence.disableUntrustedNetwork() |
| // before doing an "_unfencedTop" navigation. Should only be true if |
| // `expected_success` is false, since disabling untrusted network will |
| // prevent beacons from sending. |
| bool disable_untrusted_network = false; |
| |
| BeaconType beacon_type = { |
| blink::kFencedFrameTopNavigationCommitBeaconType, |
| blink::mojom::AutomaticBeaconType::kTopNavigationCommit}; |
| }; |
| |
| static std::string DescribeParams( |
| const ::testing::TestParamInfo<ParamType>& info) { |
| return info.param; |
| } |
| |
| std::unique_ptr<net::test_server::BasicHttpResponse> |
| GetResponseWithAccessAllowHeaders( |
| const net::test_server::HttpRequest* request) { |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| |
| response->set_code(net::HTTP_OK); |
| response->AddCustomHeader(cors::kAccessControlAllowMethods, |
| request->method_string); |
| if (base::Contains(request->headers, "Origin")) { |
| response->AddCustomHeader(cors::kAccessControlAllowOrigin, "*"); |
| } |
| if (base::Contains(request->headers, cors::kAccessControlRequestHeaders)) { |
| response->AddCustomHeader( |
| cors::kAccessControlAllowHeaders, |
| request->headers.at(cors::kAccessControlRequestHeaders)); |
| } |
| |
| return response; |
| } |
| |
| scoped_refptr<FencedFrameReporter> CreateFencedFrameReporter() { |
| return FencedFrameReporter::CreateForFledge( |
| web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(), |
| web_contents()->GetBrowserContext(), |
| /*direct_seller_is_seller=*/false, |
| static_cast<StoragePartitionImpl*>( |
| web_contents()->GetPrimaryMainFrame()->GetStoragePartition()) |
| ->GetPrivateAggregationManager(), |
| /*main_frame_origin=*/ |
| web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(), |
| /*winner_origin=*/url::Origin::Create(GURL("https://a.test")), |
| /*winner_aggregation_coordinator_origin=*/std::nullopt); |
| } |
| |
| // A helper function for specifying automatic beacon tests. |
| void RunTest(Config& config) { |
| // Disabling untrusted network only applies to fenced frames, so skip these |
| // tests for iframes. This is sort of against the spirit of parameterized |
| // tests, but it's the most practical way to deal with the clash in behavior |
| // between the two frame types. |
| if (GetParam() != std::string("fencedframe") && |
| config.disable_untrusted_network) { |
| GTEST_SKIP(); |
| } |
| |
| // In order to check events reported over the network, we register an HTTP |
| // response interceptor for each successful reportEvent request we expect. |
| net::test_server::ControllableHttpResponse response(https_server(), |
| kReportingURL); |
| |
| std::string reporting_origin = "c.test"; |
| // An additional response is used to check any spurious waiting reported |
| // events. |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Set up the embedder and a fenced frame. |
| GURL main_url = https_server()->GetURL("a.test", "/hello.html"); |
| GURL starting_url = https_server()->GetURL(config.starting_url.origin, |
| config.starting_url.path); |
| GURL navigation_url = https_server()->GetURL(config.navigation_url.origin, |
| config.navigation_url.path); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| // Create a FencedFrameReporter and pass it reporting metadata. |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter = |
| CreateFencedFrameReporter(); |
| GURL reporting_url(https_server()->GetURL(reporting_origin, kReportingURL)); |
| // Set valid reporting metadata for buyer. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| url::Origin::Create(GURL()), |
| { |
| {config.beacon_type.name, reporting_url}, |
| }); |
| // Set empty reporting url for seller. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kSeller, |
| url::Origin::Create(GURL()), {{"click", GURL()}}); |
| // Set no reporting urls for component seller. |
| fenced_frame_reporter->OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination::kComponentSeller, |
| url::Origin::Create(GURL()), {}); |
| |
| // Get the urn mapping object. |
| FencedFrameURLMapping& url_mapping = |
| root->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| |
| GURL starting_urn = test::AddAndVerifyFencedFrameURL( |
| &url_mapping, starting_url, fenced_frame_reporter); |
| |
| // ExecJs() by default gives its execution target transient user activation. |
| // If the test requires a frame to not have user activation, that must be |
| // specified in the function call's `options` parameter for every ExecJs() |
| // call made on the frame. |
| EvalJsOptions ad_frame_execjs_options = |
| config.initiator_has_user_activation ? EXECUTE_SCRIPT_DEFAULT_OPTIONS |
| : EXECUTE_SCRIPT_NO_USER_GESTURE; |
| |
| EXPECT_TRUE(ExecJs(root, |
| JsReplace("var ad_frame = document.createElement($1);" |
| "document.body.appendChild(ad_frame);", |
| GetParam()), |
| ad_frame_execjs_options)); |
| |
| EXPECT_EQ(1U, root->child_count()); |
| FrameTreeNode* ad_frame_root_node; |
| |
| if (GetParam() == std::string("fencedframe")) { |
| ad_frame_root_node = GetFencedFrameRootNode(root->child_at(0)); |
| EXPECT_TRUE(ad_frame_root_node->IsFencedFrameRoot()); |
| EXPECT_TRUE(ad_frame_root_node->IsInFencedFrameTree()); |
| } else { |
| ad_frame_root_node = root->child_at(0); |
| } |
| |
| TestFrameNavigationObserver ad_frame_observer( |
| ad_frame_root_node->current_frame_host()); |
| |
| if (GetParam() == std::string("fencedframe")) { |
| EXPECT_TRUE( |
| ExecJs(root, |
| JsReplace("ad_frame.config = new FencedFrameConfig($1);", |
| starting_urn), |
| ad_frame_execjs_options)); |
| } else { |
| EXPECT_TRUE(ExecJs(root, JsReplace("ad_frame.src = $1;", starting_urn), |
| ad_frame_execjs_options)); |
| } |
| ad_frame_observer.WaitForCommit(); |
| |
| base::Value::List destination_list; |
| if (config.register_destinations) { |
| destination_list.Append("buyer"); |
| destination_list.Append("seller"); |
| } |
| |
| if (config.register_beacon_data) { |
| if (!config.message) { |
| // Call `setReportEventDataForAutomaticBeacons()` without `eventData` |
| // field. |
| EXPECT_TRUE( |
| ExecJs(ad_frame_root_node, |
| JsReplace(R"( |
| window.fence.setReportEventDataForAutomaticBeacons({ |
| eventType: $1, |
| destination: $2 |
| }); |
| )", |
| config.beacon_type.name, destination_list.Clone()), |
| ad_frame_execjs_options)); |
| |
| histogram_tester_.ExpectUniqueSample( |
| blink::kAutomaticBeaconEventTypeHistogram, config.beacon_type.type, |
| 1); |
| } else { |
| // Call `setReportEventDataForAutomaticBeacons()` with `eventData`. |
| EvalJsResult result = |
| EvalJs(ad_frame_root_node, |
| JsReplace(R"( |
| window.fence.setReportEventDataForAutomaticBeacons({ |
| eventType: $1, |
| eventData: $2, |
| destination: $3 |
| }); |
| )", |
| config.beacon_type.name, config.message.value(), |
| destination_list.Clone()), |
| ad_frame_execjs_options); |
| |
| if (config.message->length() > blink::kFencedFrameMaxBeaconLength) { |
| // When eventData exceeds the length limit, a security error is thrown |
| // instead of a console error. |
| EXPECT_FALSE(result.error.empty()); |
| EXPECT_THAT( |
| result.error, |
| testing::HasSubstr("The data provided to " |
| "setReportEventDataForAutomaticBeacons() " |
| "exceeds the maximum length, which is 64KB.")); |
| |
| histogram_tester_.ExpectUniqueSample( |
| blink::kAutomaticBeaconEventTypeHistogram, |
| config.beacon_type.type, 0); |
| } else { |
| EXPECT_TRUE(result.error.empty()); |
| histogram_tester_.ExpectUniqueSample( |
| blink::kAutomaticBeaconEventTypeHistogram, |
| config.beacon_type.type, 1); |
| } |
| } |
| } |
| |
| // If a secondary initiator URL is specified, navigate the ad frame to the |
| // second URL before performing a top-level navigation. This checks that |
| // automatic beacons are not sent if the current URL of a frame is |
| // cross-origin to the mapped URL in the fenced frame config. |
| GURL secondary_initiator_url = |
| config.secondary_initiator_url.origin.empty() |
| ? GURL() |
| : https_server()->GetURL(config.secondary_initiator_url.origin, |
| config.secondary_initiator_url.path); |
| if (secondary_initiator_url.is_valid()) { |
| EXPECT_TRUE(ExecJs(ad_frame_root_node, R"( |
| var x_origin_frame = document.createElement('iframe'); |
| document.body.appendChild(x_origin_frame); |
| )")); |
| FrameTreeNode* x_origin_frame_node = ad_frame_root_node->child_at(0); |
| TestFrameNavigationObserver x_origin_frame_navigation_observer( |
| x_origin_frame_node->current_frame_host()); |
| EXPECT_TRUE(ExecJs( |
| ad_frame_root_node, |
| JsReplace("x_origin_frame.src = $1;", secondary_initiator_url))); |
| x_origin_frame_navigation_observer.WaitForCommit(); |
| // We will be navigating the cross-origin iframe, so we set |
| // ad_frame_root_node so that the navigation script uses that frame |
| // instead of the root ad frame. |
| ad_frame_root_node = x_origin_frame_node; |
| } |
| |
| std::string target; |
| if (config.target_blank_navigation) { |
| target = "_blank"; |
| } else if (GetParam() == std::string("fencedframe")) { |
| target = "_unfencedTop"; |
| } else { |
| target = "_top"; |
| } |
| |
| // Set up the document.cookie for credentialed automatic beacons. |
| // TODO(crbug.com/40286778): Remove this block after 3PCD. |
| GURL reporting_cookie_url = |
| https_server()->GetURL(reporting_origin, "/hello.html"); |
| if (config.expected_success) { |
| EXPECT_TRUE(ExecJs(root, |
| "var cookie_frame = document.createElement('iframe');" |
| "document.body.appendChild(cookie_frame);")); |
| EXPECT_EQ(2U, root->child_count()); |
| FrameTreeNode* cookie_frame_root_node = root->child_at(1); |
| TestFrameNavigationObserver cookie_frame_observer( |
| cookie_frame_root_node->current_frame_host()); |
| EXPECT_TRUE(ExecJs( |
| root, JsReplace("cookie_frame.src = $1;", reporting_cookie_url))); |
| cookie_frame_observer.WaitForCommit(); |
| EXPECT_TRUE( |
| ExecJs(cookie_frame_root_node, |
| "document.cookie = 'name=foobarbaz; SameSite=None; Secure';")); |
| } |
| |
| if (GetParam() == std::string("fencedframe") && |
| config.disable_untrusted_network) { |
| EXPECT_TRUE(ExecJs(ad_frame_root_node, R"( |
| window.fence.disableUntrustedNetwork(); |
| )")); |
| } |
| |
| EXPECT_TRUE( |
| ExecJs(ad_frame_root_node, |
| JsReplace("window.open($1, $2);", navigation_url, target), |
| ad_frame_execjs_options)); |
| |
| if (!config.expected_success) { |
| // Send a request with different content using `SendBasicRequest`, which |
| // uses the same infrastructure as the automatic beacons used in |
| // FencedFrameReporter. |
| // ControllableHttpResponse handles only one request. Verifying that it |
| // received the request from `SendBasicRequest`, which was sent after the |
| // possible automatic beacon, implies the automatic beacon was not sent as |
| // a result of the top navigation, as expected. |
| EXPECT_TRUE(content::WaitForLoadStop(shell()->web_contents())); |
| fenced_frame_test_helper().SendBasicRequest( |
| web_contents(), https_server()->GetURL("c.test", kReportingURL), |
| "response"); |
| response.WaitForRequest(); |
| EXPECT_TRUE(response.has_received_request()); |
| EXPECT_EQ(response.http_request()->content, "response"); |
| // Fenced frames do not allow top-level navigation without user activation |
| // due to the permissions policy always being disabled. We only test the |
| // histogram for iframes. |
| if (!config.initiator_has_user_activation && |
| GetParam() == std::string("iframe")) { |
| histogram_tester_.ExpectUniqueSample( |
| blink::kAutomaticBeaconOutcomeHistogram, |
| blink::AutomaticBeaconOutcome::kNoUserActivation, 1); |
| } |
| if (secondary_initiator_url.is_valid()) { |
| histogram_tester_.ExpectUniqueSample( |
| blink::kAutomaticBeaconOutcomeHistogram, |
| blink::AutomaticBeaconOutcome::kNotSameOriginNotOptedIn, 1); |
| } |
| return; |
| } |
| |
| response.WaitForRequest(); |
| // Verify the request has the correct content. |
| if (!config.message || !config.expected_data) { |
| EXPECT_TRUE(response.http_request()->content.empty()); |
| } else { |
| EXPECT_EQ(response.http_request()->content, config.message); |
| } |
| // Verify the request contains the eligibility header. |
| ExpectValidAttributionReportingEligibleHeaderForNavigation( |
| response.http_request()->headers.at("Attribution-Reporting-Eligible")); |
| ExpectValidAttributionReportingSupportHeader( |
| response.http_request()->headers.at("Attribution-Reporting-Support"), |
| /*web_expected=*/true, |
| /*os_expected=*/false); |
| |
| // Verify the request has credentials attached. |
| // TODO(crbug.com/40286778): Remove this block after 3PCD. |
| if (config.expected_cookie) { |
| EXPECT_EQ("name=foobarbaz", |
| response.http_request()->headers.at("Cookie")); |
| // Send a response that sets new cookies. |
| auto response_packet = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response_packet->set_code(net::HTTP_OK); |
| response_packet->AddCustomHeader("Set-Cookie", |
| "name=qux; SameSite=None; Secure"); |
| response.Send(response_packet->ToResponseString()); |
| |
| // Verify that the cookies got set correctly. |
| EXPECT_TRUE(NavigateToURL(shell(), reporting_cookie_url)); |
| root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| EXPECT_EQ("name=qux", EvalJs(root, "document.cookie")); |
| } else { |
| EXPECT_EQ(0U, response.http_request()->headers.count("Cookie")); |
| } |
| |
| response.Done(); |
| |
| histogram_tester_.ExpectUniqueSample( |
| blink::kAutomaticBeaconOutcomeHistogram, |
| blink::AutomaticBeaconOutcome::kSuccess, 1); |
| histogram_tester_.ExpectBucketCount( |
| blink::kFencedFrameTopNavigationHistogram, |
| blink::FencedFrameNavigationState::kBegin, 1); |
| histogram_tester_.ExpectBucketCount( |
| blink::kFencedFrameTopNavigationHistogram, |
| blink::FencedFrameNavigationState::kCommit, 1); |
| } |
| |
| private: |
| // Server must start after ControllableHttpResponse object being constructed. |
| void AssertServerStart() override {} |
| |
| base::HistogramTester histogram_tester_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, SameOriginBasic) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"a.test", "/fenced_frames/title1.html"}, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| CrossOriginBasic) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, BFCacheDisabled) { |
| DisableBackForwardCacheForTesting(shell()->web_contents(), |
| BackForwardCache::TEST_REQUIRES_NO_CACHING); |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, EmptyMessage) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .message = "", |
| .expected_success = true, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| MessageExceedsLengthLimit) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .message = std::string(blink::kFencedFrameMaxBeaconLength + 1, '*'), |
| .expected_success = false, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| UntrustedNetworkDisabled) { |
| Config config = {.starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"a.test", "/fenced_frames/title1.html"}, |
| .expected_success = false, |
| .disable_untrusted_network = true}; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| HasEventDataField) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .message = "Has event data.", |
| .expected_success = true, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| NoEventDataField) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .message = std::nullopt, |
| .expected_success = true, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| NoBeaconDataRegistered) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .register_beacon_data = false, |
| .expected_success = false, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| NoUserActivation) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .initiator_has_user_activation = false, |
| .expected_success = false, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| TargetBlankNavigation) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .target_blank_navigation = true, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| CrossOriginToMappedURL) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .secondary_initiator_url = {"c.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .expected_success = false, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| NoDestinationsRegistered) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"b.test", "/fenced_frames/title1.html"}, |
| .register_destinations = false, |
| .expected_data = false, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| AutomaticBeaconCredentialsDisallowed) { |
| SetAllowAutomaticBeaconCredentials(false); |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"a.test", "/fenced_frames/title1.html"}, |
| .expected_cookie = false, |
| }; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| DeprecatedTopNavigation) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"a.test", "/fenced_frames/title1.html"}, |
| .beacon_type = { |
| blink::kDeprecatedFencedFrameTopNavigationBeaconType, |
| blink::mojom::AutomaticBeaconType::kDeprecatedTopNavigation}}; |
| RunTest(config); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FencedFrameAutomaticBeaconBrowserTest, |
| TopNavigationStart) { |
| Config config = { |
| .starting_url = {"a.test", "/fenced_frames/title1.html"}, |
| .navigation_url = {"a.test", "/fenced_frames/title1.html"}, |
| .beacon_type = {blink::kFencedFrameTopNavigationStartBeaconType, |
| blink::mojom::AutomaticBeaconType::kTopNavigationStart}}; |
| RunTest(config); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| FencedFrameAutomaticBeaconBrowserTest, |
| ::testing::Values("fencedframe", "iframe"), |
| &FencedFrameAutomaticBeaconBrowserTest::DescribeParams); |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| UUIDFrameTreeBrowserTest, |
| ::testing::Combine(testing::Bool(), testing::Bool()), |
| &UUIDFrameTreeBrowserTest::DescribeParams); |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| FencedFrameIsolatedSandboxedIframesBrowserTest, |
| ::testing::Bool(), |
| [](const testing::TestParamInfo<bool>& info) { |
| return info.param ? "kIsolateFencedFramesEnabled" |
| : "kIsolateFencedFramesDisabled"; |
| }); |
| |
| class FencedFramePreconnectBrowserTest : public FencedFrameMPArchBrowserTest { |
| public: |
| net::test_server::ConnectionTracker* connection_tracker() { |
| return connection_tracker_.get(); |
| } |
| |
| private: |
| void AdditionalSetup() override { |
| connection_tracker_ = |
| std::make_unique<net::test_server::ConnectionTracker>(https_server()); |
| } |
| |
| std::unique_ptr<net::test_server::ConnectionTracker> connection_tracker_; |
| }; |
| |
| // Verify preconnect is working in fenced frame. |
| IN_PROC_BROWSER_TEST_F(FencedFramePreconnectBrowserTest, Preconnect) { |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| RenderFrameHost* fenced_frame_rfh = |
| fenced_frame_test_helper().CreateFencedFrame( |
| shell()->web_contents()->GetPrimaryMainFrame(), fenced_frame_url); |
| |
| // Reset connection counts after fenced frame has been set up. |
| connection_tracker()->ResetCounts(); |
| |
| // Navigate the fenced frame to a page with a link element that makes |
| // preconnect request. |
| TestFrameNavigationObserver observer(fenced_frame_rfh); |
| EXPECT_TRUE(ExecJs( |
| shell()->web_contents()->GetPrimaryMainFrame(), |
| JsReplace( |
| R"(document.querySelector('fencedframe').config |
| = new FencedFrameConfig($1);)", |
| https_server()->GetURL("a.test", "/link_rel_preconnect.html")))); |
| |
| observer.WaitForCommit(); |
| ASSERT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| // There should be a total of 2 connections. 1 from navigation and 1 from |
| // preconnect. |
| connection_tracker()->WaitForAcceptedConnections(2u); |
| EXPECT_EQ(connection_tracker()->GetAcceptedSocketCount(), 2u); |
| } |
| |
| // Verify preconnect is disabled after fenced frame untrusted network cutoff. |
| IN_PROC_BROWSER_TEST_F(FencedFramePreconnectBrowserTest, |
| NetworkCutoffDisablesPreconnect) { |
| ASSERT_TRUE(https_server()->Start()); |
| |
| const GURL main_url = https_server()->GetURL("a.test", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| const GURL fenced_frame_url = |
| https_server()->GetURL("a.test", "/fenced_frames/title1.html"); |
| RenderFrameHost* fenced_frame_rfh = |
| fenced_frame_test_helper().CreateFencedFrame( |
| shell()->web_contents()->GetPrimaryMainFrame(), fenced_frame_url); |
| |
| // Reset connection counts after fenced frame has been set up. |
| connection_tracker()->ResetCounts(); |
| |
| // Navigate the fenced frame. The loaded page disables untrusted network |
| // access, then adds a link element that makes preconnect request. |
| TestFrameNavigationObserver observer(fenced_frame_rfh); |
| EXPECT_TRUE( |
| ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| JsReplace( |
| R"(document.querySelector('fencedframe').config |
| = new FencedFrameConfig($1);)", |
| https_server()->GetURL( |
| "a.test", "/link_rel_preconnect_disable_network.html")))); |
| |
| observer.WaitForCommit(); |
| ASSERT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| // There should be only 1 connection from navigation. The preconnect request |
| // is cancelled because the untrusted network access is disabled. |
| connection_tracker()->WaitForAcceptedConnections(1u); |
| EXPECT_EQ(connection_tracker()->GetAcceptedSocketCount(), 1u); |
| } |
| |
| // Verify preconnect triggered by link response header is working in fenced |
| // frame. |
| IN_PROC_BROWSER_TEST_F(FencedFramePreconnectBrowserTest, |
| PreconnectFromLinkHeader) { |
| std::string relative_url = "/title1.html"; |
| net::test_server::ControllableHttpResponse response(https_server(), |
| relative_url); |
| |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Navigate to a page that contains a fenced frame. |
| const GURL main_url = https_server()->GetURL( |
| "a.test", "/cross_site_iframe_factory.html?a.test(a.test{fenced})"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Get fenced frame render frame host. |
| std::vector<RenderFrameHost*> child_frames = |
| fenced_frame_test_helper().GetChildFencedFrameHosts( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| ASSERT_EQ(child_frames.size(), 1u); |
| RenderFrameHost* fenced_frame_rfh = child_frames[0]; |
| |
| GURL navigation_url = https_server()->GetURL("a.test", relative_url); |
| |
| // Reset connection counts after fenced frame has been set up. |
| connection_tracker()->ResetCounts(); |
| |
| // Navigate the fenced frame. |
| TestFrameNavigationObserver observer(fenced_frame_rfh); |
| |
| EXPECT_TRUE( |
| ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| JsReplace( |
| R"(document.getElementsByTagName('fencedframe')[0].config = |
| new FencedFrameConfig($1);)", |
| navigation_url))); |
| |
| GURL preconnect_url = https_server()->GetURL("b.test", "/title2.html"); |
| |
| // Send a response header with link preconnect field. |
| response.WaitForRequest(); |
| response.Send( |
| base::StringPrintf("HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Supports-Loading-Mode: fenced-frame\r\n" |
| "Link: <%s>; rel=preconnect\r\n" |
| "\r\n", |
| preconnect_url.spec().c_str())); |
| response.Done(); |
| |
| // Wait until navigation commits. |
| observer.WaitForCommit(); |
| ASSERT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| // There should be a total of 2 connections. 1 from navigation and 1 from |
| // preconnect. |
| connection_tracker()->WaitForAcceptedConnections(2u); |
| EXPECT_EQ(connection_tracker()->GetAcceptedSocketCount(), 2u); |
| } |
| |
| // Verify preconnect triggered by link response header is disabled after fenced |
| // frame untrusted network cutoff. |
| IN_PROC_BROWSER_TEST_F(FencedFramePreconnectBrowserTest, |
| NetworkCutoffDisablesPreconnectFromLinkHeader) { |
| std::string relative_url = "/title1.html"; |
| net::test_server::ControllableHttpResponse response(https_server(), |
| relative_url); |
| |
| ASSERT_TRUE(https_server()->Start()); |
| |
| // Navigate to a page that contains a fenced frame. |
| const GURL main_url = https_server()->GetURL( |
| "a.test", |
| "/cross_site_iframe_factory.html?a.test(a.test{fenced}(a.test))"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Get fenced frame render frame host. |
| std::vector<RenderFrameHost*> child_frames = |
| fenced_frame_test_helper().GetChildFencedFrameHosts( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| ASSERT_EQ(child_frames.size(), 1u); |
| RenderFrameHost* fenced_frame_rfh = child_frames[0]; |
| |
| // Get nested iframe render frame host. |
| RenderFrameHost* nested_iframe_rfh = ChildFrameAt(fenced_frame_rfh, 0); |
| |
| // Reset connection counts after fenced frame has been set up. |
| connection_tracker()->ResetCounts(); |
| |
| // Disable fenced frame untrusted network access. |
| EXPECT_TRUE(ExecJs(fenced_frame_rfh, R"( |
| (async () => { |
| await window.fence.disableUntrustedNetwork(); |
| })(); |
| )")); |
| |
| GURL navigation_url = https_server()->GetURL("a.test", relative_url); |
| |
| // Exempt `navigation_url` from fenced frame network revocation. |
| test::ExemptUrlsFromFencedFrameNetworkRevocation(fenced_frame_rfh, |
| {navigation_url}); |
| |
| // Navigate the nested iframe. The navigation is allowed because the url has |
| // been exempted from network revocation. |
| TestFrameNavigationObserver observer(nested_iframe_rfh); |
| |
| EXPECT_TRUE( |
| ExecJs(fenced_frame_rfh, |
| JsReplace("document.getElementsByTagName('iframe')[0].src = $1;", |
| navigation_url))); |
| |
| GURL preconnect_url = https_server()->GetURL("b.test", "/title2.html"); |
| |
| // Send a response header with link preconnect field. |
| response.WaitForRequest(); |
| response.Send( |
| base::StringPrintf("HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Supports-Loading-Mode: fenced-frame\r\n" |
| "Link: <%s>; rel=preconnect\r\n" |
| "\r\n", |
| preconnect_url.spec().c_str())); |
| response.Done(); |
| |
| // Wait until navigation commits. |
| observer.WaitForCommit(); |
| ASSERT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| // There should be only 1 connection from navigation. The preconnect request |
| // is cancelled because the untrusted network access is disabled. |
| connection_tracker()->WaitForAcceptedConnections(1u); |
| EXPECT_EQ(connection_tracker()->GetAcceptedSocketCount(), 1u); |
| } |
| |
| } // namespace content |