| // Copyright 2020 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/renderer_host/render_frame_host_impl.h" |
| |
| #include <memory> |
| |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/buildflag.h" |
| #include "components/input/timeout_monitor.h" |
| #include "content/browser/renderer_host/navigation_controller_impl.h" |
| #include "content/browser/renderer_host/render_view_host_impl.h" |
| #include "content/common/content_navigation_policy.h" |
| #include "content/common/features.h" |
| #include "content/public/browser/cors_origin_pattern_setter.h" |
| #include "content/public/browser/shared_cors_origin_access_list.h" |
| #include "content/public/browser/web_contents_delegate.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/fake_local_frame.h" |
| #include "content/public/test/navigation_simulator.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/test/navigation_simulator_impl.h" |
| #include "content/test/test_page_broadcast.h" |
| #include "content/test/test_render_frame_host.h" |
| #include "content/test/test_render_view_host.h" |
| #include "content/test/test_web_contents.h" |
| #include "net/base/features.h" |
| #include "net/base/isolation_info.h" |
| #include "net/base/network_isolation_partition.h" |
| #include "net/cookies/site_for_cookies.h" |
| #include "services/network/public/cpp/cors/origin_access_list.h" |
| #include "services/network/public/mojom/cors.mojom.h" |
| #include "services/network/public/mojom/cors_origin_pattern.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/runtime_feature_state/runtime_feature_state_context.h" |
| #include "third_party/blink/public/common/runtime_feature_state/runtime_feature_state_read_context.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/mojom/favicon/favicon_url.mojom.h" |
| #include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| #include "url/url_util.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| void AddHostPermissions(const std::string& host, RenderFrameHost* rfh) { |
| std::vector<network::mojom::CorsOriginPatternPtr> patterns; |
| base::RunLoop run_loop; |
| patterns.push_back(network::mojom::CorsOriginPattern::New( |
| "https", host, 0, network::mojom::CorsDomainMatchMode::kAllowSubdomains, |
| network::mojom::CorsPortMatchMode::kAllowAnyPort, |
| network::mojom::CorsOriginAccessMatchPriority::kDefaultPriority)); |
| CorsOriginPatternSetter::Set(rfh->GetBrowserContext(), |
| rfh->GetLastCommittedOrigin(), |
| std::move(patterns), {}, run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| std::optional<uint64_t> NavigateAndGetCanvasNoiseToken( |
| const GURL& url, |
| RenderFrameHost* frame_host) { |
| return GetCanvasNoiseTokenForPage( |
| NavigationSimulator::NavigateAndCommitFromDocument(url, frame_host) |
| ->GetPage()); |
| } |
| } // namespace |
| |
| class RenderFrameHostImplTest : public RenderViewHostImplTestHarness { |
| public: |
| void SetUp() override { |
| RenderViewHostImplTestHarness::SetUp(); |
| contents()->GetPrimaryMainFrame()->InitializeRenderFrameIfNeeded(); |
| } |
| }; |
| |
| // TODO(crbug.com/40260854): This set-up is temporary. Eventually, all |
| // tests that reference extensions will be moved to chrome/browser/ and this |
| // class can be deleted. |
| class FirstPartyOverrideContentBrowserClient : public ContentBrowserClient { |
| public: |
| FirstPartyOverrideContentBrowserClient() = default; |
| ~FirstPartyOverrideContentBrowserClient() override = default; |
| |
| private: |
| bool ShouldUseFirstPartyStorageKey(const url::Origin& origin) override { |
| return origin.scheme() == "chrome-extension"; |
| } |
| }; |
| |
| // A test class that forces kOriginKeyedProcessesByDefault off for tests that |
| // require that same-site cross-origin navigations don't trigger a RFH swap. |
| class RenderFrameHostImplTest_NoOriginKeyedProcessesByDefault |
| : public RenderFrameHostImplTest { |
| public: |
| RenderFrameHostImplTest_NoOriginKeyedProcessesByDefault() { |
| feature_list_.InitAndDisableFeature( |
| features::kOriginKeyedProcessesByDefault); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Note: Since this test is predicate on not having a RFH swap for a |
| // cross-origin, same-site navigation, it only makes sense to run it with |
| // kOriginKeyedProcessesByDefault disabled. |
| TEST_F(RenderFrameHostImplTest_NoOriginKeyedProcessesByDefault, |
| ExpectedMainWorldOrigin) { |
| GURL initial_url = GURL("https://initial.example.test/"); |
| GURL final_url = GURL("https://final.example.test/"); |
| |
| auto get_expected_main_world_origin = [](RenderFrameHost* rfh) { |
| NavigationRequest* in_flight_request = |
| static_cast<RenderFrameHostImpl*>(rfh) |
| ->FindLatestNavigationRequestThatIsStillCommitting(); |
| |
| return in_flight_request ? in_flight_request->GetOriginToCommit() |
| : rfh->GetLastCommittedOrigin(); |
| }; |
| |
| // Start the test with a simple navigation. |
| { |
| std::unique_ptr<NavigationSimulator> simulator = |
| NavigationSimulator::CreateRendererInitiated(initial_url, main_rfh()); |
| simulator->Start(); |
| simulator->Commit(); |
| } |
| RenderFrameHostImpl* initial_rfh = main_test_rfh(); |
| // This test is for a bug that only happens when there is no RFH swap on |
| // same-site navigations, so we should disable same-site proactive |
| // BrowsingInstance for |initial_rfh| before continiung. |
| DisableProactiveBrowsingInstanceSwapFor(initial_rfh); |
| if (ShouldCreateNewHostForAllFrames()) { |
| GTEST_SKIP(); |
| } |
| // Verify expected main world origin in a steady state - after a commit it |
| // should be the same as the last committed origin. |
| EXPECT_EQ(url::Origin::Create(initial_url), |
| get_expected_main_world_origin(main_rfh())); |
| EXPECT_EQ(url::Origin::Create(initial_url), |
| main_rfh()->GetLastCommittedOrigin()); |
| EXPECT_EQ( |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(initial_url)), |
| main_test_rfh()->GetStorageKey()); |
| |
| // Verify expected main world origin when a pending navigation was started but |
| // hasn't yet reached the ready-to-commit state. |
| std::unique_ptr<NavigationSimulator> simulator2 = |
| NavigationSimulator::CreateRendererInitiated(final_url, main_rfh()); |
| simulator2->Start(); |
| EXPECT_EQ(url::Origin::Create(initial_url), |
| get_expected_main_world_origin(main_rfh())); |
| |
| // Verify expected main world origin when a pending navigation has reached the |
| // ready-to-commit state. Note that the last committed origin shouldn't |
| // change yet at this point. |
| simulator2->ReadyToCommit(); |
| simulator2->Wait(); |
| EXPECT_EQ(url::Origin::Create(final_url), |
| get_expected_main_world_origin(main_rfh())); |
| EXPECT_EQ(url::Origin::Create(initial_url), |
| main_rfh()->GetLastCommittedOrigin()); |
| EXPECT_EQ( |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(initial_url)), |
| main_test_rfh()->GetStorageKey()); |
| |
| // Verify expected main world origin once we are again in a steady state - |
| // after a commit. |
| simulator2->Commit(); |
| EXPECT_EQ(url::Origin::Create(final_url), |
| get_expected_main_world_origin(main_rfh())); |
| EXPECT_EQ(url::Origin::Create(final_url), |
| main_rfh()->GetLastCommittedOrigin()); |
| EXPECT_EQ(blink::StorageKey::CreateFirstParty(url::Origin::Create(final_url)), |
| main_test_rfh()->GetStorageKey()); |
| |
| // As a test correctness check, verify that there was no RFH swap (the bug |
| // this test protects against would only happen if there is no swap). In |
| // fact, FindLatestNavigationRequestThatIsStillCommitting might possibly be |
| // removed entirely once we swap on all document changes. |
| EXPECT_EQ(initial_rfh, main_rfh()); |
| } |
| |
| // Test that navigating to an invalid URL (which creates an empty GURL) causes |
| // about:blank to commit. |
| TEST_F(RenderFrameHostImplTest, InvalidURL) { |
| // Start from a valid commit. |
| NavigateAndCommit(GURL("https://test.example.com")); |
| |
| // Attempt to navigate to a non-empty invalid URL, which GURL treats as an |
| // empty invalid URL. Blink treats navigations to an empty URL as navigations |
| // to about:blank. |
| GURL invalid_url("invalidurl"); |
| EXPECT_TRUE(invalid_url.is_empty()); |
| EXPECT_FALSE(invalid_url.is_valid()); |
| NavigateAndCommit(invalid_url); |
| EXPECT_EQ(GURL(url::kAboutBlankURL), main_rfh()->GetLastCommittedURL()); |
| } |
| |
| // Ensures that IsolationInfo's SiteForCookies is empty and |
| // that it correctly generates a StorageKey with a kCrossSite |
| // AncestorChainBit when frames are nested in an A->B->A |
| // configuration. |
| TEST_F(RenderFrameHostImplTest, CrossSiteAncestorInFrameTree) { |
| // Enable 3p partitioning to accurately test AncestorChainBit. |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| // Load site A into the main frame. |
| GURL parent_url = GURL("https://parent.example.test/"); |
| NavigationSimulator::CreateRendererInitiated(parent_url, main_rfh()) |
| ->Commit(); |
| |
| // Create a child RenderFrameHost and navigate it to site B to establish A->B. |
| auto* child_rfh_1 = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child:a->b")); |
| GURL child_url_1 = GURL("https://child.example.com"); |
| child_rfh_1 = static_cast<TestRenderFrameHost*>( |
| NavigationSimulator::NavigateAndCommitFromDocument(child_url_1, |
| child_rfh_1)); |
| |
| // Create a child RenderFrameHost in the existing child RenderFrameHost and |
| // navigate it to site A to establish A->B->A. |
| auto* child_rfh_2 = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(child_rfh_1) |
| ->AppendChild("child:a->b->a")); |
| child_rfh_2 = static_cast<TestRenderFrameHost*>( |
| NavigationSimulator::NavigateAndCommitFromDocument(parent_url, |
| child_rfh_2)); |
| |
| // Constructing expected values. |
| url::Origin expected_final_origin = url::Origin::Create(parent_url); |
| blink::StorageKey expected_final_storage_key = blink::StorageKey::Create( |
| expected_final_origin, net::SchemefulSite(expected_final_origin), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| net::IsolationInfo expected_final_isolation_info = net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, expected_final_origin, |
| expected_final_origin, net::SiteForCookies(), /*nonce=*/std::nullopt, |
| net::NetworkIsolationPartition::kGeneral, |
| net::IsolationInfo::FrameAncestorRelation::kCrossSite); |
| |
| EXPECT_EQ(expected_final_origin, child_rfh_2->GetLastCommittedOrigin()); |
| EXPECT_EQ(expected_final_storage_key, child_rfh_2->GetStorageKey()); |
| EXPECT_TRUE(expected_final_isolation_info.IsEqualForTesting( |
| child_rfh_2->GetIsolationInfoForSubresources())); |
| EXPECT_EQ(expected_final_isolation_info.network_isolation_key(), |
| child_rfh_2->GetNetworkIsolationKey()); |
| EXPECT_TRUE(expected_final_isolation_info.site_for_cookies().IsEquivalent( |
| child_rfh_2->ComputeSiteForCookies())); |
| EXPECT_TRUE(expected_final_isolation_info.IsEqualForTesting( |
| child_rfh_2->GetPendingIsolationInfoForSubresources())); |
| } |
| |
| // Test the IsolationInfo and related fields of a request during the various |
| // phases of a commit, when a RenderFrameHost is reused. Once RenderDocument |
| // ships, this test may no longer be needed. |
| // Note: Since this test is predicate on not having a RFH swap for a |
| // cross-origin, same-site navigation, it only makes sense to run it with |
| // kOriginKeyedProcessesByDefault disabled. |
| TEST_F(RenderFrameHostImplTest_NoOriginKeyedProcessesByDefault, |
| IsolationInfoDuringCommit) { |
| GURL initial_url = GURL("https://initial.example.test/"); |
| url::Origin expected_initial_origin = url::Origin::Create(initial_url); |
| const blink::StorageKey expected_initial_storage_key = |
| blink::StorageKey::CreateFirstParty(expected_initial_origin); |
| net::IsolationInfo expected_initial_isolation_info = |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, expected_initial_origin, |
| expected_initial_origin, |
| net::SiteForCookies::FromOrigin(expected_initial_origin), |
| /*nonce=*/std::nullopt, net::NetworkIsolationPartition::kGeneral, |
| net::IsolationInfo::FrameAncestorRelation::kSameOrigin); |
| |
| GURL final_url = GURL("https://final.example.test/"); |
| url::Origin expected_final_origin = url::Origin::Create(final_url); |
| const blink::StorageKey expected_final_storage_key = |
| blink::StorageKey::CreateFirstParty(expected_final_origin); |
| net::IsolationInfo expected_final_isolation_info = net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, expected_final_origin, |
| expected_final_origin, |
| net::SiteForCookies::FromOrigin(expected_final_origin), |
| /*nonce=*/std::nullopt, net::NetworkIsolationPartition::kGeneral, |
| net::IsolationInfo::FrameAncestorRelation::kSameOrigin); |
| // Start the test with a simple navigation. |
| { |
| std::unique_ptr<NavigationSimulator> simulator = |
| NavigationSimulator::CreateRendererInitiated(initial_url, main_rfh()); |
| simulator->Start(); |
| simulator->Commit(); |
| } |
| |
| // This test is targetted at the case an RFH is reused between navigations. |
| RenderFrameHost* initial_rfh = main_rfh(); |
| DisableProactiveBrowsingInstanceSwapFor(main_rfh()); |
| if (ShouldCreateNewHostForAllFrames()) { |
| GTEST_SKIP(); |
| } |
| |
| // Check values for the initial commit. |
| EXPECT_EQ(expected_initial_origin, main_rfh()->GetLastCommittedOrigin()); |
| EXPECT_EQ(expected_initial_storage_key, main_test_rfh()->GetStorageKey()); |
| EXPECT_TRUE(expected_initial_isolation_info.IsEqualForTesting( |
| main_rfh()->GetIsolationInfoForSubresources())); |
| EXPECT_EQ(expected_initial_isolation_info.network_isolation_key(), |
| main_rfh()->GetNetworkIsolationKey()); |
| EXPECT_TRUE(expected_initial_isolation_info.site_for_cookies().IsEquivalent( |
| static_cast<RenderFrameHostImpl*>(main_rfh())->ComputeSiteForCookies())); |
| EXPECT_TRUE(expected_initial_isolation_info.IsEqualForTesting( |
| main_rfh()->GetPendingIsolationInfoForSubresources())); |
| |
| // Values should be the same when a pending navigation was started but |
| // hasn't yet reached the ready-to-commit state. |
| std::unique_ptr<NavigationSimulator> simulator2 = |
| NavigationSimulator::CreateRendererInitiated(final_url, main_rfh()); |
| simulator2->Start(); |
| EXPECT_EQ(expected_initial_origin, main_rfh()->GetLastCommittedOrigin()); |
| EXPECT_EQ(expected_initial_storage_key, main_test_rfh()->GetStorageKey()); |
| EXPECT_TRUE(expected_initial_isolation_info.IsEqualForTesting( |
| main_rfh()->GetIsolationInfoForSubresources())); |
| EXPECT_EQ(expected_initial_isolation_info.network_isolation_key(), |
| main_rfh()->GetNetworkIsolationKey()); |
| EXPECT_TRUE(expected_initial_isolation_info.site_for_cookies().IsEquivalent( |
| static_cast<RenderFrameHostImpl*>(main_rfh())->ComputeSiteForCookies())); |
| EXPECT_TRUE(expected_initial_isolation_info.IsEqualForTesting( |
| main_rfh()->GetPendingIsolationInfoForSubresources())); |
| |
| // Only the GetPendingIsolationInfoForSubresources() should change when a |
| // pending navigation has reached the ready-to-commit state. |
| simulator2->ReadyToCommit(); |
| simulator2->Wait(); |
| EXPECT_EQ(expected_initial_origin, main_rfh()->GetLastCommittedOrigin()); |
| EXPECT_EQ(expected_initial_storage_key, main_test_rfh()->GetStorageKey()); |
| EXPECT_TRUE(expected_initial_isolation_info.IsEqualForTesting( |
| main_rfh()->GetIsolationInfoForSubresources())); |
| EXPECT_EQ(expected_initial_isolation_info.network_isolation_key(), |
| main_rfh()->GetNetworkIsolationKey()); |
| EXPECT_TRUE(expected_initial_isolation_info.site_for_cookies().IsEquivalent( |
| static_cast<RenderFrameHostImpl*>(main_rfh())->ComputeSiteForCookies())); |
| EXPECT_TRUE(expected_final_isolation_info.IsEqualForTesting( |
| main_rfh()->GetPendingIsolationInfoForSubresources())); |
| |
| // Verify expected main world origin once we are again in a steady state - |
| // after a commit. |
| simulator2->Commit(); |
| EXPECT_EQ(expected_final_origin, main_rfh()->GetLastCommittedOrigin()); |
| EXPECT_EQ(expected_final_storage_key, main_test_rfh()->GetStorageKey()); |
| EXPECT_TRUE(expected_final_isolation_info.IsEqualForTesting( |
| main_rfh()->GetIsolationInfoForSubresources())); |
| EXPECT_EQ(expected_final_isolation_info.network_isolation_key(), |
| main_rfh()->GetNetworkIsolationKey()); |
| EXPECT_TRUE(expected_final_isolation_info.site_for_cookies().IsEquivalent( |
| static_cast<RenderFrameHostImpl*>(main_rfh())->ComputeSiteForCookies())); |
| EXPECT_TRUE(expected_final_isolation_info.IsEqualForTesting( |
| main_rfh()->GetPendingIsolationInfoForSubresources())); |
| |
| // As a test correctness check, verify that there was no RFH swap. When |
| // there's always an RFH swap, this test will likely no longer be useful. |
| EXPECT_EQ(initial_rfh, main_rfh()); |
| } |
| |
| TEST_F(RenderFrameHostImplTest, PolicyContainerLifecycle) { |
| TestRenderFrameHost* main_rfh = contents()->GetPrimaryMainFrame(); |
| ASSERT_NE(main_rfh->policy_container_host(), nullptr); |
| EXPECT_EQ(main_rfh->policy_container_host()->referrer_policy(), |
| network::mojom::ReferrerPolicy::kDefault); |
| |
| static_cast<blink::mojom::PolicyContainerHost*>( |
| main_rfh->policy_container_host()) |
| ->SetReferrerPolicy(network::mojom::ReferrerPolicy::kAlways); |
| EXPECT_EQ(main_rfh->policy_container_host()->referrer_policy(), |
| network::mojom::ReferrerPolicy::kAlways); |
| |
| // Create a child frame and check that it inherits the PolicyContainerHost |
| // from the parent frame. |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child")); |
| |
| ASSERT_NE(child_frame->policy_container_host(), nullptr); |
| EXPECT_EQ(child_frame->policy_container_host()->referrer_policy(), |
| network::mojom::ReferrerPolicy::kAlways); |
| |
| // Create a new WebContents with opener and test that the new main frame |
| // inherits the PolicyContainerHost from the opener. |
| static_cast<blink::mojom::PolicyContainerHost*>( |
| child_frame->policy_container_host()) |
| ->SetReferrerPolicy(network::mojom::ReferrerPolicy::kNever); |
| WebContents::CreateParams params(browser_context()); |
| std::unique_ptr<WebContentsImpl> new_contents( |
| WebContentsImpl::CreateWithOpener(params, child_frame)); |
| RenderFrameHostImpl* new_frame = |
| new_contents->GetPrimaryFrameTree().root()->current_frame_host(); |
| |
| ASSERT_NE(new_frame->policy_container_host(), nullptr); |
| EXPECT_EQ(new_frame->policy_container_host()->referrer_policy(), |
| network::mojom::ReferrerPolicy::kNever); |
| } |
| |
| TEST_F(RenderFrameHostImplTest, FaviconURLsSet) { |
| TestRenderFrameHost* main_rfh = contents()->GetPrimaryMainFrame(); |
| const auto kFavicon = blink::mojom::FaviconURL( |
| GURL("https://example.com/favicon.ico"), |
| blink::mojom::FaviconIconType::kFavicon, {}, /*is_default_icon=*/false); |
| std::unique_ptr<NavigationSimulator> navigation = |
| NavigationSimulator::CreateBrowserInitiated(GURL("https://example.com"), |
| contents()); |
| ui::PageTransition transition = ui::PAGE_TRANSITION_LINK; |
| navigation->SetTransition(transition); |
| navigation->Commit(); |
| EXPECT_EQ(0u, contents()->GetFaviconURLs().size()); |
| |
| std::vector<blink::mojom::FaviconURLPtr> one_favicon_url; |
| one_favicon_url.push_back(blink::mojom::FaviconURL::New(kFavicon)); |
| main_rfh->UpdateFaviconURL(std::move(one_favicon_url)); |
| EXPECT_EQ(1u, contents()->GetFaviconURLs().size()); |
| |
| std::vector<blink::mojom::FaviconURLPtr> two_favicon_urls; |
| two_favicon_urls.push_back(blink::mojom::FaviconURL::New(kFavicon)); |
| two_favicon_urls.push_back(blink::mojom::FaviconURL::New(kFavicon)); |
| main_rfh->UpdateFaviconURL(std::move(two_favicon_urls)); |
| EXPECT_EQ(2u, contents()->GetFaviconURLs().size()); |
| |
| std::vector<blink::mojom::FaviconURLPtr> another_one_favicon_url; |
| another_one_favicon_url.push_back(blink::mojom::FaviconURL::New(kFavicon)); |
| main_rfh->UpdateFaviconURL(std::move(another_one_favicon_url)); |
| EXPECT_EQ(1u, contents()->GetFaviconURLs().size()); |
| } |
| |
| TEST_F(RenderFrameHostImplTest, FaviconURLsResetWithNavigation) { |
| TestRenderFrameHost* main_rfh = contents()->GetPrimaryMainFrame(); |
| std::vector<blink::mojom::FaviconURLPtr> favicon_urls; |
| favicon_urls.push_back(blink::mojom::FaviconURL::New( |
| GURL("https://example.com/favicon.ico"), |
| blink::mojom::FaviconIconType::kFavicon, std::vector<gfx::Size>(), |
| /*is_default_icon=*/false)); |
| |
| std::unique_ptr<NavigationSimulator> navigation = |
| NavigationSimulator::CreateBrowserInitiated(GURL("https://example.com"), |
| contents()); |
| ui::PageTransition transition = ui::PAGE_TRANSITION_LINK; |
| navigation->SetTransition(transition); |
| navigation->Commit(); |
| |
| EXPECT_EQ(0u, contents()->GetFaviconURLs().size()); |
| main_rfh->UpdateFaviconURL(std::move(favicon_urls)); |
| EXPECT_EQ(1u, contents()->GetFaviconURLs().size()); |
| |
| navigation = NavigationSimulator::CreateBrowserInitiated( |
| GURL("https://example.com/navigation.html"), contents()); |
| navigation->SetTransition(transition); |
| navigation->Commit(); |
| EXPECT_EQ(0u, contents()->GetFaviconURLs().size()); |
| } |
| |
| TEST_F(RenderFrameHostImplTest, ChildOfCredentiallessIsCredentialless) { |
| EXPECT_FALSE(main_test_rfh()->IsCredentialless()); |
| |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child")); |
| EXPECT_FALSE(child_frame->IsCredentialless()); |
| EXPECT_FALSE(child_frame->GetStorageKey().nonce().has_value()); |
| |
| auto attributes = blink::mojom::IframeAttributes::New(); |
| attributes->parsed_csp_attribute = std::move( |
| child_frame->frame_tree_node()->attributes_->parsed_csp_attribute); |
| attributes->id = child_frame->frame_tree_node()->html_id(); |
| attributes->name = child_frame->frame_tree_node()->html_name(); |
| attributes->src = child_frame->frame_tree_node()->html_src(); |
| attributes->credentialless = true; |
| child_frame->frame_tree_node()->SetAttributes(std::move(attributes)); |
| |
| EXPECT_FALSE(child_frame->IsCredentialless()); |
| EXPECT_FALSE(child_frame->GetStorageKey().nonce().has_value()); |
| |
| // A navigation in the credentialless iframe commits a credentialless RFH. |
| std::unique_ptr<NavigationSimulator> navigation = |
| NavigationSimulator::CreateRendererInitiated( |
| GURL("https://example.com/navigation.html"), child_frame); |
| navigation->Commit(); |
| child_frame = |
| static_cast<TestRenderFrameHost*>(navigation->GetFinalRenderFrameHost()); |
| EXPECT_TRUE(child_frame->IsCredentialless()); |
| EXPECT_TRUE(child_frame->GetStorageKey().nonce().has_value()); |
| |
| // A credentialless document sets a nonce on its network isolation key. |
| EXPECT_TRUE(child_frame->GetNetworkIsolationKey().GetNonce().has_value()); |
| EXPECT_EQ(main_test_rfh()->GetPage().credentialless_iframes_nonce(), |
| child_frame->GetNetworkIsolationKey().GetNonce().value()); |
| |
| // A child of a credentialless RFH is credentialless. |
| auto* grandchild_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(child_frame) |
| ->AppendChild("grandchild")); |
| EXPECT_TRUE(grandchild_frame->IsCredentialless()); |
| EXPECT_TRUE(grandchild_frame->GetStorageKey().nonce().has_value()); |
| |
| // The two credentialless RFH's storage keys should have the same nonce. |
| EXPECT_EQ(child_frame->GetStorageKey().nonce().value(), |
| grandchild_frame->GetStorageKey().nonce().value()); |
| |
| // Also the credentialless initial empty document sets a nonce on its network |
| // isolation key. |
| EXPECT_TRUE( |
| grandchild_frame->GetNetworkIsolationKey().GetNonce().has_value()); |
| EXPECT_EQ(main_test_rfh()->GetPage().credentialless_iframes_nonce(), |
| grandchild_frame->GetNetworkIsolationKey().GetNonce().value()); |
| } |
| |
| // FakeLocalFrame implementation that records calls to BeforeUnload(). |
| class FakeLocalFrameWithBeforeUnload : public content::FakeLocalFrame { |
| public: |
| explicit FakeLocalFrameWithBeforeUnload(TestRenderFrameHost* test_host) { |
| Init(test_host->GetRemoteAssociatedInterfaces()); |
| } |
| |
| bool was_before_unload_sent_to_renderer() const { |
| return was_before_unload_sent_to_renderer_; |
| } |
| |
| // FakeLocalFrame: |
| void BeforeUnload(bool is_reload, BeforeUnloadCallback callback) override { |
| was_before_unload_sent_to_renderer_ = true; |
| } |
| |
| private: |
| bool was_before_unload_sent_to_renderer_ = false; |
| }; |
| |
| // Verifies BeforeUnload() is not sent to renderer if there is no before |
| // unload handler present. |
| TEST_F(RenderFrameHostImplTest, BeforeUnloadNotSentToRenderer) { |
| TestRenderFrameHost* rfh = contents()->GetPrimaryMainFrame(); |
| base::RunLoop run_loop; |
| bool before_unload_completed = false; |
| rfh->set_on_process_before_unload_completed_for_testing( |
| base::BindLambdaForTesting([&]() { |
| before_unload_completed = true; |
| EXPECT_TRUE(rfh->is_waiting_for_beforeunload_completion()); |
| run_loop.Quit(); |
| })); |
| FakeLocalFrameWithBeforeUnload local_frame(rfh); |
| auto simulator = NavigationSimulatorImpl::CreateBrowserInitiated( |
| GURL("https://example.com/simple.html"), contents()); |
| simulator->set_block_invoking_before_unload_completed_callback(true); |
| simulator->Start(); |
| run_loop.Run(); |
| EXPECT_TRUE(before_unload_completed); |
| EXPECT_FALSE(local_frame.was_before_unload_sent_to_renderer()); |
| // This is necessary to trigger FakeLocalFrameWithBeforeUnload to be bound. |
| rfh->FlushLocalFrameMessages(); |
| // This runs a MessageLoop, which also results in the PostTask() scheduled |
| // completing. |
| local_frame.FlushMessages(); |
| EXPECT_FALSE(local_frame.was_before_unload_sent_to_renderer()); |
| // Because of the nested message loops run by the previous calls, the task |
| // that RenderFrameHostImpl will have also completed. |
| EXPECT_FALSE(rfh->is_waiting_for_beforeunload_completion()); |
| } |
| |
| class LoadingStateChangedDelegate : public WebContentsDelegate { |
| public: |
| void LoadingStateChanged(WebContents* source, |
| bool should_show_loading_ui) final { |
| should_show_loading_ui_ = should_show_loading_ui; |
| } |
| |
| bool should_show_loading_ui() { return should_show_loading_ui_; } |
| |
| private: |
| bool should_show_loading_ui_ = false; |
| }; |
| |
| TEST_F(RenderFrameHostImplTest, NavigationApiInterceptShowLoadingUi) { |
| // Initial commit. |
| const GURL url1("http://foo"); |
| NavigationSimulator::NavigateAndCommitFromDocument(url1, main_test_rfh()); |
| |
| std::unique_ptr<LoadingStateChangedDelegate> delegate = |
| std::make_unique<LoadingStateChangedDelegate>(); |
| contents()->SetDelegate(delegate.get()); |
| ASSERT_FALSE(delegate->should_show_loading_ui()); |
| ASSERT_FALSE(contents()->IsLoading()); |
| ASSERT_FALSE(contents()->ShouldShowLoadingUI()); |
| |
| // Emulate navigateEvent.intercept(). |
| const GURL url2("http://foo#a"); |
| auto params = mojom::DidCommitProvisionalLoadParams::New(); |
| params->did_create_new_entry = false; |
| params->url = url2; |
| params->origin = url::Origin::Create(url2); |
| params->referrer = blink::mojom::Referrer::New(); |
| params->transition = ui::PAGE_TRANSITION_LINK; |
| params->should_update_history = true; |
| params->method = "GET"; |
| params->page_state = blink::PageState::CreateFromURL(url2); |
| params->post_id = -1; |
| main_test_rfh()->SendDidCommitSameDocumentNavigation( |
| std::move(params), |
| blink::mojom::SameDocumentNavigationType::kNavigationApiIntercept, |
| /*should_replace_current_entry=*/false); |
| EXPECT_FALSE(delegate->should_show_loading_ui()); |
| EXPECT_TRUE(contents()->IsLoading()); |
| EXPECT_FALSE(contents()->ShouldShowLoadingUI()); |
| |
| // After a delay, the NavigationApi sends a message to start the loading UI. |
| // This delay is to prevent jitters due to short same-document navigations. |
| main_test_rfh()->SendStartLoadingForAsyncNavigationApiCommit(); |
| |
| // Once the delay has elapsed, navigateEvent.intercept() should leave |
| // WebContents in the loading state and showing loading UI, unlike other |
| // same-document navigations. |
| EXPECT_TRUE(delegate->should_show_loading_ui()); |
| EXPECT_TRUE(contents()->IsLoading()); |
| EXPECT_TRUE(contents()->ShouldShowLoadingUI()); |
| } |
| |
| TEST_F(RenderFrameHostImplTest, NavigationApiInterceptBrowserInitiated) { |
| // Initial commit. |
| const GURL url1("http://foo"); |
| NavigationSimulator::NavigateAndCommitFromDocument(url1, main_test_rfh()); |
| |
| std::unique_ptr<LoadingStateChangedDelegate> delegate = |
| std::make_unique<LoadingStateChangedDelegate>(); |
| contents()->SetDelegate(delegate.get()); |
| ASSERT_FALSE(delegate->should_show_loading_ui()); |
| ASSERT_FALSE(contents()->IsLoading()); |
| ASSERT_FALSE(contents()->ShouldShowLoadingUI()); |
| |
| // Emulate navigateEvent.intercept(). |
| const GURL url2("http://foo#a"); |
| std::unique_ptr<NavigationSimulator> navigation = |
| NavigationSimulator::CreateBrowserInitiated(url2, contents()); |
| navigation->Start(); |
| ASSERT_TRUE(contents()->IsLoading()); |
| ASSERT_FALSE(contents()->ShouldShowLoadingUI()); |
| |
| auto params = mojom::DidCommitProvisionalLoadParams::New(); |
| params->did_create_new_entry = false; |
| params->url = url2; |
| params->origin = url::Origin::Create(url2); |
| params->referrer = blink::mojom::Referrer::New(); |
| params->transition = ui::PAGE_TRANSITION_LINK; |
| params->should_update_history = true; |
| params->method = "GET"; |
| params->page_state = blink::PageState::CreateFromURL(url2); |
| params->post_id = -1; |
| main_test_rfh()->SendDidCommitSameDocumentNavigation( |
| std::move(params), |
| blink::mojom::SameDocumentNavigationType::kNavigationApiIntercept, true); |
| EXPECT_FALSE(delegate->should_show_loading_ui()); |
| EXPECT_TRUE(contents()->IsLoading()); |
| EXPECT_FALSE(contents()->ShouldShowLoadingUI()); |
| |
| // After a delay, the NavigationApi sends a message to start the loading UI. |
| // This delay is to prevent jitters due to short same-document navigations. |
| main_test_rfh()->SendStartLoadingForAsyncNavigationApiCommit(); |
| |
| // Once the delay has elapsed, navigateEvent.intercept() should leave |
| // WebContents in the loading state and showing loading UI, unlike other |
| // same-document navigations. |
| EXPECT_TRUE(delegate->should_show_loading_ui()); |
| EXPECT_TRUE(contents()->IsLoading()); |
| EXPECT_TRUE(contents()->ShouldShowLoadingUI()); |
| } |
| |
| // TODO(crbug.com/40260854): This test should be migrated to //chrome. |
| TEST_F(RenderFrameHostImplTest, CalculateStorageKey) { |
| // Register extension scheme for testing. |
| url::ScopedSchemeRegistryForTests scoped_registry; |
| url::AddStandardScheme("chrome-extension", url::SCHEME_WITH_HOST); |
| |
| GURL initial_url_ext = GURL("chrome-extension://initial.example.test/"); |
| NavigationSimulator::CreateRendererInitiated(initial_url_ext, main_rfh()) |
| ->Commit(); |
| |
| // Create a child frame and navigate to `child_url`. |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child")); |
| |
| GURL child_url = GURL("https://childframe.com"); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| NavigationSimulator::NavigateAndCommitFromDocument(child_url, |
| child_frame)); |
| |
| // Create a grandchild frame and navigate to `grandchild_url`. |
| auto* grandchild_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(child_frame) |
| ->AppendChild("grandchild")); |
| |
| GURL grandchild_url = GURL("https://grandchildframe.com/"); |
| grandchild_frame = static_cast<TestRenderFrameHost*>( |
| NavigationSimulator::NavigateAndCommitFromDocument(grandchild_url, |
| grandchild_frame)); |
| |
| // With no host permissions the grandchild document should have a cross-site |
| // storage key with the `initial_url_ext` as it's top level origin. |
| blink::StorageKey expected_grandchild_no_permissions_storage_key = |
| blink::StorageKey::Create( |
| grandchild_frame->GetLastCommittedOrigin(), |
| net::SchemefulSite(url::Origin::Create(initial_url_ext)), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| |
| EXPECT_EQ(expected_grandchild_no_permissions_storage_key, |
| grandchild_frame->CalculateStorageKey( |
| grandchild_frame->GetLastCommittedOrigin(), nullptr)); |
| |
| // Give extension host permissions to `grandchild_frame`. Since |
| // `grandchild_frame` is not the root non-extension frame |
| // `CalculateStorageKey` should still create a storage key that has the |
| // extension as the `top_level_site`. |
| AddHostPermissions("grandchildframe.com", main_rfh()); |
| |
| EXPECT_EQ(expected_grandchild_no_permissions_storage_key, |
| grandchild_frame->CalculateStorageKey( |
| grandchild_frame->GetLastCommittedOrigin(), nullptr)); |
| |
| // Now give extension host permissions to `child_frame`. Since the root |
| // extension rfh has host permissions to`child_frame` calling |
| // `CalculateStorageKey` should create a storage key with the `child_origin` |
| // as the `top_level_site`. |
| AddHostPermissions("childframe.com", main_rfh()); |
| |
| // Child host should now have a storage key that is same site and uses the |
| // `child_origin` as the `top_level_site`. |
| blink::StorageKey expected_child_with_permissions_storage_key = |
| blink::StorageKey::Create( |
| child_frame->GetLastCommittedOrigin(), |
| net::SchemefulSite(child_frame->GetLastCommittedOrigin()), |
| blink::mojom::AncestorChainBit::kSameSite); |
| EXPECT_EQ(expected_child_with_permissions_storage_key, |
| child_frame->CalculateStorageKey( |
| child_frame->GetLastCommittedOrigin(), nullptr)); |
| |
| blink::StorageKey expected_grandchild_with_permissions_storage_key = |
| blink::StorageKey::Create( |
| grandchild_frame->GetLastCommittedOrigin(), |
| net::SchemefulSite(child_frame->GetLastCommittedOrigin()), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| EXPECT_EQ(expected_grandchild_with_permissions_storage_key, |
| grandchild_frame->CalculateStorageKey( |
| grandchild_frame->GetLastCommittedOrigin(), nullptr)); |
| } |
| |
| // TODO(crbug.com/41483148): Flaky on Linux. |
| #if BUILDFLAG(IS_LINUX) |
| #define MAYBE_CalculateStorageKeyFirstPartyOverride \ |
| DISABLED_CalculateStorageKeyFirstPartyOverride |
| #else |
| #define MAYBE_CalculateStorageKeyFirstPartyOverride \ |
| CalculateStorageKeyFirstPartyOverride |
| #endif |
| |
| // TODO(crbug.com/40260854): Eventually, this test will be moved to |
| // chrome/browser/ so that we no longer need to override the |
| // ContentBrowserClient, and we can test using real extension URLs. |
| TEST_F(RenderFrameHostImplTest, MAYBE_CalculateStorageKeyFirstPartyOverride) { |
| // Enable third-party storage partitioning. |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| // Temporarily enable FirstPartyOverrideContentBrowserClient. This allows us |
| // to mock ShouldUseFirstPartyStorageKey() to check the origin scheme (as done |
| // in the ChromeContentBrowserClient) rather than indiscriminately return |
| // false (as written in the ContentBrowserClient implementation). |
| FirstPartyOverrideContentBrowserClient modified_client; |
| ContentBrowserClient* regular_client = |
| SetBrowserClientForTesting(&modified_client); |
| |
| // Register extension scheme for testing. |
| url::ScopedSchemeRegistryForTests scoped_registry; |
| url::AddStandardScheme("chrome-extension", url::SCHEME_WITH_HOST); |
| |
| // Navigate and commit to a non-extension URL. |
| GURL initial_url("https://initial.example.test"); |
| NavigationSimulator::CreateRendererInitiated(initial_url, main_rfh()) |
| ->Commit(); |
| |
| // Create a child extension frame and navigate to it. |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child")); |
| |
| // TODO(crbug.com/40260854): once this test is moved to chrome/browser/ |
| // replace with a legitimate chrome-extension URL. But for the purposes of |
| // this test, it is sufficient to check that it has a chrome-extension scheme. |
| GURL child_url = GURL("chrome-extension://childframeid"); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| NavigationSimulator::NavigateAndCommitFromDocument(child_url, |
| child_frame)); |
| |
| // Subframes that contain extension URLs should have first-party StorageKeys. |
| EXPECT_EQ(child_frame->GetLastCommittedOrigin().GetURL(), child_url); |
| blink::StorageKey expected_storage_key = blink::StorageKey::CreateFirstParty( |
| child_frame->GetLastCommittedOrigin()); |
| |
| EXPECT_EQ(expected_storage_key, |
| child_frame->CalculateStorageKey( |
| child_frame->GetLastCommittedOrigin(), /*nonce=*/nullptr)); |
| |
| SetBrowserClientForTesting(regular_client); |
| } |
| |
| TEST_F(RenderFrameHostImplTest, |
| CalculateStorageKeyWhenPassedOriginIsNotCurrentFrame) { |
| // Register extension scheme for testing. |
| url::ScopedSchemeRegistryForTests scoped_registry; |
| url::AddStandardScheme("chrome-extension", url::SCHEME_WITH_HOST); |
| |
| GURL initial_url_ext = GURL("chrome-extension://initial.example.test/"); |
| NavigationSimulator::CreateRendererInitiated(initial_url_ext, main_rfh()) |
| ->Commit(); |
| |
| // Create a child frame and navigate to `child_url`. |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child")); |
| |
| GURL child_url = GURL("https://childframe.com"); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| NavigationSimulator::NavigateAndCommitFromDocument(child_url, |
| child_frame)); |
| |
| // Give extension host permissions to `child_url`. |
| AddHostPermissions("childframe.com", main_rfh()); |
| |
| // The top level document has host permssions to the child_url so the top |
| // level document should be excluded from storage key calculations and a first |
| // party, same-site storage key is expected. |
| blink::StorageKey expected_child_with_permissions_storage_key = |
| blink::StorageKey::Create( |
| child_frame->GetLastCommittedOrigin(), |
| net::SchemefulSite(child_frame->GetLastCommittedOrigin()), |
| blink::mojom::AncestorChainBit::kSameSite); |
| EXPECT_EQ(expected_child_with_permissions_storage_key, |
| child_frame->CalculateStorageKey( |
| child_frame->GetLastCommittedOrigin(), nullptr)); |
| |
| // CalculateStorageKey is called with an origin that the top level document |
| // does not have host permissions to. A cross-site storage key is expected and |
| // the top level document's site should be used in the storage key |
| // calculation. |
| GURL no_host_permissions_url = GURL("https://noHostPermissions.com/"); |
| blink::StorageKey expected_storage_key_no_permissions = |
| blink::StorageKey::Create( |
| url::Origin::Create(no_host_permissions_url), |
| net::SchemefulSite(url::Origin::Create(initial_url_ext)), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| EXPECT_EQ(expected_storage_key_no_permissions, |
| child_frame->CalculateStorageKey( |
| url::Origin::Create(no_host_permissions_url), nullptr)); |
| } |
| |
| // Test that the correct StorageKey is calculated when a RFH takes its document |
| // properties from a navigation. |
| // TODO(crbug.com/40092527): Once we are able to compute the origin to |
| // commit in the browser, `navigation_request->commit_params().storage_key` |
| // will contain the correct origin and it won't be necessary to override it |
| // with `param.origin` anymore. Meaning this test may be removed because we |
| // already check that the NavigationRequest calculates the correct key. |
| TEST_F(RenderFrameHostImplTest, |
| CalculateStorageKeyTakeNewDocumentPropertiesFromNavigation) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| // Because the StorageKey's (and Storage Partitioning's) usage of |
| // RuntimeFeatureState is only meant to disable partitioning (i.e.: |
| // first-party only), we need the make sure the net::features is always |
| // enabled. |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| // This lamdba performs the navigation and disables Storage Partitioning for |
| // the navigation if `disable_sp` is true. It returns the new |
| // TestRenderFrameHost* to the navigated frame. |
| auto NavigateFrame = [](NavigationSimulator* navigation, |
| bool disable_sp = false) -> TestRenderFrameHost* { |
| navigation->Start(); |
| |
| if (disable_sp) { |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| // Disable Storage Partitioning by enabling the user bypass. |
| request->GetMutableRuntimeFeatureStateContext() |
| .SetThirdPartyStoragePartitioningUserBypassEnabled(true); |
| } |
| |
| navigation->Commit(); |
| return static_cast<TestRenderFrameHost*>( |
| navigation->GetFinalRenderFrameHost()); |
| }; |
| |
| // Throughout the test we'll be creating a frame tree with a main frame, a |
| // child frame, and a grandchild frame. |
| GURL main_url("https://main.com"); |
| GURL b_url("https://b.com"); |
| GURL c_url("https://c.com"); |
| |
| url::Origin main_origin = url::Origin::Create(main_url); |
| url::Origin b_origin = url::Origin::Create(b_url); |
| url::Origin c_origin = url::Origin::Create(c_url); |
| |
| // Begin by testing with Storage Partitioning enabled. |
| |
| auto main_navigation = |
| NavigationSimulatorImpl::CreateBrowserInitiated(main_url, contents()); |
| |
| // By definition the main frame's StorageKey will always be first party |
| blink::StorageKey main_frame_key = |
| blink::StorageKey::CreateFirstParty(main_origin); |
| |
| NavigateFrame(main_navigation.get()); |
| |
| EXPECT_EQ(main_frame_key, main_test_rfh()->GetStorageKey()); |
| |
| TestRenderFrameHost* child_frame = static_cast<TestRenderFrameHost*>( |
| RenderFrameHostTester::For(main_rfh())->AppendChild("child")); |
| |
| auto child_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(b_url, child_frame); |
| |
| // The child and grandchild should both be third-party keys. |
| blink::StorageKey child_frame_key = |
| blink::StorageKey::Create(b_origin, net::SchemefulSite(main_origin), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| |
| child_frame = NavigateFrame(child_navigation.get()); |
| |
| EXPECT_EQ(child_frame_key, child_frame->GetStorageKey()); |
| |
| TestRenderFrameHost* grandchild_frame = |
| child_frame->AppendChild("grandchild"); |
| |
| auto grandchild_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(c_url, grandchild_frame); |
| |
| blink::StorageKey grandchild_frame_key = |
| blink::StorageKey::Create(c_origin, net::SchemefulSite(main_origin), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| grandchild_frame = NavigateFrame(grandchild_navigation.get()); |
| |
| EXPECT_EQ(grandchild_frame_key, grandchild_frame->GetStorageKey()); |
| |
| // Only the RuntimeFeatureStateContext in the main frame's matters. So |
| // disabling Storage Partitioning in the child_frame shouldn't affect the |
| // child's or the grandchild's StorageKey. |
| child_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(b_url, child_frame); |
| |
| child_frame = NavigateFrame(child_navigation.get(), |
| /*disable_sp=*/true); |
| EXPECT_EQ(child_frame_key, child_frame->GetStorageKey()); |
| |
| grandchild_frame = child_frame->AppendChild("grandchild"); |
| |
| grandchild_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(c_url, grandchild_frame); |
| |
| grandchild_frame = NavigateFrame(grandchild_navigation.get()); |
| |
| EXPECT_EQ(grandchild_frame_key, grandchild_frame->GetStorageKey()); |
| |
| // Disabling Storage Partitioning on the main frame should cause the child's |
| // and grandchild's StorageKey to be first-party. |
| main_navigation = |
| NavigationSimulatorImpl::CreateBrowserInitiated(main_url, contents()); |
| |
| NavigateFrame(main_navigation.get(), |
| /*disable_sp=*/true); |
| |
| child_frame = static_cast<TestRenderFrameHost*>( |
| RenderFrameHostTester::For(main_rfh())->AppendChild("child")); |
| |
| child_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(b_url, child_frame); |
| |
| // The child and grandchild should both be first-party keys. |
| blink::StorageKey child_frame_key_1p = |
| blink::StorageKey::CreateFirstParty(b_origin); |
| |
| child_frame = NavigateFrame(child_navigation.get()); |
| |
| EXPECT_EQ(child_frame_key_1p, child_frame->GetStorageKey()); |
| |
| grandchild_frame = child_frame->AppendChild("grandchild"); |
| |
| blink::StorageKey grandchild_frame_key_1p = |
| blink::StorageKey::CreateFirstParty(c_origin); |
| |
| grandchild_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(c_url, grandchild_frame); |
| |
| grandchild_frame = NavigateFrame(grandchild_navigation.get()); |
| |
| EXPECT_EQ(grandchild_frame_key_1p, grandchild_frame->GetStorageKey()); |
| } |
| |
| // Tests that the StorageKey calculated for a frame under an extension main |
| // frame has storage partitioning enabled/disabled as expected via the |
| // RuntimeFeatureStateReadContext when the extension has host permissions. |
| // TODO(crbug.com/40260854): This test should be migrated to //chrome. |
| TEST_F(RenderFrameHostImplTest, |
| CalculateStorageKeyStoragePartitioningCorrectFrameWithExtension) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| // Because the StorageKey's (and Storage Partitioning's) usage of |
| // RuntimeFeatureState is only meant to disable partitioning (i.e.: |
| // first-party only), we need the make sure the net::features is always |
| // enabled. |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| // Register extension scheme for testing. |
| url::ScopedSchemeRegistryForTests scoped_registry; |
| url::AddStandardScheme("chrome-extension", url::SCHEME_WITH_HOST); |
| |
| GURL initial_url_ext = GURL("chrome-extension://initial.example.test/"); |
| NavigationSimulator::CreateRendererInitiated(initial_url_ext, main_rfh()) |
| ->Commit(); |
| |
| // Create a child frame, disable Storage Partitioning, and navigate to |
| // `child_url`. |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child")); |
| |
| GURL child_url = GURL("https://childframe.com"); |
| auto child_navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated(child_url, child_frame); |
| |
| // This lamdba performs the navigation and disables Storage Partitioning for |
| // the navigation if `disable_sp` is true. It returns the new |
| // TestRenderFrameHost* to the navigated frame. |
| auto NavigateFrame = [](NavigationSimulator* navigation, |
| bool disable_sp = false) -> TestRenderFrameHost* { |
| navigation->Start(); |
| |
| if (disable_sp) { |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| request->GetMutableRuntimeFeatureStateContext() |
| .SetThirdPartyStoragePartitioningUserBypassEnabled(true); |
| } |
| |
| navigation->Commit(); |
| return static_cast<TestRenderFrameHost*>( |
| navigation->GetFinalRenderFrameHost()); |
| }; |
| |
| child_frame = NavigateFrame(child_navigation.get(), /*disable_sp=*/true); |
| |
| // Create a grandchild frame and navigate to `grandchild_url`. |
| auto* grandchild_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(child_frame) |
| ->AppendChild("grandchild")); |
| |
| GURL grandchild_url = GURL("https://grandchildframe.com/"); |
| grandchild_frame = static_cast<TestRenderFrameHost*>( |
| NavigationSimulator::NavigateAndCommitFromDocument(grandchild_url, |
| grandchild_frame)); |
| |
| // At this point the extension doesn't have host permissions for the |
| // child_frame, so the child_frame's RuntimeFeatureStateReadContext's state |
| // isn't used and therefore any StorageKeys created for the grandchild_frame |
| // will be third-party. |
| url::Origin grandchild_origin = url::Origin::Create(grandchild_url); |
| blink::StorageKey grandchild_frame_key = blink::StorageKey::Create( |
| grandchild_origin, net::SchemefulSite(initial_url_ext), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| |
| EXPECT_EQ(grandchild_frame_key, |
| grandchild_frame->CalculateStorageKey( |
| grandchild_frame->GetLastCommittedOrigin(), nullptr)); |
| |
| // Now give extension host permissions to `child_frame`. Since the root |
| // extension rfh has host permissions to`child_frame` calling |
| // `CalculateStorageKey` should use the RuntimeFeatureStateReadContext in |
| // child_frame thereby creating a first-party StorageKey in grandchild_frame |
| // (since we disabled storage partitioning). |
| AddHostPermissions("childframe.com", main_rfh()); |
| |
| blink::StorageKey grandchild_frame_key_1P = |
| blink::StorageKey::CreateFirstParty(grandchild_origin); |
| |
| EXPECT_EQ(grandchild_frame_key_1P, |
| grandchild_frame->CalculateStorageKey( |
| grandchild_frame->GetLastCommittedOrigin(), nullptr)); |
| } |
| |
| // Test that CalculateStorageKey creates a first-party or third-party key |
| // depending on state of Storage Partitioning the main frame's |
| // RuntimeFeatureStateReadContext for a new unnavigated frame. |
| TEST_F(RenderFrameHostImplTest, CalculateStorageKeyOfUnnavigatedFrame) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| // Because Storage partitioning's usage of RuntimeFeatureState is only meant |
| // to disable (i.e.: 1p only) partitioning, we need the make sure the feature |
| // is on first. |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| // This test will create a main frame that has Storage Partitioning disabled |
| // via its RuntimeFeatureStateReadContext. It will have a navigated child |
| // frame's whose RFSRC will be the default (i.e.: Storage Partitioning |
| // enabled) and that child will then spawn an unnavigated grandchild whose |
| // StorageKey should still depend upon the main frame's RFSRC. |
| |
| GURL url = GURL("https://a.com"); |
| GURL child_url = GURL("https://b.com"); |
| |
| // Start by giving the main frame a SP disabled |
| // RuntimeFeatureStateReadContext. |
| auto navigation = |
| NavigationSimulator::CreateRendererInitiated(url, main_rfh()); |
| navigation->Start(); |
| |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| |
| // Disable Storage Partitioning by enabling the user bypass. |
| request->GetMutableRuntimeFeatureStateContext() |
| .SetThirdPartyStoragePartitioningUserBypassEnabled(true); |
| |
| navigation->Commit(); |
| |
| EXPECT_TRUE(RuntimeFeatureStateDocumentData::GetForCurrentDocument(main_rfh()) |
| ->runtime_feature_state_read_context() |
| .IsThirdPartyStoragePartitioningUserBypassEnabled()); |
| |
| // Create a child frame and navigate to `child_url`. |
| auto* child_frame = main_test_rfh()->AppendChild("child"); |
| auto child_navigation = |
| NavigationSimulator::CreateRendererInitiated(child_url, child_frame); |
| child_navigation->Commit(); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| child_navigation->GetFinalRenderFrameHost()); |
| |
| // Create a grand child and check it's StorageKey. |
| auto* grandchild_frame = child_frame->AppendChild("grandchild"); |
| |
| // Since Storage Partitioning is disabled, the key should be first party. |
| blink::StorageKey grandchild_frame_key_1p = |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(child_url)); |
| EXPECT_EQ(grandchild_frame_key_1p, grandchild_frame->GetStorageKey()); |
| |
| // Now perform the same test, except the main frame also gets a default |
| // RuntimeFeatureStateReadContext. (I.e.: Storage Partitioning enabled). |
| NavigationSimulator::NavigateAndCommitFromDocument(url, main_rfh()); |
| |
| child_frame = main_test_rfh()->AppendChild("child"); |
| child_navigation = |
| NavigationSimulator::CreateRendererInitiated(child_url, child_frame); |
| child_navigation->Commit(); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| child_navigation->GetFinalRenderFrameHost()); |
| |
| grandchild_frame = child_frame->AppendChild("grandchild"); |
| |
| blink::StorageKey grandchild_frame_key = |
| blink::StorageKey::Create(url::Origin::Create(child_url), |
| net::SchemefulSite(url::Origin::Create(url)), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| EXPECT_EQ(grandchild_frame_key, grandchild_frame->GetStorageKey()); |
| } |
| |
| TEST_F(RenderFrameHostImplTest, |
| NewFrameInheritsRuntimeFeatureStateReadContext) { |
| GURL url = GURL("https://a.com"); |
| GURL child_url = GURL("https://b.com"); |
| |
| // Start by giving the main frame a non-default |
| // RuntimeFeatureStateReadContext. |
| |
| auto navigation = |
| NavigationSimulator::CreateRendererInitiated(url, main_rfh()); |
| navigation->Start(); |
| |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| |
| request->GetMutableRuntimeFeatureStateContext().SetTestFeatureEnabled(true); |
| |
| navigation->Commit(); |
| |
| EXPECT_TRUE(RuntimeFeatureStateDocumentData::GetForCurrentDocument(main_rfh()) |
| ->runtime_feature_state_read_context() |
| .IsTestFeatureEnabled()); |
| |
| // Now add a child and check its RFSRC. |
| auto* child_frame = main_test_rfh()->AppendChild("child"); |
| EXPECT_TRUE( |
| RuntimeFeatureStateDocumentData::GetForCurrentDocument(child_frame) |
| ->runtime_feature_state_read_context() |
| .IsTestFeatureEnabled()); |
| |
| // Navigating the child away should change the RFSRC. |
| auto child_navigation = |
| NavigationSimulator::CreateRendererInitiated(child_url, child_frame); |
| child_navigation->Commit(); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| child_navigation->GetFinalRenderFrameHost()); |
| EXPECT_FALSE( |
| RuntimeFeatureStateDocumentData::GetForCurrentDocument(child_frame) |
| ->runtime_feature_state_read_context() |
| .IsTestFeatureEnabled()); |
| } |
| |
| class TestUnpartitionedStorageAcessContentBrowserClient |
| : public ContentBrowserClient { |
| public: |
| TestUnpartitionedStorageAcessContentBrowserClient() = default; |
| ~TestUnpartitionedStorageAcessContentBrowserClient() override = default; |
| |
| bool IsUnpartitionedStorageAccessAllowedByUserPreference( |
| BrowserContext* browser_context, |
| const GURL& url, |
| const net::SiteForCookies& site_for_cookies, |
| const url::Origin& top_frame_origin) override { |
| return is_unpartitioned_storage_access_allowed_by_user_preference_; |
| } |
| |
| void SetIsUnpartitionedStorageAccessAllowedByUserPreference(bool value) { |
| is_unpartitioned_storage_access_allowed_by_user_preference_ = value; |
| } |
| |
| private: |
| bool is_unpartitioned_storage_access_allowed_by_user_preference_ = false; |
| }; |
| |
| // Test that CalculateStorageKey will create a first-party or third-party key, |
| // in the presence of a user bypass, depending on the state of |
| // IsUnpartitionedStorageAccessAllowedByUserPreference() |
| TEST_F( |
| RenderFrameHostImplTest, |
| CalculateStorageKeyWithIsUnpartitionedStorageAccessAllowedByUserPreference) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| // Because Storage partitioning's usage of RuntimeFeatureState is only meant |
| // to disable (i.e.: 1p only) partitioning, we need the make sure the feature |
| // is on first. |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| TestUnpartitionedStorageAcessContentBrowserClient client; |
| ContentBrowserClient* regular_client = SetBrowserClientForTesting(&client); |
| |
| client.SetIsUnpartitionedStorageAccessAllowedByUserPreference(true); |
| |
| // This test will create a main frame that has a storage partitioning |
| // user bypass active and a child frame that is navigated to a |
| // third-party site. Since IsUnpartitionedStorageAccessAllowedByUserPreference |
| // returns true the child frame's StorageKey should be first-party. |
| |
| GURL url = GURL("https://a.com"); |
| GURL child_url = GURL("https://b.com"); |
| |
| // Start by giving the main frame a SP disabled |
| // RuntimeFeatureStateReadContext. |
| auto navigation = |
| NavigationSimulator::CreateRendererInitiated(url, main_rfh()); |
| navigation->Start(); |
| |
| NavigationRequest* request = |
| NavigationRequest::From(navigation->GetNavigationHandle()); |
| |
| // Disable Storage Partitioning by enabling the user bypass. |
| request->GetMutableRuntimeFeatureStateContext() |
| .SetThirdPartyStoragePartitioningUserBypassEnabled(true); |
| |
| navigation->Commit(); |
| |
| EXPECT_TRUE(RuntimeFeatureStateDocumentData::GetForCurrentDocument(main_rfh()) |
| ->runtime_feature_state_read_context() |
| .IsThirdPartyStoragePartitioningUserBypassEnabled()); |
| |
| // Create a child frame and navigate to `child_url`. |
| auto* child_frame = main_test_rfh()->AppendChild("child"); |
| auto child_navigation = |
| NavigationSimulator::CreateRendererInitiated(child_url, child_frame); |
| child_navigation->Commit(); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| child_navigation->GetFinalRenderFrameHost()); |
| |
| // Since IsUnpartitionedStorageAccessAllowedByUserPreference is true the |
| // StorageKey should be first-party. |
| blink::StorageKey child_frame_key_1p = |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(child_url)); |
| EXPECT_EQ(child_frame_key_1p, child_frame->GetStorageKey()); |
| |
| // Now perform the same test, except |
| // IsUnpartitionedStorageAccessAllowedByUserPreference is false. |
| client.SetIsUnpartitionedStorageAccessAllowedByUserPreference(false); |
| GURL child_url2 = GURL("https://c.com"); |
| |
| child_navigation = |
| NavigationSimulator::CreateRendererInitiated(child_url2, child_frame); |
| child_navigation->Commit(); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| child_navigation->GetFinalRenderFrameHost()); |
| |
| // Since IsUnpartitionedStorageAccessAllowedByUserPreference is false the |
| // StorageKey should be third-party. |
| blink::StorageKey child_frame_key_3p = |
| blink::StorageKey::Create(url::Origin::Create(child_url2), |
| net::SchemefulSite(url::Origin::Create(url)), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| EXPECT_EQ(child_frame_key_3p, child_frame->GetStorageKey()); |
| |
| SetBrowserClientForTesting(regular_client); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| class TestWebAuthnContentBrowserClientImpl : public ContentBrowserClient { |
| public: |
| MOCK_METHOD(bool, |
| IsSecurityLevelAcceptableForWebAuthn, |
| (RenderFrameHost*, const url::Origin& origin), |
| ()); |
| }; |
| |
| class RenderFrameHostImplWebAuthnTest : public RenderFrameHostImplTest { |
| public: |
| void SetUp() override { |
| RenderFrameHostImplTest::SetUp(); |
| old_browser_client_ = SetBrowserClientForTesting(browser_client_.get()); |
| contents()->GetController().LoadURLWithParams( |
| NavigationController::LoadURLParams( |
| GURL("https://example.com/navigation.html"))); |
| } |
| |
| void TearDown() override { |
| RenderFrameHostImplTest::TearDown(); |
| SetBrowserClientForTesting(old_browser_client_); |
| } |
| |
| protected: |
| raw_ptr<ContentBrowserClient> old_browser_client_; |
| std::unique_ptr<TestWebAuthnContentBrowserClientImpl> browser_client_ = |
| std::make_unique<TestWebAuthnContentBrowserClientImpl>(); |
| }; |
| |
| TEST_F(RenderFrameHostImplWebAuthnTest, |
| PerformGetAssertionWebAuthSecurityChecks_TLSError) { |
| GURL url("https://doofenshmirtz.evil"); |
| const auto origin = url::Origin::Create(url); |
| EXPECT_CALL(*browser_client_, |
| IsSecurityLevelAcceptableForWebAuthn(main_test_rfh(), origin)) |
| .WillOnce(testing::Return(false)); |
| std::optional<blink::mojom::AuthenticatorStatus> status; |
| main_test_rfh()->PerformGetAssertionWebAuthSecurityChecks( |
| "doofenshmirtz.evil", url::Origin::Create(url), |
| /*is_payment_credential_get_assertion=*/false, |
| /*remote_desktop_client_override_origin=*/std::nullopt, |
| base::BindLambdaForTesting( |
| [&status](blink::mojom::AuthenticatorStatus s, bool is_cross_origin) { |
| status = s; |
| })); |
| EXPECT_EQ(status.value(), |
| blink::mojom::AuthenticatorStatus::CERTIFICATE_ERROR); |
| } |
| |
| TEST_F(RenderFrameHostImplWebAuthnTest, |
| PerformMakeCredentialWebAuthSecurityChecks_TLSError) { |
| GURL url("https://doofenshmirtz.evil"); |
| const auto origin = url::Origin::Create(url); |
| EXPECT_CALL(*browser_client_, |
| IsSecurityLevelAcceptableForWebAuthn(main_test_rfh(), origin)) |
| .WillOnce(testing::Return(false)); |
| std::optional<blink::mojom::AuthenticatorStatus> status; |
| main_test_rfh()->PerformMakeCredentialWebAuthSecurityChecks( |
| "doofenshmirtz.evil", url::Origin::Create(url), |
| /*is_payment_credential_creation=*/false, |
| /*remote_desktop_client_override_origin=*/std::nullopt, |
| base::BindLambdaForTesting( |
| [&status](blink::mojom::AuthenticatorStatus s, bool is_cross_origin) { |
| status = s; |
| })); |
| EXPECT_EQ(status.value(), |
| blink::mojom::AuthenticatorStatus::CERTIFICATE_ERROR); |
| } |
| |
| TEST_F(RenderFrameHostImplWebAuthnTest, |
| PerformGetAssertionWebAuthSecurityChecks_Success) { |
| GURL url("https://owca.org"); |
| const auto origin = url::Origin::Create(url); |
| EXPECT_CALL(*browser_client_, |
| IsSecurityLevelAcceptableForWebAuthn(main_test_rfh(), origin)) |
| .WillOnce(testing::Return(true)); |
| std::optional<blink::mojom::AuthenticatorStatus> status; |
| main_test_rfh()->PerformGetAssertionWebAuthSecurityChecks( |
| "owca.org", url::Origin::Create(url), |
| /*is_payment_credential_get_assertion=*/false, |
| /*remote_desktop_client_override_origin=*/std::nullopt, |
| base::BindLambdaForTesting( |
| [&status](blink::mojom::AuthenticatorStatus s, bool is_cross_origin) { |
| status = s; |
| })); |
| EXPECT_EQ(status.value(), blink::mojom::AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(RenderFrameHostImplWebAuthnTest, |
| PerformMakeCredentialWebAuthSecurityChecks_Success) { |
| GURL url("https://owca.org"); |
| const auto origin = url::Origin::Create(url); |
| EXPECT_CALL(*browser_client_, |
| IsSecurityLevelAcceptableForWebAuthn(main_test_rfh(), origin)) |
| .WillOnce(testing::Return(true)); |
| std::optional<blink::mojom::AuthenticatorStatus> status; |
| main_test_rfh()->PerformMakeCredentialWebAuthSecurityChecks( |
| "owca.org", url::Origin::Create(url), |
| /*is_payment_credential_creation=*/false, |
| /*remote_desktop_client_override_origin=*/std::nullopt, |
| base::BindLambdaForTesting( |
| [&status](blink::mojom::AuthenticatorStatus s, bool is_cross_origin) { |
| status = s; |
| })); |
| EXPECT_EQ(status.value(), blink::mojom::AuthenticatorStatus::SUCCESS); |
| } |
| |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| class AvoidUnnecessaryBeforeUnloadCheckSyncTest |
| : public RenderFrameHostImplTest { |
| public: |
| class ForcePostTaskContentBrowserClient : public ContentBrowserClient { |
| bool SupportsAvoidUnnecessaryBeforeUnloadCheckSync() override { |
| return false; |
| } |
| }; |
| |
| // In this function, the following code path will be executed on navigation. |
| // |
| // [AvoidUnnecessaryBeforeUnloadCheckSync disabled] |
| // - Start a browser initiated navigation. |
| // - Run TestRenderFrameHost::SendBeforeUnload() |
| // - Run on_sendbeforeunload_begin_ closure |
| // - Run RenderFrameHostImpl::SendBeforeUnload() => Post a task |
| // - Run on_sendbeforeunload_end_ closure |
| // |
| // (In the posted task) |
| // - Run RenderFrameHostImpl::ProcessBeforeUnloadCompleted() |
| // - Run on_process_before_unload_completed_for_testing_ closure |
| // |
| // [AvoidUnnecessaryBeforeUnloadCheckSync + `kWithSendBeforeUnload`] |
| // - Start a browser initiated navigation. |
| // - Run TestRenderFrameHost::SendBeforeUnload() |
| // - Run on_sendbeforeunload_begin_ closure |
| // - Run RenderFrameHostImpl::SendBeforeUnload() |
| // - Run RenderFrameHostImpl::ProcessBeforeUnloadCompleted() |
| // - Run on_process_before_unload_completed_for_testing_ closure |
| // - Run on_sendbeforeunload_end_ closure |
| // |
| // [AvoidUnnecessaryBeforeUnloadCheckSync + `kWithoutSendBeforeUnload`] |
| // - Start a browser initiated navigation. |
| // - (the rest will be skipped) |
| // |
| // - expect_beforeunload_processed_on_sendbeforeunload_stack argument checks |
| // if ProcessBeforeUnloadCompleted() is called without posting a task by |
| // checking it in on_sendbeforeunload_end_ closure. This argument is |
| // optional because this argument doesn't make sense when SendBeforeUnload() |
| // and ProcessBeforeUnloadCompleted are not called at all. |
| // |
| // - expect_to_run_sendbeforeunload argument checks if both |
| // SendBeforeUnload() and ProcessBeforeUnloadCompleted() are called or not. |
| void TestBeforeUnloadBehaviorOnNavigation( |
| std::optional<bool> |
| expect_beforeunload_processed_on_sendbeforeunload_stack, |
| bool expect_to_run_sendbeforeunload, |
| const base::Location& location = FROM_HERE) { |
| TestRenderFrameHost* rfh = contents()->GetPrimaryMainFrame(); |
| bool beforeunload_processed = false; |
| bool run_sendbeforeunload = false; |
| // The following callback is called when processing beforeunload is |
| // completed. |
| rfh->set_on_process_before_unload_completed_for_testing( |
| base::BindLambdaForTesting([&]() { beforeunload_processed = true; })); |
| // The following callback is called when SendBeforeUnload() is about to |
| // start. |
| rfh->set_on_sendbeforeunload_begin(base::BindLambdaForTesting([&]() { |
| EXPECT_FALSE(beforeunload_processed) << location.ToString(); |
| })); |
| // The following callback is called when SendBeforeUnload() is about to end. |
| rfh->set_on_sendbeforeunload_end(base::BindLambdaForTesting([&]() { |
| EXPECT_EQ(beforeunload_processed, |
| *expect_beforeunload_processed_on_sendbeforeunload_stack) |
| << location.ToString(); |
| run_sendbeforeunload = true; |
| })); |
| |
| auto simulator = NavigationSimulatorImpl::CreateBrowserInitiated( |
| GURL("https://example.com/navigation.html"), contents()); |
| simulator->Start(); |
| simulator->Wait(); |
| |
| EXPECT_EQ(beforeunload_processed, expect_to_run_sendbeforeunload) |
| << location.ToString(); |
| EXPECT_EQ(run_sendbeforeunload, expect_to_run_sendbeforeunload) |
| << location.ToString(); |
| } |
| }; |
| |
| TEST_F(AvoidUnnecessaryBeforeUnloadCheckSyncTest, EnabledWithSendBeforeUnload) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeaturesAndParameters( |
| /*enabled_features=*/{{features::kAvoidUnnecessaryBeforeUnloadCheckSync, |
| {{features:: |
| kAvoidUnnecessaryBeforeUnloadCheckSyncMode |
| .name, |
| "WithSendBeforeUnload"}}}}, |
| /*disabled_features=*/{}); |
| |
| TestBeforeUnloadBehaviorOnNavigation( |
| /*expect_beforeunload_processed_on_sendbeforeunload_stack=*/true, |
| /*expect_to_run_sendbeforeunload=*/true); |
| } |
| |
| TEST_F(AvoidUnnecessaryBeforeUnloadCheckSyncTest, Disabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndDisableFeature( |
| features::kAvoidUnnecessaryBeforeUnloadCheckSync); |
| |
| TestBeforeUnloadBehaviorOnNavigation( |
| /*expect_beforeunload_processed_on_sendbeforeunload_stack=*/false, |
| /*expect_to_run_sendbeforeunload=*/true); |
| } |
| |
| TEST_F(AvoidUnnecessaryBeforeUnloadCheckSyncTest, |
| EnabledWithSendBeforeUnloadButBrowserClientProhibits) { |
| ForcePostTaskContentBrowserClient force_post_task_content_browser_client; |
| ContentBrowserClient* old_browser_client = |
| SetBrowserClientForTesting(&force_post_task_content_browser_client); |
| |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeaturesAndParameters( |
| /*enabled_features=*/{{features::kAvoidUnnecessaryBeforeUnloadCheckSync, |
| {{features:: |
| kAvoidUnnecessaryBeforeUnloadCheckSyncMode |
| .name, |
| "WithSendBeforeUnload"}}}}, |
| /*disabled_features=*/{}); |
| |
| // SupportsAvoidUnnecessaryBeforeUnloadCheckSync() takes precedence over |
| // enabling the kAvoidUnnecessaryBeforeUnloadCheckSync feature. |
| TestBeforeUnloadBehaviorOnNavigation( |
| /*expect_beforeunload_processed_on_sendbeforeunload_stack=*/false, |
| /*expect_to_run_sendbeforeunload=*/true); |
| |
| SetBrowserClientForTesting(old_browser_client); |
| } |
| |
| TEST_F(AvoidUnnecessaryBeforeUnloadCheckSyncTest, |
| EnabledWithoutSendBeforeUnload) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeaturesAndParameters( |
| /*enabled_features=*/{{features::kAvoidUnnecessaryBeforeUnloadCheckSync, |
| {{features:: |
| kAvoidUnnecessaryBeforeUnloadCheckSyncMode |
| .name, |
| "WithoutSendBeforeUnload"}}}}, |
| /*disabled_features=*/{}); |
| |
| TestBeforeUnloadBehaviorOnNavigation( |
| /*expect_beforeunload_processed_on_send_beforeunload_stack=*/std::nullopt, |
| /*expect_to_run_send_beforeunload=*/false); |
| } |
| |
| TEST_F(AvoidUnnecessaryBeforeUnloadCheckSyncTest, |
| EnabledWithoutSendBeforeUnloadButBrowserClientProhibits) { |
| ForcePostTaskContentBrowserClient force_post_task_content_browser_client; |
| ContentBrowserClient* old_browser_client = |
| SetBrowserClientForTesting(&force_post_task_content_browser_client); |
| |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeaturesAndParameters( |
| /*enabled_features=*/{{features::kAvoidUnnecessaryBeforeUnloadCheckSync, |
| {{features:: |
| kAvoidUnnecessaryBeforeUnloadCheckSyncMode |
| .name, |
| "WithoutSendBeforeUnload"}}}}, |
| /*disabled_features=*/{}); |
| |
| // SupportsAvoidUnnecessaryBeforeUnloadCheckSync() takes precedence over |
| // enabling the kAvoidUnnecessaryBeforeUnloadCheckSync feature. |
| TestBeforeUnloadBehaviorOnNavigation( |
| /*expect_beforeunload_processed_on_send_beforeunload_stack=*/false, |
| /*expect_to_run_send_beforeunload=*/true); |
| |
| SetBrowserClientForTesting(old_browser_client); |
| } |
| |
| class RenderFrameHostImplThirdPartyStorageTest |
| : public RenderViewHostImplTestHarness, |
| public testing::WithParamInterface<bool> { |
| public: |
| void SetUp() override { |
| RenderViewHostImplTestHarness::SetUp(); |
| contents()->GetPrimaryMainFrame()->InitializeRenderFrameIfNeeded(); |
| if (ThirdPartyStoragePartitioningEnabled()) { |
| scoped_feature_list_.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| } else { |
| scoped_feature_list_.InitAndDisableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| } |
| } |
| bool ThirdPartyStoragePartitioningEnabled() { return GetParam(); } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| RenderFrameHostImplThirdPartyStorageTest, |
| /*third_party_storage_partitioning_enabled*/ testing::Bool()); |
| |
| TEST_P(RenderFrameHostImplThirdPartyStorageTest, |
| ChildFramePartitionedByThirdPartyStorageKey) { |
| GURL initial_url = GURL("https://initial.example.test/"); |
| |
| NavigationSimulator::CreateRendererInitiated(initial_url, main_rfh()) |
| ->Commit(); |
| |
| // Create a child frame and check that it has the correct storage key. |
| auto* child_frame = static_cast<TestRenderFrameHost*>( |
| content::RenderFrameHostTester::For(main_test_rfh()) |
| ->AppendChild("child")); |
| |
| GURL child_url = GURL("https://exampleChildSite.com"); |
| child_frame = static_cast<TestRenderFrameHost*>( |
| NavigationSimulator::NavigateAndCommitFromDocument(child_url, |
| child_frame)); |
| |
| // Top level storage key should not change if third party partitioning is on |
| // or off |
| EXPECT_EQ( |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(initial_url)), |
| main_test_rfh()->GetStorageKey()); |
| |
| if (ThirdPartyStoragePartitioningEnabled()) { |
| // child frame storage key should contain child_origin + top_level_origin if |
| // third party partitioning is on. |
| EXPECT_EQ(blink::StorageKey::Create( |
| url::Origin::Create(child_url), |
| net::SchemefulSite(url::Origin::Create(initial_url)), |
| blink::mojom::AncestorChainBit::kCrossSite), |
| child_frame->GetStorageKey()); |
| } else { |
| // child frame storage key should only be partitioned by child origin if |
| // third party partitioning is off. |
| EXPECT_EQ( |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(child_url)), |
| child_frame->GetStorageKey()); |
| } |
| } |
| |
| namespace { |
| |
| class MockWebContentsDelegate : public WebContentsDelegate { |
| public: |
| MOCK_METHOD(void, CloseContents, (WebContents*)); |
| MOCK_METHOD(void, |
| OnTextCopiedToClipboard, |
| (RenderFrameHost*, std::u16string)); |
| }; |
| |
| } // namespace |
| |
| // Ensure that a close request from the renderer process is ignored if a |
| // navigation causes a different RenderFrameHost to commit first. See |
| // https://crbug.com/1406023. |
| TEST_F(RenderFrameHostImplTest, |
| RendererInitiatedCloseIsCancelledIfPageIsntPrimary) { |
| MockWebContentsDelegate delegate; |
| contents()->SetDelegate(&delegate); |
| |
| RenderFrameHostImpl* rfh = main_test_rfh(); |
| EXPECT_CALL(delegate, CloseContents(contents())).Times(0); |
| |
| // Have the renderer request to close the page. |
| rfh->ClosePage(RenderFrameHostImpl::ClosePageSource::kRenderer); |
| |
| // The close timeout should be running. |
| EXPECT_TRUE(rfh->close_timeout_ && rfh->close_timeout_->IsRunning()); |
| |
| // Simulate the rfh going into the back-forward cache before the close timeout |
| // fires. |
| rfh->lifecycle_state_ = |
| RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache; |
| |
| // Simulate the close timer firing. |
| rfh->ClosePageTimeout(RenderFrameHostImpl::ClosePageSource::kRenderer); |
| |
| // The page should not close since it's no longer the primary page. |
| testing::Mock::VerifyAndClearExpectations(&delegate); |
| } |
| |
| // Ensure that a close request from the browser process cannot be ignored even |
| // if a navigation causes a different RenderFrameHost to commit first. See |
| // https://crbug.com/1406023. |
| TEST_F(RenderFrameHostImplTest, |
| BrowserInitiatedCloseIsNotCancelledIfPageIsntPrimary) { |
| MockWebContentsDelegate delegate; |
| contents()->SetDelegate(&delegate); |
| |
| RenderFrameHostImpl* rfh = main_test_rfh(); |
| EXPECT_CALL(delegate, CloseContents(contents())); |
| |
| // Have the browser request to close the page. |
| rfh->ClosePage(RenderFrameHostImpl::ClosePageSource::kBrowser); |
| |
| // The close timeout should be running. |
| EXPECT_TRUE(rfh->close_timeout_ && rfh->close_timeout_->IsRunning()); |
| |
| // Simulate the rfh going into the back-forward cache before the close timeout |
| // fires. |
| rfh->lifecycle_state_ = |
| RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache; |
| |
| // Simulate the close timer firing. |
| rfh->ClosePageTimeout(RenderFrameHostImpl::ClosePageSource::kBrowser); |
| |
| // The page should close regardless of it not being primary since the browser |
| // requested it. |
| testing::Mock::VerifyAndClearExpectations(&delegate); |
| } |
| |
| // A mock WebContentsObserver for listening to text copy events. |
| class TextCopiedEventObserver : public WebContentsObserver { |
| public: |
| explicit TextCopiedEventObserver(WebContents* web_contents) |
| : WebContentsObserver(web_contents) {} |
| |
| MOCK_METHOD(void, |
| OnTextCopiedToClipboard, |
| (RenderFrameHost*, const std::u16string&), |
| (override)); |
| }; |
| |
| // Test that the WebContentObserver is notified when text is copied to the |
| // clipboard for a RenderFrameHost. |
| TEST_F(RenderFrameHostImplTest, OnTextCopiedToClipboard) { |
| testing::StrictMock<TextCopiedEventObserver> observer(contents()); |
| std::u16string copied_text = u"copied_text"; |
| |
| RenderFrameHostImpl* rfh = main_test_rfh(); |
| EXPECT_CALL(observer, OnTextCopiedToClipboard(rfh, copied_text)); |
| |
| rfh->OnTextCopiedToClipboard(copied_text); |
| } |
| |
| // Test if `LoadedWithCacheControlNoStoreHeader()` behaves |
| // as expected. |
| TEST_F(RenderFrameHostImplTest, LoadedWithCacheControlNoStoreHeader) { |
| TestRenderFrameHost* rfh = main_test_rfh(); |
| // In the default state, `LoadedWithCacheControlNoStoreHeader()` |
| // will return false. |
| ASSERT_FALSE(rfh->LoadedWithCacheControlNoStoreHeader()); |
| // Register the `kMainResourceHasCacheControlNoStore` feature and |
| // `LoadedWithCacheControlNoStoreHeader()` will return true. |
| rfh->OnBackForwardCacheDisablingStickyFeatureUsed( |
| blink::scheduler::WebSchedulerTrackedFeature:: |
| kMainResourceHasCacheControlNoStore); |
| ASSERT_TRUE(rfh->LoadedWithCacheControlNoStoreHeader()); |
| // Simulate a same RFH navigation and the |
| // `LoadedWithCacheControlNoStoreHeader()` should return false because the |
| // registered feature is reset. |
| NavigationSimulator::NavigateAndCommitFromDocument(GURL("http://foo"), rfh); |
| ASSERT_EQ(main_test_rfh(), rfh); |
| ASSERT_FALSE(main_test_rfh()->LoadedWithCacheControlNoStoreHeader()); |
| } |
| |
| class MediaStreamCaptureObserver : public WebContentsObserver { |
| public: |
| explicit MediaStreamCaptureObserver(WebContents* web_contents) |
| : WebContentsObserver(web_contents) {} |
| |
| MOCK_METHOD(void, |
| OnFrameIsCapturingMediaStreamChanged, |
| (RenderFrameHost*, bool), |
| (override)); |
| }; |
| |
| TEST_F(RenderFrameHostImplTest, CapturedMediaStreamAddedRemoved) { |
| testing::StrictMock<MediaStreamCaptureObserver> observer(contents()); |
| |
| TestRenderFrameHost* main_rfh = contents()->GetPrimaryMainFrame(); |
| |
| // Calling OnMediaStreamAdded for the first time will cause a notification. |
| EXPECT_CALL(observer, OnFrameIsCapturingMediaStreamChanged(main_rfh, true)); |
| main_rfh->OnMediaStreamAdded( |
| RenderFrameHostImpl::MediaStreamType::kCapturingMediaStream); |
| |
| // Calling it again will not result in a notification (verified by the |
| // StrictMock). |
| main_rfh->OnMediaStreamAdded( |
| RenderFrameHostImpl::MediaStreamType::kCapturingMediaStream); |
| |
| // Calling OnMediaStreamRemoved to cancel out one of the OnMediaStreamAdded |
| // calls. Overall, the frame is still capturing at least one media stream so |
| // there is no notifications. |
| main_rfh->OnMediaStreamRemoved( |
| RenderFrameHostImpl::MediaStreamType::kCapturingMediaStream); |
| |
| // Cancelling the first OnMediaStreamAdded call. This changes the state of the |
| // frame and thus cause a notification. |
| EXPECT_CALL(observer, OnFrameIsCapturingMediaStreamChanged(main_rfh, false)); |
| main_rfh->OnMediaStreamRemoved( |
| RenderFrameHostImpl::MediaStreamType::kCapturingMediaStream); |
| } |
| |
| class MockPageBroadcastForCanvasNoise : public TestPageBroadcast { |
| public: |
| using TestPageBroadcast::TestPageBroadcast; |
| MOCK_METHOD(void, |
| UpdateCanvasNoiseToken, |
| (const std::optional<uint64_t> canvas_noise_token), |
| (override)); |
| }; |
| |
| class CanvasNoiseTestContentBrowserClient : public ContentBrowserClient { |
| public: |
| CanvasNoiseTestContentBrowserClient() = default; |
| ~CanvasNoiseTestContentBrowserClient() override = default; |
| |
| private: |
| bool ShouldEnableCanvasNoise(content::BrowserContext* browser_context, |
| const GURL& origin) override { |
| return false; |
| } |
| }; |
| |
| TEST_F(RenderFrameHostImplTest, |
| CanvasNoiseToken_PageBroadcastCalledPerMainFrameNavigation) { |
| GURL initial_url = GURL("https://initial.example.test/"); |
| GURL next_url = GURL("https://next.example.test/"); |
| |
| CanvasNoiseTestContentBrowserClient modified_client; |
| ContentBrowserClient* regular_client = |
| SetBrowserClientForTesting(&modified_client); |
| |
| mojo::AssociatedRemote<blink::mojom::PageBroadcast> broadcast_remote; |
| testing::NiceMock<MockPageBroadcastForCanvasNoise> mock_page_broadcast( |
| broadcast_remote.BindNewEndpointAndPassDedicatedReceiver()); |
| contents()->GetRenderViewHost()->BindPageBroadcast(broadcast_remote.Unbind()); |
| |
| // In this test, the kCanvasNoise feature is not enabled therefore |
| // NavigateAndGetCanvasNoiseToken should return std::nullopt. This is fine as |
| // this test only checks to ensure the PageBroadcast gets called when |
| // navigating to different origins and std::nullopt is a valid value for |
| // UpdateCanvasNoiseToken PageBroadcast method. |
| std::optional<uint64_t> token_init = |
| NavigateAndGetCanvasNoiseToken(initial_url, main_rfh()); |
| EXPECT_EQ(token_init, std::nullopt); |
| |
| std::optional<uint64_t> token_next = |
| NavigateAndGetCanvasNoiseToken(next_url, main_rfh()); |
| EXPECT_EQ(token_next, std::nullopt); |
| // Since token_init == token_next == std::nullopt, this is fine. |
| EXPECT_CALL(mock_page_broadcast, UpdateCanvasNoiseToken(token_init)).Times(2); |
| |
| EXPECT_EQ(token_init, token_next); |
| |
| SetBrowserClientForTesting(regular_client); |
| mock_page_broadcast.FlushForTesting(); |
| } |
| |
| } // namespace content |