| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stdint.h> |
| |
| #include <array> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/statistics_recorder.h" |
| #include "base/run_loop.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_mock_time_task_runner.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/test/values_test_util.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/uuid.h" |
| #include "build/build_config.h" |
| #include "cc/test/pixel_test_utils.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "content/browser/browser_url_handler_impl.h" |
| #include "content/browser/child_process_security_policy_impl.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/renderer_host/navigation_state_keep_alive.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/renderer_host/spare_render_process_host_manager_impl.h" |
| #include "content/browser/site_instance_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/content_navigation_policy.h" |
| #include "content/common/features.h" |
| #include "content/common/frame_messages.mojom.h" |
| #include "content/common/navigation_client.mojom-forward.h" |
| #include "content/common/navigation_client.mojom.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_message_filter.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/browser_url_handler.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/download_manager_delegate.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/network_service_util.h" |
| #include "content/public/browser/page_navigator.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/storage_partition_config.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/result_codes.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_content_browser_client.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/content_mock_cert_verifier.h" |
| #include "content/public/test/download_test_observer.h" |
| #include "content/public/test/fenced_frame_test_util.h" |
| #include "content/public/test/hit_test_region_observer.h" |
| #include "content/public/test/navigation_handle_observer.h" |
| #include "content/public/test/no_renderer_crashes_assertion.h" |
| #include "content/public/test/slow_http_response.h" |
| #include "content/public/test/test_devtools_protocol_client.h" |
| #include "content/public/test/test_frame_navigation_observer.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/test_navigation_throttle.h" |
| #include "content/public/test/test_navigation_throttle_inserter.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/public/test/url_loader_monitor.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/shell/browser/shell_download_manager_delegate.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "content/test/did_commit_navigation_interceptor.h" |
| #include "content/test/fake_network_url_loader_factory.h" |
| #include "content/test/render_document_feature.h" |
| #include "content/test/task_runner_deferring_throttle.h" |
| #include "content/test/test_render_frame_host_factory.h" |
| #include "ipc/ipc_security_test_util.h" |
| #include "mojo/public/cpp/bindings/pending_associated_remote.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "net/base/features.h" |
| #include "net/base/load_flags.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/controllable_http_response.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "net/test/url_request/url_request_failed_job.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/web_sandbox_flags.h" |
| #include "services/network/public/mojom/url_loader.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/loader/url_loader_throttle.h" |
| #include "third_party/blink/public/mojom/frame/remote_frame.mojom-test-utils.h" |
| #include "third_party/blink/public/mojom/frame/sudden_termination_disabler_type.mojom-shared.h" |
| #include "third_party/blink/public/mojom/navigation/navigation_params.mojom-shared.h" |
| #include "ui/base/page_transition_types.h" |
| #include "url/gurl.h" |
| #include "url/url_constants.h" |
| #include "url/url_util.h" |
| |
| namespace content { |
| |
| namespace { |
| class InterceptAndCancelDidCommitProvisionalLoad |
| : public DidCommitNavigationInterceptor { |
| public: |
| explicit InterceptAndCancelDidCommitProvisionalLoad(WebContents* web_contents) |
| : DidCommitNavigationInterceptor(web_contents) {} |
| ~InterceptAndCancelDidCommitProvisionalLoad() override {} |
| |
| void Wait(size_t number_of_messages) { |
| while (intercepted_messages_.size() < number_of_messages) { |
| loop_ = std::make_unique<base::RunLoop>(); |
| loop_->Run(); |
| } |
| } |
| |
| const std::vector<raw_ptr<NavigationRequest, VectorExperimental>>& |
| intercepted_navigations() const { |
| return intercepted_navigations_; |
| } |
| |
| const std::vector<mojom::DidCommitProvisionalLoadParamsPtr>& |
| intercepted_messages() const { |
| return intercepted_messages_; |
| } |
| |
| protected: |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) |
| override { |
| intercepted_navigations_.push_back(navigation_request); |
| intercepted_messages_.push_back(std::move(*params)); |
| if (loop_) |
| loop_->Quit(); |
| // Do not send the message to the RenderFrameHostImpl. |
| return false; |
| } |
| |
| // Note: Do not dereference the intercepted_navigations_, they are used as |
| // indices in the RenderFrameHostImpl and not for themselves. |
| std::vector<raw_ptr<NavigationRequest, VectorExperimental>> |
| intercepted_navigations_; |
| std::vector<mojom::DidCommitProvisionalLoadParamsPtr> intercepted_messages_; |
| std::unique_ptr<base::RunLoop> loop_; |
| }; |
| |
| class RenderFrameHostImplForHistoryBackInterceptor |
| : public RenderFrameHostImpl { |
| public: |
| using RenderFrameHostImpl::RenderFrameHostImpl; |
| |
| void GoToEntryAtOffset(int32_t offset, |
| bool has_user_gesture, |
| std::optional<blink::scheduler::TaskAttributionId> |
| soft_navigation_heuristics_task_id) override { |
| if (quit_handler_) |
| std::move(quit_handler_).Run(); |
| } |
| |
| void set_quit_handler(base::OnceClosure handler) { |
| quit_handler_ = std::move(handler); |
| } |
| |
| private: |
| friend class RenderFrameHostFactoryForHistoryBackInterceptor; |
| base::OnceClosure quit_handler_; |
| }; |
| |
| class RenderFrameHostFactoryForHistoryBackInterceptor |
| : public TestRenderFrameHostFactory { |
| protected: |
| std::unique_ptr<RenderFrameHostImpl> CreateRenderFrameHost( |
| SiteInstance* site_instance, |
| scoped_refptr<RenderViewHostImpl> render_view_host, |
| RenderFrameHostDelegate* delegate, |
| FrameTree* frame_tree, |
| FrameTreeNode* frame_tree_node, |
| int32_t routing_id, |
| mojo::PendingAssociatedRemote<mojom::Frame> frame_remote, |
| const blink::LocalFrameToken& frame_token, |
| const blink::DocumentToken& document_token, |
| base::UnguessableToken devtools_frame_token, |
| bool renderer_initiated_creation, |
| RenderFrameHostImpl::LifecycleStateImpl lifecycle_state, |
| scoped_refptr<BrowsingContextState> browsing_context_state) override { |
| return base::WrapUnique(new RenderFrameHostImplForHistoryBackInterceptor( |
| site_instance, std::move(render_view_host), delegate, frame_tree, |
| frame_tree_node, routing_id, std::move(frame_remote), frame_token, |
| document_token, devtools_frame_token, renderer_initiated_creation, |
| lifecycle_state, std::move(browsing_context_state), |
| frame_tree_node->frame_owner_element_type(), frame_tree_node->parent(), |
| frame_tree_node->fenced_frame_status())); |
| } |
| }; |
| |
| // Simulate embedders of content/ keeping track of the current visible URL using |
| // NavigateStateChanged() and GetVisibleURL() API. |
| class EmbedderVisibleUrlTracker : public WebContentsDelegate { |
| public: |
| const GURL& url() { return url_; } |
| |
| // WebContentsDelegate's implementation: |
| void NavigationStateChanged(WebContents* source, |
| InvalidateTypes changed_flags) override { |
| if (!(changed_flags & INVALIDATE_TYPE_URL)) |
| return; |
| url_ = source->GetVisibleURL(); |
| if (on_url_invalidated_) |
| std::move(on_url_invalidated_).Run(); |
| } |
| |
| void WaitUntilUrlInvalidated() { |
| base::RunLoop loop; |
| on_url_invalidated_ = loop.QuitClosure(); |
| loop.Run(); |
| } |
| |
| private: |
| GURL url_; |
| base::OnceClosure on_url_invalidated_; |
| }; |
| |
| // 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_; |
| }; |
| |
| // Helper class. Immediately run a callback when a navigation finishes. |
| class DidFinishNavigationCallback final : public WebContentsObserver { |
| public: |
| explicit DidFinishNavigationCallback( |
| WebContents* web_contents, |
| base::OnceCallback<void(NavigationHandle*)> callback) |
| : WebContentsObserver(web_contents), callback_(std::move(callback)) {} |
| ~DidFinishNavigationCallback() override = default; |
| |
| private: |
| void DidFinishNavigation(NavigationHandle* navigation_handle) override { |
| if (callback_) |
| std::move(callback_).Run(navigation_handle); |
| } |
| base::OnceCallback<void(NavigationHandle*)> callback_; |
| }; |
| |
| const char* non_cacheable_html_response = |
| "HTTP/1.1 200 OK\n" |
| "cache-control: no-cache, no-store, must-revalidate\n" |
| "content-type: text/html; charset=UTF-8\n" |
| "\n" |
| "HTML content."; |
| |
| // Insert a navigation throttle blocking every navigation in its |
| // WillProcessResponse handler. |
| std::unique_ptr<content::TestNavigationThrottleInserter> |
| BlockNavigationWillProcessResponse(WebContentsImpl* web_content) { |
| return std::make_unique<content::TestNavigationThrottleInserter>( |
| web_content, |
| base::BindLambdaForTesting( |
| [&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { |
| auto throttle = std::make_unique<TestNavigationThrottle>(handle); |
| throttle->SetResponse(TestNavigationThrottle::WILL_PROCESS_RESPONSE, |
| TestNavigationThrottle::SYNCHRONOUS, |
| NavigationThrottle::BLOCK_RESPONSE); |
| |
| return throttle; |
| })); |
| } |
| |
| void WaitForHistogramRecordedInChildProcess(std::string name) { |
| base::HistogramTester histogram_tester; |
| |
| while (true) { |
| if (!histogram_tester.GetAllSamples(name).empty()) { |
| return; |
| } |
| |
| // Retry fetching the histogram since it's not populated yet. |
| content::FetchHistogramsFromChildProcesses(); |
| base::StatisticsRecorder::ImportProvidedHistogramsSync(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| |
| } // namespace |
| |
| // Test about navigation. |
| // If you don't need a custom embedded test server, please use the next class |
| // below (NavigationBrowserTest), it will automatically start the |
| // default server. |
| class NavigationBaseBrowserTest : public ContentBrowserTest { |
| public: |
| NavigationBaseBrowserTest() {} |
| |
| void PreRunTestOnMainThread() override { |
| ContentBrowserTest::PreRunTestOnMainThread(); |
| test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); |
| } |
| |
| const ukm::TestAutoSetUkmRecorder& test_ukm_recorder() const { |
| return *test_ukm_recorder_; |
| } |
| |
| protected: |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| } |
| |
| WebContentsImpl* web_contents() const { |
| return static_cast<WebContentsImpl*>(shell()->web_contents()); |
| } |
| |
| FrameTreeNode* main_frame() { |
| return web_contents()->GetPrimaryFrameTree().root(); |
| } |
| |
| RenderFrameHostImpl* current_frame_host() { |
| return main_frame()->current_frame_host(); |
| } |
| |
| private: |
| std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_; |
| }; |
| |
| class NavigationBrowserTest : public NavigationBaseBrowserTest { |
| protected: |
| void SetUpOnMainThread() override { |
| NavigationBaseBrowserTest::SetUpOnMainThread(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| }; |
| |
| class NavigationGoToEntryAtOffsetBrowserTest : public NavigationBrowserTest { |
| public: |
| void SetQuitHandlerForGoToEntryAtOffset(base::OnceClosure handler) { |
| RenderFrameHostImplForHistoryBackInterceptor* render_frame_host = |
| static_cast<RenderFrameHostImplForHistoryBackInterceptor*>( |
| current_frame_host()); |
| render_frame_host->set_quit_handler(std::move(handler)); |
| } |
| |
| private: |
| RenderFrameHostFactoryForHistoryBackInterceptor render_frame_host_factory_; |
| }; |
| |
| class NetworkIsolationNavigationBrowserTest : public ContentBrowserTest { |
| public: |
| NetworkIsolationNavigationBrowserTest() = default; |
| |
| protected: |
| void SetUpOnMainThread() override { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| ContentBrowserTest::SetUpOnMainThread(); |
| } |
| }; |
| |
| class NavigationBrowserTestReferrerPolicy |
| : public NavigationBrowserTest, |
| public ::testing::WithParamInterface<network::mojom::ReferrerPolicy> { |
| protected: |
| network::mojom::ReferrerPolicy GetReferrerPolicy() const { |
| return GetParam(); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| NavigationBrowserTestReferrerPolicy, |
| ::testing::Values( |
| network::mojom::ReferrerPolicy::kAlways, |
| network::mojom::ReferrerPolicy::kDefault, |
| network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade, |
| network::mojom::ReferrerPolicy::kNever, |
| network::mojom::ReferrerPolicy::kOrigin, |
| network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin, |
| network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin, |
| network::mojom::ReferrerPolicy::kSameOrigin, |
| network::mojom::ReferrerPolicy::kStrictOrigin)); |
| |
| // Ensure that browser initiated basic navigations work. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BrowserInitiatedNavigations) { |
| // Perform a navigation with no live renderer. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_FALSE(observer.last_initiator_origin().has_value()); |
| EXPECT_FALSE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, |
| observer.last_initiator_process_id()); |
| } |
| |
| RenderFrameHost* initial_rfh = current_frame_host(); |
| |
| // Perform a same site navigation. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL url(embedded_test_server()->GetURL("/title2.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_FALSE(observer.last_initiator_origin().has_value()); |
| EXPECT_FALSE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, |
| observer.last_initiator_process_id()); |
| } |
| |
| RenderFrameHost* second_rfh = current_frame_host(); |
| |
| if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) { |
| // If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument |
| // is enabled, the navigation will result in a new RFH. |
| EXPECT_NE(initial_rfh, second_rfh); |
| } else { |
| EXPECT_EQ(initial_rfh, second_rfh); |
| } |
| |
| // Perform a cross-site navigation. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL url = embedded_test_server()->GetURL("foo.com", "/title3.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_FALSE(observer.last_initiator_origin().has_value()); |
| EXPECT_FALSE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, |
| observer.last_initiator_process_id()); |
| } |
| |
| // The RenderFrameHost should have changed. |
| EXPECT_NE(second_rfh, current_frame_host()); |
| |
| // Check the UKM for navigation responses received. |
| EXPECT_EQ(3u, test_ukm_recorder() |
| .GetEntriesByName("Navigation.ReceivedResponse") |
| .size()); |
| } |
| |
| // Ensure that renderer initiated same-site navigations work. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| RendererInitiatedSameSiteNavigation) { |
| // Perform a navigation with no live renderer. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL url(embedded_test_server()->GetURL("/simple_links.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_FALSE(observer.last_initiator_origin().has_value()); |
| EXPECT_FALSE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, |
| observer.last_initiator_process_id()); |
| } |
| |
| RenderFrameHost* initial_rfh = current_frame_host(); |
| auto initial_rfh_global_token = initial_rfh->GetGlobalFrameToken(); |
| |
| // Simulate clicking on a same-site link. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL url(embedded_test_server()->GetURL("/title2.html")); |
| EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteLink();")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| |
| EXPECT_EQ(current_frame_host()->GetLastCommittedOrigin(), |
| observer.last_initiator_origin()); |
| |
| EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); |
| if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) { |
| // If same-site ProactivelySwapBrowsingInstance or main-frame |
| // RenderDocument is enabled, the navigation will result in a new RFH, so |
| // we need to compare with |initial_rfh|. |
| EXPECT_NE(current_frame_host(), initial_rfh); |
| EXPECT_EQ(initial_rfh_global_token.frame_token, |
| observer.last_initiator_frame_token().value()); |
| EXPECT_EQ(initial_rfh_global_token.child_id, |
| observer.last_initiator_process_id()); |
| } else { |
| EXPECT_EQ(current_frame_host(), initial_rfh); |
| EXPECT_EQ(current_frame_host()->GetFrameToken(), |
| observer.last_initiator_frame_token().value()); |
| EXPECT_EQ(current_frame_host()->GetProcess()->GetDeprecatedID(), |
| observer.last_initiator_process_id()); |
| } |
| } |
| |
| RenderFrameHost* second_rfh = current_frame_host(); |
| |
| if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) { |
| // If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument |
| // is enabled, the navigation will result in a new RFH. |
| EXPECT_NE(initial_rfh, second_rfh); |
| } else { |
| EXPECT_EQ(initial_rfh, second_rfh); |
| } |
| } |
| |
| // Ensure that renderer initiated cross-site navigations work. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| RendererInitiatedCrossSiteNavigation) { |
| // Perform a navigation with no live renderer. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL url(embedded_test_server()->GetURL("/simple_links.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| } |
| |
| RenderFrameHost* initial_rfh = current_frame_host(); |
| url::Origin initial_origin = initial_rfh->GetLastCommittedOrigin(); |
| auto initial_rfh_global_token = initial_rfh->GetGlobalFrameToken(); |
| |
| // Simulate clicking on a cross-site link. |
| { |
| TestNavigationObserver observer(web_contents()); |
| const char kReplacePortNumber[] = "setPortNumber(%d);"; |
| uint16_t port_number = embedded_test_server()->port(); |
| GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html"); |
| EXPECT_EQ(true, EvalJs(shell(), base::StringPrintf(kReplacePortNumber, |
| port_number))); |
| EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteLink();")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_EQ(initial_origin, observer.last_initiator_origin().value()); |
| EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(initial_rfh_global_token.frame_token, |
| observer.last_initiator_frame_token().value()); |
| EXPECT_EQ(initial_rfh_global_token.child_id, |
| observer.last_initiator_process_id()); |
| } |
| |
| // The RenderFrameHost should have changed unless full site isolation and |
| // proactive BrowsingInstance swaps are both disabled. |
| if (!AreAllSitesIsolatedForTesting() && |
| !CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) { |
| EXPECT_EQ(initial_rfh, current_frame_host()); |
| } else { |
| EXPECT_NE(initial_rfh, current_frame_host()); |
| } |
| } |
| |
| // Ensure navigation failures are handled. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FailedNavigation) { |
| // Perform a navigation with no live renderer. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| // Check the UKM for navigation responses received. |
| EXPECT_EQ(1u, test_ukm_recorder() |
| .GetEntriesByName("Navigation.ReceivedResponse") |
| .size()); |
| } |
| |
| // Now navigate to an unreachable url. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL error_url(embedded_test_server()->GetURL("/close-socket")); |
| GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&net::URLRequestFailedJob::AddUrlHandler)); |
| EXPECT_FALSE(NavigateToURL(shell(), error_url)); |
| EXPECT_EQ(error_url, observer.last_navigation_url()); |
| NavigationEntry* entry = |
| web_contents()->GetController().GetLastCommittedEntry(); |
| EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType()); |
| // No response on an unreachable URL, so the ReceivedResponse event should |
| // not have increased. |
| EXPECT_EQ(1u, test_ukm_recorder() |
| .GetEntriesByName("Navigation.ReceivedResponse") |
| .size()); |
| } |
| } |
| |
| // Ensure that browser initiated navigations to view-source URLs works. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| ViewSourceNavigation_BrowserInitiated) { |
| TestNavigationObserver observer(web_contents()); |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| GURL view_source_url(content::kViewSourceScheme + std::string(":") + |
| url.spec()); |
| EXPECT_TRUE(NavigateToURL(shell(), view_source_url)); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| } |
| |
| // Ensure that content initiated navigations to view-sources URLs are blocked. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| ViewSourceNavigation_RendererInitiated) { |
| TestNavigationObserver observer(web_contents()); |
| GURL kUrl(embedded_test_server()->GetURL("/simple_links.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), kUrl)); |
| EXPECT_EQ(kUrl, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "Not allowed to load local resource: view-source:about:blank"); |
| |
| EXPECT_EQ(true, EvalJs(web_contents(), "clickViewSourceLink();")); |
| ASSERT_TRUE(console_observer.Wait()); |
| // Original page shouldn't navigate away. |
| EXPECT_EQ(kUrl, web_contents()->GetLastCommittedURL()); |
| EXPECT_FALSE(shell() |
| ->web_contents() |
| ->GetController() |
| .GetLastCommittedEntry() |
| ->IsViewSourceMode()); |
| } |
| |
| // Ensure that content initiated navigations to googlechrome: URLs are blocked. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| GoogleChromeNavigation_RendererInitiated) { |
| TestNavigationObserver observer(web_contents()); |
| GURL kUrl(embedded_test_server()->GetURL("/simple_links.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), kUrl)); |
| EXPECT_EQ(kUrl, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "Not allowed to load local resource: googlechrome://"); |
| |
| EXPECT_EQ(true, EvalJs(web_contents(), "clickGoogleChromeLink();")); |
| ASSERT_TRUE(console_observer.Wait()); |
| // Original page shouldn't navigate away. |
| EXPECT_EQ(kUrl, web_contents()->GetLastCommittedURL()); |
| } |
| |
| // Ensure that closing a page by running its beforeunload handler doesn't hang |
| // if there's an ongoing navigation. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, UnloadDuringNavigation) { |
| WebContentsDestroyedWatcher close_observer(web_contents()); |
| GURL url("chrome://resources/css/tabs.css"); |
| NavigationHandleObserver handle_observer(web_contents(), url); |
| shell()->LoadURL(url); |
| web_contents()->DispatchBeforeUnload(false /* auto_cancel */); |
| close_observer.Wait(); |
| EXPECT_EQ(net::ERR_ABORTED, handle_observer.net_error_code()); |
| } |
| |
| // Ensure that the referrer of a navigation is properly sanitized. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SanitizeReferrer) { |
| const GURL kInsecureUrl(embedded_test_server()->GetURL("/title1.html")); |
| const Referrer kSecureReferrer( |
| GURL("https://secure-url.com"), |
| network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade); |
| |
| // Navigate to an insecure url with a secure referrer with a policy of no |
| // referrer on downgrades. The referrer url should be rewritten right away. |
| NavigationController::LoadURLParams load_params(kInsecureUrl); |
| load_params.referrer = kSecureReferrer; |
| TestNavigationManager manager(web_contents(), kInsecureUrl); |
| web_contents()->GetController().LoadURLWithParams(load_params); |
| EXPECT_TRUE(manager.WaitForRequestStart()); |
| |
| // The referrer should have been sanitized. |
| ASSERT_TRUE(main_frame()->navigation_request()); |
| EXPECT_EQ(GURL(), main_frame()->navigation_request()->GetReferrer().url); |
| |
| // The navigation should commit without being blocked. |
| EXPECT_TRUE(manager.WaitForResponse()); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| EXPECT_EQ(kInsecureUrl, web_contents()->GetLastCommittedURL()); |
| } |
| |
| // Ensure the correctness of a navigation request's referrer. This is a |
| // regression test for https://crbug.com/1004083. |
| IN_PROC_BROWSER_TEST_P(NavigationBrowserTestReferrerPolicy, ReferrerPolicy) { |
| const GURL kDestination(embedded_test_server()->GetURL("/title1.html")); |
| const GURL kReferrerURL(embedded_test_server()->GetURL("/referrer-page")); |
| const url::Origin kReferrerOrigin = url::Origin::Create(kReferrerURL); |
| |
| // It is possible that the referrer URL does not match what the policy |
| // demands (e.g., non-empty URL and kNever policy), so we'll test that the |
| // correct referrer is generated, and that the navigation succeeds. |
| const Referrer referrer(kReferrerURL, GetReferrerPolicy()); |
| |
| // Navigate to a resource whose destination URL is same-origin with the |
| // navigation's referrer. The final referrer should be generated correctly. |
| NavigationController::LoadURLParams load_params(kDestination); |
| load_params.referrer = referrer; |
| TestNavigationManager manager(web_contents(), kDestination); |
| web_contents()->GetController().LoadURLWithParams(load_params); |
| EXPECT_TRUE(manager.WaitForRequestStart()); |
| |
| // The referrer should have been sanitized. |
| ASSERT_TRUE(main_frame()->navigation_request()); |
| switch (GetReferrerPolicy()) { |
| case network::mojom::ReferrerPolicy::kAlways: |
| case network::mojom::ReferrerPolicy::kDefault: |
| case network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade: |
| case network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin: |
| case network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin: |
| case network::mojom::ReferrerPolicy::kSameOrigin: |
| EXPECT_EQ(kReferrerURL, |
| main_frame()->navigation_request()->GetReferrer().url); |
| break; |
| case network::mojom::ReferrerPolicy::kNever: |
| EXPECT_EQ(GURL(), main_frame()->navigation_request()->GetReferrer().url); |
| break; |
| case network::mojom::ReferrerPolicy::kOrigin: |
| case network::mojom::ReferrerPolicy::kStrictOrigin: |
| EXPECT_EQ(kReferrerOrigin.GetURL(), |
| main_frame()->navigation_request()->GetReferrer().url); |
| break; |
| } |
| |
| // The navigation should commit without being blocked. |
| EXPECT_TRUE(manager.WaitForResponse()); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| EXPECT_EQ(kDestination, web_contents()->GetLastCommittedURL()); |
| } |
| |
| // Test to verify that an exploited renderer process trying to upload a file |
| // it hasn't been explicitly granted permissions to is correctly terminated. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, PostUploadIllegalFilePath) { |
| GURL form_url( |
| embedded_test_server()->GetURL("/form_that_posts_to_echoall.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), form_url)); |
| |
| // Prepare a file for the upload form. |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::ScopedTempDir temp_dir; |
| base::FilePath file_path; |
| std::string file_content("test-file-content"); |
| ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); |
| ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path)); |
| ASSERT_TRUE(base::WriteFile(file_path, file_content)); |
| |
| base::RunLoop run_loop; |
| // Fill out the form to refer to the test file. |
| std::unique_ptr<FileChooserDelegate> delegate( |
| new FileChooserDelegate(file_path, run_loop.QuitClosure())); |
| web_contents()->SetDelegate(delegate.get()); |
| EXPECT_TRUE( |
| ExecJs(web_contents(), "document.getElementById('file').click();")); |
| run_loop.Run(); |
| |
| // Ensure that the process is allowed to access to the chosen file and |
| // does not have access to the other file name. |
| EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile( |
| current_frame_host()->GetProcess()->GetDeprecatedID(), file_path)); |
| |
| // Revoke the access to the file and submit the form. The renderer process |
| // should be terminated. |
| RenderProcessHostBadIpcMessageWaiter process_kill_waiter( |
| current_frame_host()->GetProcess()); |
| ChildProcessSecurityPolicyImpl* security_policy = |
| ChildProcessSecurityPolicyImpl::GetInstance(); |
| security_policy->RevokeAllPermissionsForFile( |
| current_frame_host()->GetProcess()->GetDeprecatedID(), file_path); |
| |
| // Use EvalJs and respond back to the browser process before doing the actual |
| // submission. This will ensure that the process termination is guaranteed to |
| // arrive after the response from the executed JavaScript. |
| EXPECT_EQ( |
| true, |
| EvalJs( |
| shell(), |
| "setTimeout(() => document.getElementById('file-form').submit(), 0);" |
| "true;")); |
| EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, process_kill_waiter.Wait()); |
| } |
| |
| // Test case to verify that redirects to data: URLs are properly disallowed, |
| // even when invoked through a reload. |
| // See https://crbug.com/723796. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| VerifyBlockedErrorPageURL_Reload) { |
| NavigationControllerImpl& controller = web_contents()->GetController(); |
| |
| GURL start_url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); |
| |
| // Navigate to an URL, which redirects to a data: URL, since it is an |
| // unsafe redirect and will result in a blocked navigation and error page. |
| GURL redirect_to_blank_url( |
| embedded_test_server()->GetURL("/server-redirect?data:text/html,Hello!")); |
| EXPECT_FALSE(NavigateToURL(shell(), redirect_to_blank_url)); |
| EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); |
| EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType()); |
| |
| TestNavigationObserver reload_observer(web_contents()); |
| EXPECT_TRUE(ExecJs(shell(), "location.reload()")); |
| reload_observer.Wait(); |
| |
| // The expectation is that the blocked URL is present in the NavigationEntry, |
| // and shows up in both GetURL and GetVirtualURL. |
| EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); |
| EXPECT_FALSE( |
| controller.GetLastCommittedEntry()->GetURL().SchemeIs(url::kDataScheme)); |
| EXPECT_EQ(redirect_to_blank_url, |
| controller.GetLastCommittedEntry()->GetURL()); |
| EXPECT_EQ(redirect_to_blank_url, |
| controller.GetLastCommittedEntry()->GetVirtualURL()); |
| } |
| |
| // TODO(crbug.com/40924471): Test is flaky on Android, Linux. |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX) |
| #define MAYBE_BackFollowedByReload DISABLED_BackFollowedByReload |
| #else |
| #define MAYBE_BackFollowedByReload BackFollowedByReload |
| #endif |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, MAYBE_BackFollowedByReload) { |
| // First, make two history entries. |
| GURL url1(embedded_test_server()->GetURL("/title1.html")); |
| GURL url2(embedded_test_server()->GetURL("/title2.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(NavigateToURL(shell(), url2)); |
| |
| // Then execute a back navigation in Javascript followed by a reload. |
| TestNavigationObserver navigation_observer(web_contents()); |
| EXPECT_TRUE(ExecJs(web_contents(), "history.back(); location.reload();")); |
| navigation_observer.Wait(); |
| |
| // The reload should have cancelled the back navigation, and the last |
| // committed URL should still be the second URL. |
| EXPECT_EQ(url2, web_contents()->GetLastCommittedURL()); |
| } |
| |
| // Test that a navigation response can be entirely fetched, even after the |
| // NavigationURLLoader has been deleted. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, |
| FetchResponseAfterNavigationURLLoaderDeleted) { |
| net::test_server::ControllableHttpResponse response(embedded_test_server(), |
| "/main_document"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Load a new document. |
| GURL url(embedded_test_server()->GetURL("/main_document")); |
| TestNavigationManager navigation_manager(web_contents(), url); |
| shell()->LoadURL(url); |
| |
| // The navigation starts. |
| EXPECT_TRUE(navigation_manager.WaitForRequestStart()); |
| navigation_manager.ResumeNavigation(); |
| |
| // A NavigationRequest exists at this point. |
| EXPECT_TRUE(main_frame()->navigation_request()); |
| |
| // The response's headers are received. |
| response.WaitForRequest(); |
| response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "\r\n" |
| "..."); |
| EXPECT_TRUE(navigation_manager.WaitForResponse()); |
| navigation_manager.ResumeNavigation(); |
| |
| // The renderer commits the navigation and the browser deletes its |
| // NavigationRequest. |
| ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); |
| EXPECT_FALSE(main_frame()->navigation_request()); |
| |
| // The NavigationURLLoader has been deleted by now. Check that the renderer |
| // can still receive more bytes. |
| DOMMessageQueue dom_message_queue(web_contents()); |
| response.Send( |
| "<script>window.domAutomationController.send('done');</script>"); |
| std::string done; |
| EXPECT_TRUE(dom_message_queue.WaitForMessage(&done)); |
| EXPECT_EQ("\"done\"", done); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest, |
| BrowserNavigationNetworkIsolationKey) { |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| url::Origin origin = url::Origin::Create(url); |
| URLLoaderMonitor monitor({url}); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| monitor.WaitForUrls(); |
| |
| std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); |
| ASSERT_TRUE(request->trusted_params); |
| EXPECT_TRUE(net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, origin, origin, |
| net::SiteForCookies::FromOrigin(origin)) |
| .IsEqualForTesting(request->trusted_params->isolation_info)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest, |
| RenderNavigationIsolationInfo) { |
| GURL url(embedded_test_server()->GetURL("/title2.html")); |
| url::Origin origin = url::Origin::Create(url); |
| EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank"))); |
| URLLoaderMonitor monitor({url}); |
| EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url)); |
| monitor.WaitForUrls(); |
| |
| std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); |
| ASSERT_TRUE(request->trusted_params); |
| EXPECT_TRUE(net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, origin, origin, |
| net::SiteForCookies::FromOrigin(origin)) |
| .IsEqualForTesting(request->trusted_params->isolation_info)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest, |
| SubframeIsolationInfo) { |
| GURL url(embedded_test_server()->GetURL("/page_with_iframe.html")); |
| GURL iframe_document = embedded_test_server()->GetURL("/title1.html"); |
| url::Origin origin = url::Origin::Create(url); |
| url::Origin iframe_origin = url::Origin::Create(iframe_document); |
| URLLoaderMonitor monitor({iframe_document}); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| monitor.WaitForUrls(); |
| |
| std::optional<network::ResourceRequest> main_frame_request = |
| monitor.GetRequestInfo(url); |
| ASSERT_TRUE(main_frame_request.has_value()); |
| ASSERT_TRUE(main_frame_request->trusted_params); |
| EXPECT_TRUE(net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, origin, origin, |
| net::SiteForCookies::FromOrigin(origin)) |
| .IsEqualForTesting( |
| main_frame_request->trusted_params->isolation_info)); |
| |
| std::optional<network::ResourceRequest> iframe_request = |
| monitor.GetRequestInfo(iframe_document); |
| ASSERT_TRUE(iframe_request->trusted_params); |
| EXPECT_TRUE( |
| net::IsolationInfo::Create(net::IsolationInfo::RequestType::kSubFrame, |
| origin, iframe_origin, |
| net::SiteForCookies::FromOrigin(origin)) |
| .IsEqualForTesting(iframe_request->trusted_params->isolation_info)); |
| } |
| |
| // Tests that the initiator is not set for a browser initiated top frame |
| // navigation. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BrowserNavigationInitiator) { |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| |
| URLLoaderMonitor monitor; |
| |
| // Perform the actual navigation. |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); |
| ASSERT_TRUE(request.has_value()); |
| ASSERT_FALSE(request->request_initiator.has_value()); |
| } |
| |
| // Test that the initiator is set to the starting page when a renderer initiated |
| // navigation goes from the starting page to another page. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererNavigationInitiator) { |
| GURL starting_page(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| url::Origin starting_page_origin; |
| starting_page_origin = starting_page_origin.Create(starting_page); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), starting_page)); |
| |
| GURL url(embedded_test_server()->GetURL("/title2.html")); |
| |
| URLLoaderMonitor monitor; |
| |
| // Perform the actual navigation. |
| EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url)); |
| |
| std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); |
| ASSERT_TRUE(request.has_value()); |
| EXPECT_EQ(starting_page_origin, request->request_initiator); |
| } |
| |
| // Test that the initiator is set to the starting page when a sub frame is |
| // navigated by Javascript from some starting page to another page. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SubFrameJsNavigationInitiator) { |
| GURL starting_page(embedded_test_server()->GetURL("/frame_tree/top.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), starting_page)); |
| |
| // The main_frame() and subframe should each have a live RenderFrame. |
| EXPECT_TRUE(main_frame() |
| ->current_frame_host() |
| ->render_view_host() |
| ->IsRenderViewLive()); |
| EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive()); |
| EXPECT_TRUE( |
| main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive()); |
| |
| GURL url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| |
| URLLoaderMonitor monitor({url}); |
| std::string script = "location.href='" + url.spec() + "'"; |
| |
| // Perform the actual navigation. |
| EXPECT_TRUE(ExecJs(main_frame()->child_at(0)->current_frame_host(), script)); |
| |
| EXPECT_TRUE(main_frame() |
| ->current_frame_host() |
| ->render_view_host() |
| ->IsRenderViewLive()); |
| EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive()); |
| EXPECT_TRUE( |
| main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive()); |
| |
| url::Origin starting_page_origin; |
| starting_page_origin = starting_page_origin.Create(starting_page); |
| |
| monitor.WaitForUrls(); |
| std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); |
| EXPECT_EQ(starting_page_origin, request->request_initiator); |
| } |
| |
| // Test that the initiator is set to the starting page when a sub frame, |
| // selected by Id, is navigated by Javascript from some starting page to another |
| // page. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SubframeNavigationByTopFrameInitiator) { |
| // Go to a page on a.com with an iframe that is on b.com |
| GURL starting_page(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), starting_page)); |
| |
| // The main_frame and subframe should each have a live RenderFrame. |
| EXPECT_TRUE(main_frame() |
| ->current_frame_host() |
| ->render_view_host() |
| ->IsRenderViewLive()); |
| EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive()); |
| EXPECT_TRUE( |
| main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive()); |
| |
| GURL url(embedded_test_server()->GetURL("c.com", "/title1.html")); |
| |
| URLLoaderMonitor monitor; |
| |
| // Perform the actual navigation. |
| NavigateIframeToURL(web_contents(), "child-0", url); |
| |
| EXPECT_TRUE(main_frame() |
| ->current_frame_host() |
| ->render_view_host() |
| ->IsRenderViewLive()); |
| EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive()); |
| EXPECT_TRUE( |
| main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive()); |
| |
| url::Origin starting_page_origin; |
| starting_page_origin = starting_page_origin.Create(starting_page); |
| |
| std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); |
| ASSERT_TRUE(request.has_value()); |
| EXPECT_EQ(starting_page_origin, request->request_initiator); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| RendererInitiatedCrossSiteNewWindowInitator) { |
| GURL url(embedded_test_server()->GetURL("/simple_links.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| auto initiator_global_token = current_frame_host()->GetGlobalFrameToken(); |
| |
| // Simulate clicking on a cross-site link. |
| { |
| const char kReplacePortNumber[] = "setPortNumber(%d);"; |
| uint16_t port_number = embedded_test_server()->port(); |
| url = embedded_test_server()->GetURL("foo.com", "/title2.html"); |
| EXPECT_TRUE( |
| ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number))); |
| |
| TestNavigationObserver observer(url); |
| observer.StartWatchingNewWebContents(); |
| EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteNewWindowLink();")); |
| |
| observer.Wait(); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(initiator_global_token.frame_token, |
| observer.last_initiator_frame_token().value()); |
| EXPECT_EQ(initiator_global_token.child_id, |
| observer.last_initiator_process_id()); |
| } |
| } |
| |
| // Ensure that renderer initiated navigations which have the opener suppressed |
| // work. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| RendererInitiatedNewWindowNoOpenerNavigation) { |
| GURL url(embedded_test_server()->GetURL("/simple_links.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| RenderFrameHost* initial_rfh = current_frame_host(); |
| url::Origin initial_origin = initial_rfh->GetLastCommittedOrigin(); |
| auto initiator_global_token = initial_rfh->GetGlobalFrameToken(); |
| |
| // Simulate clicking on a cross-site link which has rel="noopener". |
| { |
| const char kReplacePortNumber[] = "setPortNumber(%d);"; |
| uint16_t port_number = embedded_test_server()->port(); |
| url = embedded_test_server()->GetURL("foo.com", "/title2.html"); |
| EXPECT_TRUE( |
| ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number))); |
| |
| TestNavigationObserver observer(url); |
| observer.StartWatchingNewWebContents(); |
| EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteNewWindowNoOpenerLink();")); |
| |
| observer.Wait(); |
| |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_EQ(initial_origin, observer.last_initiator_origin().value()); |
| EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(initiator_global_token.frame_token, |
| observer.last_initiator_frame_token().value()); |
| EXPECT_EQ(initiator_global_token.child_id, |
| observer.last_initiator_process_id()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| RendererInitiatedWithSubframeInitator) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a())")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| GURL subframe_url = |
| embedded_test_server()->GetURL("a.com", "/simple_links.html"); |
| EXPECT_TRUE( |
| NavigateToURLFromRenderer(main_frame()->child_at(0), subframe_url)); |
| |
| RenderFrameHostImpl* subframe_rfh = |
| current_frame_host()->child_at(0)->current_frame_host(); |
| auto initiator_global_token = subframe_rfh->GetGlobalFrameToken(); |
| |
| // Simulate clicking on a cross-site link. |
| { |
| const char kReplacePortNumber[] = "setPortNumber(%d);"; |
| uint16_t port_number = embedded_test_server()->port(); |
| url = embedded_test_server()->GetURL("foo.com", "/title2.html"); |
| EXPECT_TRUE(ExecJs(subframe_rfh, |
| base::StringPrintf(kReplacePortNumber, port_number))); |
| |
| TestNavigationObserver observer(url); |
| observer.StartWatchingNewWebContents(); |
| EXPECT_EQ(true, EvalJs(subframe_rfh, "clickCrossSiteNewWindowLink();")); |
| |
| observer.Wait(); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(initiator_global_token.frame_token, |
| observer.last_initiator_frame_token().value()); |
| EXPECT_EQ(initiator_global_token.child_id, |
| observer.last_initiator_process_id()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| InitiatorFrameStateConsistentAtDidStartNavigation) { |
| GURL form_page_url(embedded_test_server()->GetURL( |
| "a.com", "/form_that_posts_to_echoall.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), form_page_url)); |
| |
| // Give the form an action that will navigate to a slow page. |
| GURL form_action_url(embedded_test_server()->GetURL("b.com", "/slow?100")); |
| EXPECT_TRUE( |
| ExecJs(shell(), JsReplace("document.getElementById('form').action = $1", |
| form_action_url))); |
| |
| // Open a new window that can be targeted by the form submission. |
| WebContents* form_contents = web_contents(); |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE(ExecJs(shell(), "window.open('about:blank', 'target_frame');")); |
| WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); |
| |
| EXPECT_TRUE( |
| ExecJs(form_contents, |
| "document.getElementById('form').target = 'target_frame';")); |
| |
| TestNavigationManager popup_manager(popup_contents, form_action_url); |
| TestNavigationManager form_manager( |
| form_contents, embedded_test_server()->GetURL("a.com", "/title2.html")); |
| |
| // Submit the form and navigate the form's page. |
| EXPECT_TRUE(ExecJs(form_contents, "window.location.href = 'title2.html'")); |
| EXPECT_TRUE( |
| ExecJs(form_contents, "document.getElementById('form').submit();")); |
| |
| // The form page's navigation should start prior to the form navigation. |
| EXPECT_TRUE(form_manager.WaitForRequestStart()); |
| EXPECT_FALSE(popup_manager.GetNavigationHandle()); |
| |
| // When the navigation starts for the popup, ensure that the original page has |
| // not finished navigating. If this was not the case, we could not make any |
| // statements on the validity of initiator state during a navigation. |
| // Navigation handles are only available prior to DidFinishNavigation(). |
| EXPECT_TRUE(popup_manager.WaitForRequestStart()); |
| EXPECT_TRUE(form_manager.GetNavigationHandle()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| RendererInitiatedMiddleClickInitator) { |
| GURL url(embedded_test_server()->GetURL("/simple_links.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| auto initiator_global_token = current_frame_host()->GetGlobalFrameToken(); |
| |
| // Simulate middle-clicking on a cross-site link. |
| { |
| const char kReplacePortNumber[] = "setPortNumber(%d);"; |
| uint16_t port_number = embedded_test_server()->port(); |
| url = embedded_test_server()->GetURL("foo.com", "/title2.html"); |
| EXPECT_TRUE( |
| ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number))); |
| |
| TestNavigationObserver observer(url); |
| observer.StartWatchingNewWebContents(); |
| EXPECT_EQ(true, EvalJs(shell(), R"( |
| target = document.getElementById('cross_site_link'); |
| var evt = new MouseEvent("click", {"button": 1 /* middle_button */}); |
| target.dispatchEvent(evt);)")); |
| |
| observer.Wait(); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); |
| EXPECT_EQ(initiator_global_token.frame_token, |
| observer.last_initiator_frame_token().value()); |
| EXPECT_EQ(initiator_global_token.child_id, |
| observer.last_initiator_process_id()); |
| } |
| } |
| |
| // Data URLs can have a reference fragment like any other URLs. This test makes |
| // sure it is taken into account. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, DataURLWithReferenceFragment) { |
| GURL url("data:text/html,body#foo"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| EXPECT_EQ("body", EvalJs(shell(), "document.body.textContent;")); |
| |
| EXPECT_EQ("#foo", EvalJs(shell(), "location.hash;")); |
| } |
| |
| // Regression test for https://crbug.com/796561. |
| // 1) Start on a document with history.length == 1. |
| // 2) Create an iframe and call history.pushState at the same time. |
| // 3) history.back() must work. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| IframeAndPushStateSimultaneously) { |
| GURL main_url = embedded_test_server()->GetURL("/simple_page.html"); |
| GURL iframe_url = embedded_test_server()->GetURL("/hello.html"); |
| |
| // 1) Start on a new document such that history.length == 1. |
| { |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| EXPECT_EQ(1, EvalJs(shell(), "history.length")); |
| } |
| |
| // 2) Create an iframe and call history.pushState at the same time. |
| { |
| TestNavigationManager iframe_navigation(web_contents(), iframe_url); |
| ExecuteScriptAsync(shell(), |
| "let iframe = document.createElement('iframe');" |
| "iframe.src = '/hello.html';" |
| "document.body.appendChild(iframe);"); |
| EXPECT_TRUE(iframe_navigation.WaitForRequestStart()); |
| |
| // The iframe navigation is paused. In the meantime, a pushState navigation |
| // begins and ends. |
| TestNavigationManager push_state_navigation(web_contents(), main_url); |
| ExecuteScriptAsync(shell(), "window.history.pushState({}, null);"); |
| ASSERT_TRUE(push_state_navigation.WaitForNavigationFinished()); |
| |
| // The iframe navigation is resumed. |
| ASSERT_TRUE(iframe_navigation.WaitForNavigationFinished()); |
| } |
| |
| // 3) history.back() must work. |
| { |
| TestNavigationObserver navigation_observer(web_contents()); |
| EXPECT_TRUE(ExecJs(web_contents(), "history.back();")); |
| navigation_observer.Wait(); |
| } |
| } |
| |
| // Regression test for https://crbug.com/260144 |
| // Back/Forward navigation in an iframe must not stop ongoing XHR. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, |
| IframeNavigationsDoNotStopXHR) { |
| // A response for the XHR request. It will be delayed until the end of all the |
| // navigations. |
| net::test_server::ControllableHttpResponse xhr_response( |
| embedded_test_server(), "/xhr"); |
| EXPECT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| DOMMessageQueue dom_message_queue(web_contents()); |
| std::string message; |
| |
| // 1) Send an XHR. |
| ExecuteScriptAsync( |
| shell(), |
| "let xhr = new XMLHttpRequest();" |
| "xhr.open('GET', './xhr', true);" |
| "xhr.onabort = () => window.domAutomationController.send('xhr.onabort');" |
| "xhr.onerror = () => window.domAutomationController.send('xhr.onerror');" |
| "xhr.onload = () => window.domAutomationController.send('xhr.onload');" |
| "xhr.send();"); |
| |
| // 2) Create an iframe and wait for the initial load. |
| { |
| ExecuteScriptAsync( |
| shell(), |
| "var iframe = document.createElement('iframe');" |
| "iframe.src = './title1.html';" |
| "iframe.onload = function() {" |
| " window.domAutomationController.send('iframe.onload');" |
| "};" |
| "document.body.appendChild(iframe);"); |
| |
| EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); |
| EXPECT_EQ("\"iframe.onload\"", message); |
| } |
| |
| // 3) Navigate the iframe elsewhere. |
| { |
| ExecuteScriptAsync(shell(), |
| "var iframe = document.querySelector('iframe');" |
| "iframe.src = './title2.html';"); |
| |
| EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); |
| EXPECT_EQ("\"iframe.onload\"", message); |
| } |
| |
| // 4) history.back() in the iframe. |
| { |
| ExecuteScriptAsync(shell(), |
| "var iframe = document.querySelector('iframe');" |
| "iframe.contentWindow.history.back()"); |
| |
| EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); |
| EXPECT_EQ("\"iframe.onload\"", message); |
| } |
| |
| // 5) history.forward() in the iframe. |
| { |
| ExecuteScriptAsync(shell(), |
| "var iframe = document.querySelector('iframe');" |
| "iframe.contentWindow.history.forward()"); |
| |
| EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); |
| EXPECT_EQ("\"iframe.onload\"", message); |
| } |
| |
| // 6) Wait for the XHR. |
| { |
| xhr_response.WaitForRequest(); |
| xhr_response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: close\r\n" |
| "Content-Length: 2\r\n" |
| "Content-Type: text/plain; charset=utf-8\r\n" |
| "\r\n" |
| "OK"); |
| xhr_response.Done(); |
| EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); |
| EXPECT_EQ("\"xhr.onload\"", message); |
| } |
| |
| EXPECT_FALSE(dom_message_queue.PopMessage(&message)); |
| } |
| |
| // Regression test for https://crbug.com/856396. |
| // Note that original issue for the bug is not applicable anymore, because there |
| // is no provisional document loader which has not committed yet. We keep the |
| // modified version of this test to check removing iframe from the load event |
| // handler. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, |
| ReplacingDocumentLoaderFiresLoadEvent) { |
| net::test_server::ControllableHttpResponse main_document_response( |
| embedded_test_server(), "/main_document"); |
| net::test_server::ControllableHttpResponse iframe_response( |
| embedded_test_server(), "/iframe"); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // 1) Load the main document. |
| shell()->LoadURL(embedded_test_server()->GetURL("/main_document")); |
| main_document_response.WaitForRequest(); |
| main_document_response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "\r\n" |
| "<script>" |
| " var detach_iframe = function() {" |
| " var iframe = document.querySelector('iframe');" |
| " iframe.parentNode.removeChild(iframe);" |
| " }" |
| "</script>" |
| "<body onload='detach_iframe()'>" |
| " <iframe src='/iframe'></iframe>" |
| "</body>"); |
| main_document_response.Done(); |
| |
| // 2) The iframe starts to load, but the server only have time to send the |
| // response's headers, not the response's body. This should commit the |
| // iframe's load. |
| iframe_response.WaitForRequest(); |
| iframe_response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "\r\n"); |
| |
| // 3) In the meantime the iframe navigates elsewhere. It causes the previous |
| // DocumentLoader to be replaced by the new one. Removing it may |
| // trigger the 'load' event and delete the iframe. |
| EXPECT_TRUE( |
| ExecJs(shell(), "document.querySelector('iframe').src = '/title1.html'")); |
| |
| // 4) Finish the original request. |
| iframe_response.Done(); |
| |
| // Wait for the iframe to be deleted and check the renderer process is still |
| // alive. |
| int iframe_count = 1; |
| while (iframe_count != 0) { |
| iframe_count = |
| EvalJs( |
| shell(), |
| "var iframe_count = document.getElementsByTagName('iframe').length;" |
| "iframe_count;") |
| .ExtractInt(); |
| } |
| } |
| |
| class NavigationDownloadBrowserTest : public NavigationBaseBrowserTest { |
| protected: |
| void SetUpOnMainThread() override { |
| NavigationBaseBrowserTest::SetUpOnMainThread(); |
| |
| // Set up a test download directory, in order to prevent prompting for |
| // handling downloads. |
| ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir()); |
| ShellDownloadManagerDelegate* delegate = |
| static_cast<ShellDownloadManagerDelegate*>( |
| web_contents()->GetBrowserContext()->GetDownloadManagerDelegate()); |
| delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath()); |
| } |
| |
| private: |
| base::ScopedTempDir downloads_directory_; |
| }; |
| |
| // Regression test for https://crbug.com/855033 |
| // 1) A page contains many scripts and DOM elements. It forces the parser to |
| // yield CPU to other tasks. That way the response body's data are not fully |
| // read when URLLoaderClient::OnComplete(..) is received. |
| // 2) A script makes the document navigates elsewhere while it is still loading. |
| // It cancels the parser of the current document. Due to a bug, the document |
| // loader was not marked to be 'loaded' at this step. |
| // 3) The request for the new navigation starts and it turns out it is a |
| // download. The navigation is dropped. |
| // 4) There are no more possibilities for DidStopLoading() to be sent. |
| IN_PROC_BROWSER_TEST_F(NavigationDownloadBrowserTest, |
| StopLoadingAfterDroppedNavigation) { |
| net::test_server::ControllableHttpResponse main_response( |
| embedded_test_server(), "/main"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL main_url(embedded_test_server()->GetURL("/main")); |
| GURL download_url(embedded_test_server()->GetURL("/download-test1.lib")); |
| |
| shell()->LoadURL(main_url); |
| main_response.WaitForRequest(); |
| std::string headers = |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "\r\n"; |
| |
| // Craft special HTML to make the blink::DocumentParser yield CPU to other |
| // tasks. The goal is to ensure the response body datapipe is not fully read |
| // when URLLoaderClient::OnComplete() is called. |
| // This relies on the HTMLParserScheduler::ShouldYield() heuristics. |
| std::string mix_of_script_and_div = "<script></script><div></div>"; |
| for (size_t i = 0; i < 10; ++i) { |
| mix_of_script_and_div += mix_of_script_and_div; // Exponential growth. |
| } |
| |
| std::string navigate_to_download = |
| "<script>location.href='" + download_url.spec() + "'</script>"; |
| |
| main_response.Send(headers + navigate_to_download + mix_of_script_and_div); |
| main_response.Done(); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| } |
| |
| // Renderer initiated back/forward navigation in beforeunload should not prevent |
| // the user to navigate away from a website. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, HistoryBackInBeforeUnload) { |
| GURL url_1(embedded_test_server()->GetURL("/title1.html")); |
| GURL url_2(embedded_test_server()->GetURL("/title2.html")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_1)); |
| EXPECT_TRUE(ExecJs(web_contents(), |
| "onbeforeunload = function() {" |
| " history.pushState({}, null, '/');" |
| " history.back();" |
| "};", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| EXPECT_TRUE(NavigateToURL(shell(), url_2)); |
| } |
| |
| // Same as 'HistoryBackInBeforeUnload', but wraps history.back() inside |
| // window.setTimeout(). Thus it is executed "outside" of its beforeunload |
| // handler and thus avoid basic navigation circumventions. |
| // Regression test for: https://crbug.com/879965. |
| IN_PROC_BROWSER_TEST_F(NavigationGoToEntryAtOffsetBrowserTest, |
| HistoryBackInBeforeUnloadAfterSetTimeout) { |
| GURL url_1(embedded_test_server()->GetURL("/title1.html")); |
| GURL url_2(embedded_test_server()->GetURL("/title2.html")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_1)); |
| EXPECT_TRUE(ExecJs(web_contents(), |
| "onbeforeunload = function() {" |
| " history.pushState({}, null, '/');" |
| " setTimeout(()=>history.back());" |
| "};", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| TestNavigationManager navigation(web_contents(), url_2); |
| |
| base::RunLoop run_loop; |
| SetQuitHandlerForGoToEntryAtOffset(run_loop.QuitClosure()); |
| shell()->LoadURL(url_2); |
| run_loop.Run(); |
| |
| ASSERT_TRUE(navigation.WaitForNavigationFinished()); |
| |
| EXPECT_TRUE(navigation.was_successful()); |
| } |
| |
| // Renderer initiated back/forward navigation can't cancel an ongoing browser |
| // initiated navigation if it is not user initiated. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| HistoryBackCancelPendingNavigationNoUserGesture) { |
| GURL url_1(embedded_test_server()->GetURL("/title1.html")); |
| GURL url_2(embedded_test_server()->GetURL("/title2.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_1)); |
| |
| // 1) A pending browser initiated navigation (omnibox, ...) starts. |
| TestNavigationManager navigation(web_contents(), url_2); |
| shell()->LoadURL(url_2); |
| EXPECT_TRUE(navigation.WaitForRequestStart()); |
| |
| // 2) history.back() is sent but is not user initiated. |
| EXPECT_TRUE(ExecJs(web_contents(), |
| "history.pushState({}, null, '/');" |
| "history.back();", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| // 3) The first pending navigation is not canceled and can continue. |
| ASSERT_TRUE(navigation.WaitForNavigationFinished()); // Resume navigation. |
| EXPECT_TRUE(navigation.was_successful()); |
| } |
| |
| // Renderer initiated back/forward navigation can cancel an ongoing browser |
| // initiated navigation if it is user initiated. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| HistoryBackCancelPendingNavigationUserGesture) { |
| GURL url_1(embedded_test_server()->GetURL("/title1.html")); |
| GURL url_2(embedded_test_server()->GetURL("/title2.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_1)); |
| |
| // 1) A pending browser initiated navigation (omnibox, ...) starts. |
| TestNavigationManager navigation(web_contents(), url_2); |
| shell()->LoadURL(url_2); |
| EXPECT_TRUE(navigation.WaitForRequestStart()); |
| |
| // 2) history.back() is sent and is user initiated. |
| EXPECT_TRUE(ExecJs(web_contents(), |
| "history.pushState({}, null, '/');" |
| "history.back();")); |
| |
| // 3) Check the first pending navigation has been canceled. |
| ASSERT_TRUE(navigation.WaitForNavigationFinished()); // Resume navigation. |
| EXPECT_FALSE(navigation.was_successful()); |
| } |
| |
| // Ensure the renderer process doesn't send too many IPC to the browser process |
| // when history.pushState() and history.back() are called in a loop. |
| // Failing to do so causes the browser to become unresponsive. |
| // See https://crbug.com/882238 |
| // TODO(crbug.com/379844650): Disabled on Linux sanitizer bots due to flakiness. |
| #if BUILDFLAG(IS_LINUX) && defined(ADDRESS_SANITIZER) |
| #define MAYBE_IPCFlood_GoToEntryAtOffset DISABLED_IPCFlood_GoToEntryAtOffset |
| #else |
| #define MAYBE_IPCFlood_GoToEntryAtOffset IPCFlood_GoToEntryAtOffset |
| #endif |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, MAYBE_IPCFlood_GoToEntryAtOffset) { |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "Throttling navigation to prevent the browser from hanging. See " |
| "https://crbug.com/1038223. Command line switch " |
| "--disable-ipc-flooding-protection can be used to bypass the " |
| "protection"); |
| |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| for(let i = 0; i<1000; ++i) { |
| history.pushState({},"page 2", "bar.html"); |
| history.back(); |
| } |
| )")); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // Ensure the renderer process doesn't send too many IPC to the browser process |
| // when doing a same-document navigation is requested in a loop. |
| // Failing to do so causes the browser to become unresponsive. |
| // TODO(arthursonzogni): Make the same test, but when the navigation is |
| // requested from a remote frame. |
| // See https://crbug.com/882238 |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, IPCFlood_Navigation) { |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "Throttling navigation to prevent the browser from hanging. See " |
| "https://crbug.com/1038223. Command line switch " |
| "--disable-ipc-flooding-protection can be used to bypass the " |
| "protection"); |
| |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| for(let i = 0; i<1000; ++i) { |
| location.href = "#" + i; |
| ++i; |
| } |
| )")); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // TODO(http://crbug.com/632514): This test currently expects opener downloads |
| // go through, but when the linked bug is resolved the download should be |
| // disallowed. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OpenerNavigation_DownloadPolicy) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::ScopedTempDir download_dir; |
| ASSERT_TRUE(download_dir.CreateUniqueTempDir()); |
| ShellDownloadManagerDelegate* delegate = |
| static_cast<ShellDownloadManagerDelegate*>( |
| web_contents()->GetBrowserContext()->GetDownloadManagerDelegate()); |
| delegate->SetDownloadBehaviorForTesting(download_dir.GetPath()); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| |
| // Open a popup. |
| EXPECT_EQ(true, EvalJs(web_contents(), "!!window.open();")); |
| EXPECT_EQ(2u, Shell::windows().size()); |
| |
| // Using the popup, navigate its opener to a download. |
| base::HistogramTester histograms; |
| WebContents* popup = Shell::windows()[1]->web_contents(); |
| EXPECT_NE(popup, web_contents()); |
| DownloadTestObserverInProgress observer( |
| web_contents()->GetBrowserContext()->GetDownloadManager(), |
| 1 /* wait_count */); |
| EXPECT_TRUE(ExecJs( |
| popup, |
| "window.opener.location ='data:html/text;base64,'+btoa('payload');", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| observer.WaitForFinished(); |
| } |
| |
| // A variation of the OpenerNavigation_DownloadPolicy test above, but uses a |
| // cross-origin URL for the popup window. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| CrossOriginOpenerNavigation_DownloadPolicy) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::ScopedTempDir download_dir; |
| ASSERT_TRUE(download_dir.CreateUniqueTempDir()); |
| ShellDownloadManagerDelegate* delegate = |
| static_cast<ShellDownloadManagerDelegate*>( |
| web_contents()->GetBrowserContext()->GetDownloadManagerDelegate()); |
| delegate->SetDownloadBehaviorForTesting(download_dir.GetPath()); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Open a popup. |
| ShellAddedObserver shell_observer; |
| EXPECT_TRUE(EvalJs(web_contents(), JsReplace("!!window.open($1);", |
| embedded_test_server()->GetURL( |
| "bar.com", "/title1.html"))) |
| .ExtractBool()); |
| Shell* new_shell = shell_observer.GetShell(); |
| EXPECT_EQ(2u, Shell::windows().size()); |
| |
| // Wait for the navigation in the popup to complete, so the origin of the |
| // document will be correct. |
| WebContents* popup = new_shell->web_contents(); |
| EXPECT_NE(popup, web_contents()); |
| EXPECT_TRUE(WaitForLoadStop(popup)); |
| |
| // Using the popup, navigate its opener to a download. |
| base::HistogramTester histograms; |
| const GURL data_url("data:html/text;base64,cGF5bG9hZA=="); |
| TestNavigationManager manager(web_contents(), data_url); |
| EXPECT_TRUE(ExecJs(popup, base::StringPrintf("window.opener.location ='%s'", |
| data_url.spec().c_str()))); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| |
| EXPECT_FALSE(manager.was_successful()); |
| } |
| |
| // Regression test for https://crbug.com/872284. |
| // A NavigationThrottle cancels a download in WillProcessResponse. |
| // The navigation request must be canceled and it must also cancel the network |
| // request. Failing to do so resulted in the network socket being leaked. |
| IN_PROC_BROWSER_TEST_F(NavigationDownloadBrowserTest, |
| CancelDownloadOnResponseStarted) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Block every iframe in WillProcessResponse. |
| content::TestNavigationThrottleInserter throttle_inserter( |
| web_contents(), |
| base::BindLambdaForTesting( |
| [&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { |
| auto throttle = std::make_unique<TestNavigationThrottle>(handle); |
| throttle->SetResponse(TestNavigationThrottle::WILL_PROCESS_RESPONSE, |
| TestNavigationThrottle::SYNCHRONOUS, |
| NavigationThrottle::CANCEL_AND_IGNORE); |
| |
| return throttle; |
| })); |
| |
| // Insert enough iframes so that if sockets are not properly released: there |
| // will not be enough of them to complete all navigations. As of today, only 6 |
| // sockets can be used simultaneously. So using 7 iframes is enough. This test |
| // uses 33 as a margin. |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| for(let i = 0; i<33; ++i) { |
| let iframe = document.createElement('iframe'); |
| iframe.src = './download-test1.lib' |
| document.body.appendChild(iframe); |
| } |
| )")); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| } |
| |
| // Add header on redirect. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, AddRequestHeaderOnRedirect) { |
| net::test_server::ControllableHttpResponse response_1(embedded_test_server(), |
| "", true); |
| net::test_server::ControllableHttpResponse response_2(embedded_test_server(), |
| "", true); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| content::TestNavigationThrottleInserter throttle_inserter( |
| web_contents(), |
| base::BindLambdaForTesting( |
| [](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { |
| auto throttle = std::make_unique<TestNavigationThrottle>(handle); |
| NavigationRequest* request = NavigationRequest::From(handle); |
| throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST, |
| base::BindLambdaForTesting([request]() { |
| request->SetRequestHeader("header_name", |
| "header_value"); |
| })); |
| return throttle; |
| })); |
| |
| // 1) There is no "header_name" header in the initial request. |
| shell()->LoadURL(embedded_test_server()->GetURL("/doc")); |
| response_1.WaitForRequest(); |
| EXPECT_FALSE( |
| base::Contains(response_1.http_request()->headers, "header_name")); |
| response_1.Send( |
| "HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n"); |
| response_1.Done(); |
| |
| // 2) The header is added to the second request after the redirect. |
| response_2.WaitForRequest(); |
| EXPECT_EQ("header_value", |
| response_2.http_request()->headers.at("header_name")); |
| |
| // Redirect should not record a ReceivedResponse event. |
| EXPECT_EQ(0u, test_ukm_recorder() |
| .GetEntriesByName("Navigation.ReceivedResponse") |
| .size()); |
| } |
| |
| // Add header on request start, modify it on redirect. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, |
| AddRequestHeaderModifyOnRedirect) { |
| net::test_server::ControllableHttpResponse response_1(embedded_test_server(), |
| "", true); |
| net::test_server::ControllableHttpResponse response_2(embedded_test_server(), |
| "", true); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| content::TestNavigationThrottleInserter throttle_inserter( |
| web_contents(), |
| base::BindLambdaForTesting( |
| [](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { |
| auto throttle = std::make_unique<TestNavigationThrottle>(handle); |
| NavigationRequest* request = NavigationRequest::From(handle); |
| throttle->SetCallback(TestNavigationThrottle::WILL_START_REQUEST, |
| base::BindLambdaForTesting([request]() { |
| request->SetRequestHeader("header_name", |
| "header_value"); |
| })); |
| throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST, |
| base::BindLambdaForTesting([request]() { |
| request->SetRequestHeader("header_name", |
| "other_value"); |
| })); |
| return throttle; |
| })); |
| |
| // 1) The header is added to the initial request. |
| shell()->LoadURL(embedded_test_server()->GetURL("/doc")); |
| response_1.WaitForRequest(); |
| EXPECT_EQ("header_value", |
| response_1.http_request()->headers.at("header_name")); |
| response_1.Send( |
| "HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n"); |
| response_1.Done(); |
| |
| // 2) The header is modified in the second request after the redirect. |
| response_2.WaitForRequest(); |
| EXPECT_EQ("other_value", |
| response_2.http_request()->headers.at("header_name")); |
| } |
| |
| // Add header on request start, remove it on redirect. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, |
| AddRequestHeaderRemoveOnRedirect) { |
| net::test_server::ControllableHttpResponse response_1(embedded_test_server(), |
| "", true); |
| net::test_server::ControllableHttpResponse response_2(embedded_test_server(), |
| "", true); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| content::TestNavigationThrottleInserter throttle_inserter( |
| web_contents(), |
| base::BindLambdaForTesting( |
| [](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { |
| NavigationRequest* request = NavigationRequest::From(handle); |
| auto throttle = std::make_unique<TestNavigationThrottle>(handle); |
| throttle->SetCallback(TestNavigationThrottle::WILL_START_REQUEST, |
| base::BindLambdaForTesting([request]() { |
| request->SetRequestHeader("header_name", |
| "header_value"); |
| })); |
| throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST, |
| base::BindLambdaForTesting([request]() { |
| request->RemoveRequestHeader("header_name"); |
| })); |
| return throttle; |
| })); |
| |
| // 1) The header is added to the initial request. |
| shell()->LoadURL(embedded_test_server()->GetURL("/doc")); |
| response_1.WaitForRequest(); |
| EXPECT_EQ("header_value", |
| response_1.http_request()->headers.at("header_name")); |
| response_1.Send( |
| "HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n"); |
| response_1.Done(); |
| |
| // 2) The header is removed from the second request after the redirect. |
| response_2.WaitForRequest(); |
| EXPECT_FALSE( |
| base::Contains(response_2.http_request()->headers, "header_name")); |
| } |
| |
| // Name of header used by CorsInjectingUrlLoader. |
| const std::string kCorsHeaderName = "test-header"; |
| |
| // URLLoaderThrottle that stores the last value of |kCorsHeaderName|. |
| class CorsInjectingUrlLoader : public blink::URLLoaderThrottle { |
| public: |
| explicit CorsInjectingUrlLoader(std::string* last_cors_header_value) |
| : last_cors_header_value_(last_cors_header_value) {} |
| |
| // blink::URLLoaderThrottle: |
| void WillStartRequest(network::ResourceRequest* request, |
| bool* defer) override { |
| if (std::optional<std::string> header = |
| request->cors_exempt_headers.GetHeader(kCorsHeaderName); |
| header) { |
| last_cors_header_value_->swap(*header); |
| } else { |
| last_cors_header_value_->clear(); |
| } |
| } |
| |
| private: |
| // See |NavigationCorsExemptBrowserTest::last_cors_header_value_| for details. |
| raw_ptr<std::string> last_cors_header_value_; |
| }; |
| |
| // ContentBrowserClient responsible for creating CorsInjectingUrlLoader. |
| class CorsContentBrowserClient : public ContentBrowserTestContentBrowserClient { |
| public: |
| explicit CorsContentBrowserClient(std::string* last_cors_header_value) |
| : last_cors_header_value_(last_cors_header_value) {} |
| |
| // ContentBrowserClient overrides: |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> |
| CreateURLLoaderThrottles( |
| const network::ResourceRequest& request, |
| BrowserContext* browser_context, |
| const base::RepeatingCallback<WebContents*()>& wc_getter, |
| NavigationUIData* navigation_ui_data, |
| FrameTreeNodeId frame_tree_node_id, |
| std::optional<int64_t> navigation_id) override { |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles; |
| throttles.push_back( |
| std::make_unique<CorsInjectingUrlLoader>(last_cors_header_value_)); |
| return throttles; |
| } |
| |
| private: |
| // See |NavigationCorsExemptBrowserTest::last_cors_header_value_| for details. |
| raw_ptr<std::string> last_cors_header_value_; |
| }; |
| |
| class NavigationCorsExemptBrowserTest : public NavigationBaseBrowserTest { |
| public: |
| NavigationCorsExemptBrowserTest() = default; |
| |
| protected: |
| const std::string& last_cors_header_value() const { |
| return last_cors_header_value_; |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ShellContentBrowserClient::set_allow_any_cors_exempt_header_for_browser( |
| true); |
| NavigationBaseBrowserTest::SetUpCommandLine(command_line); |
| } |
| void SetUpOnMainThread() override { |
| cors_content_browser_client_ = |
| std::make_unique<CorsContentBrowserClient>(&last_cors_header_value_); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| } |
| void TearDownOnMainThread() override { |
| cors_content_browser_client_.reset(); |
| ShellContentBrowserClient::set_allow_any_cors_exempt_header_for_browser( |
| false); |
| } |
| |
| private: |
| // Last value of kCorsHeaderName. Set by CorsInjectingUrlLoader. |
| std::string last_cors_header_value_; |
| std::unique_ptr<CorsContentBrowserClient> cors_content_browser_client_; |
| }; |
| |
| // Verifies a header added by way of SetRequestHeader() makes it into |
| // |cors_exempt_headers|. |
| IN_PROC_BROWSER_TEST_F(NavigationCorsExemptBrowserTest, |
| SetCorsExemptRequestHeader) { |
| net::test_server::ControllableHttpResponse response(embedded_test_server(), |
| "", true); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const std::string header_value = "value"; |
| content::TestNavigationThrottleInserter throttle_inserter( |
| web_contents(), |
| base::BindLambdaForTesting([header_value](NavigationHandle* handle) |
| -> std::unique_ptr<NavigationThrottle> { |
| NavigationRequest* request = NavigationRequest::From(handle); |
| auto throttle = std::make_unique<TestNavigationThrottle>(handle); |
| throttle->SetCallback( |
| TestNavigationThrottle::WILL_START_REQUEST, |
| base::BindLambdaForTesting([request, header_value]() { |
| request->SetCorsExemptRequestHeader(kCorsHeaderName, |
| header_value); |
| })); |
| return throttle; |
| })); |
| shell()->LoadURL(embedded_test_server()->GetURL("/doc")); |
| response.WaitForRequest(); |
| EXPECT_EQ(header_value, response.http_request()->headers.at(kCorsHeaderName)); |
| EXPECT_EQ(header_value, last_cors_header_value()); |
| } |
| |
| // Test NavigationRequest::CheckAboutSrcDoc() |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BlockedSrcDocBrowserInitiated) { |
| const char* about_srcdoc_urls[] = {"about:srcdoc", "about:srcdoc?foo", |
| "about:srcdoc#foo"}; |
| // 1. Main frame navigations to about:srcdoc and its variations are blocked. |
| for (const char* url : about_srcdoc_urls) { |
| NavigationHandleObserver handle_observer(web_contents(), GURL(url)); |
| EXPECT_FALSE(NavigateToURL(shell(), GURL(url))); |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_TRUE(handle_observer.is_error()); |
| EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code()); |
| } |
| |
| // 2. Subframe navigations to variations of about:srcdoc are not blocked. |
| for (const char* url : about_srcdoc_urls) { |
| GURL main_url = |
| embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| NavigationHandleObserver handle_observer(web_contents(), GURL(url)); |
| shell()->LoadURLForFrame(GURL(url), "child-name-0", |
| ui::PAGE_TRANSITION_FORWARD_BACK); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_FALSE(handle_observer.is_error()); |
| EXPECT_EQ(net::OK, handle_observer.net_error_code()); |
| } |
| } |
| |
| // Test NavigationRequest::CheckAboutSrcDoc(). |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BlockedSrcDocRendererInitiated) { |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| const char* about_srcdoc_urls[] = {"about:srcdoc", "about:srcdoc?foo", |
| "about:srcdoc#foo"}; |
| |
| // 1. Main frame navigations to about:srcdoc and its variations are blocked. |
| for (const char* url : about_srcdoc_urls) { |
| DidStartNavigationObserver start_observer(web_contents()); |
| NavigationHandleObserver handle_observer(web_contents(), GURL(url)); |
| EXPECT_TRUE(ExecJs(main_frame(), JsReplace("location.href = $1", url))); |
| start_observer.Wait(); |
| WaitForLoadStop(web_contents()); |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_TRUE(handle_observer.is_error()); |
| EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code()); |
| } |
| |
| // 2. Subframe navigations to variations of about:srcdoc are blocked unless |
| // they are same-document or the initiator is same origin to the srcdoc's |
| // parent. In this test suite, the subframe is self-navigating, but attempting |
| // a cross-origin navigation from a non-about:srcdoc page to about:srcdoc, |
| // which isn't allowed. |
| for (const char* url : about_srcdoc_urls) { |
| GURL main_url = |
| embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| DidStartNavigationObserver start_observer(web_contents()); |
| NavigationHandleObserver handle_observer(web_contents(), GURL(url)); |
| FrameTreeNode* subframe = main_frame()->child_at(0); |
| // Executing location.href = "about:srcdoc" fails. |
| // Note: if the subframe had already been navigated to about:srcdoc, then |
| // executing location.href = 'about:srcdoc#foo' would be considered a same- |
| // document navigation, and would be allowed. This behavior is tested in |
| // web platform tests, e.g. |
| // grandparent_location_aboutsrcdoc.sub.window.js, added in the same CL that |
| // added this comment. |
| EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", url))); |
| start_observer.Wait(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_TRUE(handle_observer.is_error()); |
| EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code()); |
| } |
| |
| // 3. Navigate the sub-frame to be same-origin to the mainframe, then do the |
| // about:srcdoc navigations with with the main frame as the initiator. The |
| // test should succeed since the initiator and the parent are the same. |
| { |
| DidStartNavigationObserver start_observer(web_contents()); |
| EXPECT_TRUE(ExecJs( |
| main_frame(), "document.querySelector('iframe').src = 'title1.html';")); |
| start_observer.Wait(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| } |
| for (const char* url : about_srcdoc_urls) { |
| DidStartNavigationObserver start_observer(web_contents()); |
| NavigationHandleObserver handle_observer(web_contents(), GURL(url)); |
| EXPECT_TRUE( |
| ExecJs(main_frame(), JsReplace("frames[0].location.href = $1", url))); |
| start_observer.Wait(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_FALSE(handle_observer.is_error()); |
| EXPECT_EQ(net::OK, handle_observer.net_error_code()); |
| } |
| |
| // 4. The subframe is now on about:srcdoc ... verify it can reload itself. |
| { |
| FrameTreeNode* subframe = main_frame()->child_at(0); |
| GURL url("about:srcdoc"); |
| { |
| // First, navigate the subframe to about:srcdoc without a fragment, so |
| // that the subsequent reload will have a commit (i.e. it won't be |
| // reloading a same-document navigation). |
| DidStartNavigationObserver start_observer(web_contents()); |
| NavigationHandleObserver handle_observer(web_contents(), GURL(url)); |
| EXPECT_TRUE( |
| ExecJs(main_frame(), JsReplace("frames[0].location.href = $1", url))); |
| start_observer.Wait(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_FALSE(handle_observer.is_error()); |
| EXPECT_EQ(net::OK, handle_observer.net_error_code()); |
| } |
| DidStartNavigationObserver start_observer(web_contents()); |
| NavigationHandleObserver handle_observer(web_contents(), url); |
| EXPECT_TRUE(ExecJs(subframe, "location.reload()")); |
| start_observer.Wait(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| EXPECT_TRUE(handle_observer.has_committed()); |
| EXPECT_FALSE(handle_observer.is_error()); |
| EXPECT_EQ(net::OK, handle_observer.net_error_code()); |
| } |
| } |
| |
| // Ensure that about:srcdoc navigations get their origin and base URL from their |
| // parent frame (since that's where the content comes from) and not from the |
| // initiator of the navigation (like about:blank cases). See also the |
| // NavigateGrandchildToAboutBlank test. See https://crbug.com/1515381. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| GrandchildToAboutSrcdoc_BaseUrl_CrossOrigin) { |
| GURL parent_url = embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_one_frame.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), parent_url)); |
| FrameTreeNode* subframe = main_frame()->child_at(0); |
| |
| // Navigate the subframe to a cross-site URL with a srcdoc subframe. |
| GURL child_url = embedded_test_server()->GetURL( |
| "b.com", "/frame_tree/page_with_srcdoc_frame.html"); |
| TestNavigationObserver observer(web_contents()); |
| EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", child_url))); |
| observer.Wait(); |
| EXPECT_EQ(child_url, subframe->current_frame_host()->GetLastCommittedURL()); |
| FrameTreeNode* grandchild = subframe->child_at(0); |
| EXPECT_EQ("hello", |
| EvalJs(grandchild, "document.body.innerHTML").ExtractString()); |
| |
| // From the main frame, attempt to navigate the grandchild frame to |
| // about:srcdoc. This should fail. |
| TestNavigationObserver srcdoc_observer(web_contents()); |
| EXPECT_TRUE( |
| ExecJs(main_frame(), "frames[0][0].location.href = 'about:srcdoc';")); |
| srcdoc_observer.Wait(); |
| |
| EXPECT_EQ( |
| "Could not load the requested resource.<br>Error code: -300 " |
| "(net::ERR_INVALID_URL)", |
| EvalJs(grandchild, "document.body.innerHTML").ExtractString()); |
| |
| // Since the navigation attempt failed, the origin and base URI are inherited |
| // from the error page. |
| EXPECT_EQ(GURL(url::kAboutSrcdocURL), |
| grandchild->current_frame_host()->GetLastCommittedURL()); |
| EXPECT_TRUE( |
| grandchild->current_frame_host()->GetLastCommittedOrigin().opaque()); |
| EXPECT_EQ("chrome-error://chromewebdata/", |
| EvalJs(grandchild, "document.baseURI").ExtractString()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| GrandchildToAboutSrcdoc_BaseUrl_SameOrigin) { |
| GURL mainframe_url = embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_one_frame.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), mainframe_url)); |
| FrameTreeNode* sub_frame = main_frame()->child_at(0); |
| |
| // Navigate `sub_frame` to a same-origin URL with a srcdoc subframe. |
| GURL subframe_url = embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_srcdoc_frame.html"); |
| TestNavigationObserver observer(web_contents()); |
| EXPECT_TRUE(ExecJs(sub_frame, JsReplace("location.href = $1", subframe_url))); |
| observer.Wait(); |
| EXPECT_EQ(subframe_url, |
| sub_frame->current_frame_host()->GetLastCommittedURL()); |
| FrameTreeNode* srcdoc_frame = sub_frame->child_at(0); |
| EXPECT_EQ("hello", |
| EvalJs(srcdoc_frame, "document.body.innerHTML").ExtractString()); |
| EXPECT_EQ(subframe_url, |
| EvalJs(srcdoc_frame, "document.baseURI").ExtractString()); |
| |
| // From the mainframe, attempt to navigate `srcdoc_frame` to about:srcdoc. |
| // This should succeed since the mainframe and `sub_frame` are same origin. |
| TestNavigationObserver srcdoc_observer(web_contents()); |
| EXPECT_TRUE( |
| ExecJs(main_frame(), "frames[0][0].location.href = 'about:srcdoc';")); |
| srcdoc_observer.Wait(); |
| |
| EXPECT_EQ("hello", |
| EvalJs(srcdoc_frame, "document.body.innerHTML").ExtractString()); |
| |
| // The origin and base URI should be inherited from the initiator, since it's |
| // same-origin to the srcdoc's parent frame `sub_frame`. |
| EXPECT_EQ(GURL(url::kAboutSrcdocURL), |
| srcdoc_frame->current_frame_host()->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(subframe_url), |
| srcdoc_frame->current_frame_host()->GetLastCommittedOrigin()); |
| // This picks up the mainframe's url since it's same-origin to `sub_frame`, |
| // which is `srcdoc_frame`'s parent. |
| EXPECT_EQ(mainframe_url, |
| EvalJs(srcdoc_frame, "document.baseURI").ExtractString()); |
| } |
| |
| // Ensure that about:blank navigations get their origin and base URL from the |
| // initiator of the navigation, and not from their parent frame (like |
| // about:srcdoc cases). See also the NavigateGrandchildToAboutSrcdoc test. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, GrandchildToAboutBlank_BaseUrl) { |
| GURL url_a = embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_one_frame.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| FrameTreeNode* subframe = main_frame()->child_at(0); |
| |
| // Navigate the subframe to a cross-site URL with a srcdoc subframe. |
| GURL url_b = embedded_test_server()->GetURL( |
| "b.com", "/frame_tree/page_with_srcdoc_frame.html"); |
| TestNavigationObserver observer(web_contents()); |
| EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", url_b))); |
| observer.Wait(); |
| EXPECT_EQ(url_b, subframe->current_frame_host()->GetLastCommittedURL()); |
| FrameTreeNode* grandchild = subframe->child_at(0); |
| EXPECT_EQ("hello", |
| EvalJs(grandchild, "document.body.innerHTML").ExtractString()); |
| |
| // From the main frame, navigate the grandchild frame to about:blank. |
| TestNavigationObserver srcdoc_observer(web_contents()); |
| EXPECT_TRUE( |
| ExecJs(main_frame(), "frames[0][0].location.href = 'about:blank';")); |
| srcdoc_observer.Wait(); |
| |
| // There is no content when navigating to about:blank. |
| EXPECT_EQ("", EvalJs(grandchild, "document.body.innerHTML").ExtractString()); |
| |
| // The origin and base URI should be inherited from the initiator of the |
| // navigation and not the parent frame, unlike navigations to about:srcdoc. |
| EXPECT_EQ(GURL(url::kAboutBlankURL), |
| grandchild->current_frame_host()->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(url_a), |
| grandchild->current_frame_host()->GetLastCommittedOrigin()); |
| EXPECT_EQ(url_a, EvalJs(grandchild, "document.baseURI").ExtractString()); |
| } |
| |
| // Test renderer initiated navigations to about:srcdoc are routed through the |
| // browser process. It means RenderFrameHostImpl::BeginNavigation() is called. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, AboutSrcDocUsesBeginNavigation) { |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // If DidStartNavigation is called before DidCommitProvisionalLoad, then it |
| // means the navigation was driven by the browser process, otherwise by the |
| // renderer process. This tests it was driven by the browser process: |
| InterceptAndCancelDidCommitProvisionalLoad interceptor(web_contents()); |
| DidStartNavigationObserver observer(web_contents()); |
| |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| let iframe = document.createElement("iframe"); |
| iframe.srcdoc = "foo" |
| document.body.appendChild(iframe); |
| )")); |
| |
| observer.Wait(); // BeginNavigation is called. |
| interceptor.Wait(1); // DidCommitNavigation is called. |
| } |
| |
| // Regression test for https://crbug.com/996044 |
| // 1) Navigate an iframe to srcdoc (about:srcdoc); |
| // 2) Same-document navigation to about:srcdoc#1. |
| // 3) Same-document navigation to about:srcdoc#2. |
| // 4) history.back() to about:srcdoc#1. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SrcDocWithFragmentHistoryNavigation) { |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // 1) Navigate an iframe to srcdoc (about:srcdoc) |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| new Promise(async resolve => { |
| let iframe = document.createElement('iframe'); |
| iframe.srcdoc = "test"; |
| iframe.onload = resolve; |
| document.body.appendChild(iframe); |
| }); |
| )")); |
| |
| // 2) Same-document navigation to about:srcdoc#1. |
| // 3) Same-document navigation to about:srcdoc#2. |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| let subwindow = document.querySelector('iframe').contentWindow; |
| subwindow.location.hash = "1"; |
| subwindow.location.hash = "2"; |
| )")); |
| |
| // Inspect the session history. |
| NavigationControllerImpl& controller = web_contents()->GetController(); |
| ASSERT_EQ(3, controller.GetEntryCount()); |
| ASSERT_EQ(2, controller.GetCurrentEntryIndex()); |
| |
| std::array<FrameNavigationEntry*, 3> entry; |
| for (int i = 0; i < 3; ++i) { |
| entry[i] = controller.GetEntryAtIndex(i) |
| ->root_node() |
| ->children[0] |
| ->frame_entry.get(); |
| } |
| |
| EXPECT_EQ(entry[0]->url(), "about:srcdoc"); |
| EXPECT_EQ(entry[1]->url(), "about:srcdoc#1"); |
| EXPECT_EQ(entry[2]->url(), "about:srcdoc#2"); |
| |
| // 4) history.back() to about:srcdoc#1. |
| EXPECT_TRUE(ExecJs(shell(), "history.back()")); |
| |
| ASSERT_EQ(3, controller.GetEntryCount()); |
| ASSERT_EQ(1, controller.GetCurrentEntryIndex()); |
| } |
| |
| // Regression test for https://crbug.com/996044. |
| // 1) Navigate an iframe to srcdoc (about:srcdoc). |
| // 2) Cross-document navigation to about:srcdoc?1. |
| // 3) Cross-document navigation to about:srcdoc?2. |
| // 4) history.back() to about:srcdoc?1. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SrcDocWithQueryHistoryNavigation) { |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // 1) Navigate an iframe to srcdoc (about:srcdoc). |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| new Promise(async resolve => { |
| let iframe = document.createElement('iframe'); |
| iframe.srcdoc = "test"; |
| iframe.onload = resolve; |
| document.body.appendChild(iframe); |
| }); |
| )")); |
| |
| // 2) Cross-document navigation to about:srcdoc?1. |
| { |
| TestNavigationManager commit_waiter(web_contents(), GURL("about:srcdoc?1")); |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| let subwindow = document.querySelector('iframe').contentWindow; |
| subwindow.location.search = "1"; |
| )")); |
| ASSERT_TRUE(commit_waiter.WaitForNavigationFinished()); |
| } |
| |
| // 3) Cross-document navigation to about:srcdoc?2. |
| { |
| TestNavigationManager commit_waiter(web_contents(), GURL("about:srcdoc?2")); |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| let subwindow = document.querySelector('iframe').contentWindow; |
| subwindow.location.search = "2"; |
| )")); |
| ASSERT_TRUE(commit_waiter.WaitForNavigationFinished()); |
| } |
| |
| // Inspect the session history. |
| NavigationControllerImpl& controller = web_contents()->GetController(); |
| ASSERT_EQ(3, controller.GetEntryCount()); |
| ASSERT_EQ(2, controller.GetCurrentEntryIndex()); |
| |
| std::array<FrameNavigationEntry*, 3> entry; |
| for (int i = 0; i < 3; ++i) { |
| entry[i] = controller.GetEntryAtIndex(i) |
| ->root_node() |
| ->children[0] |
| ->frame_entry.get(); |
| } |
| |
| EXPECT_EQ(entry[0]->url(), "about:srcdoc"); |
| EXPECT_EQ(entry[1]->url(), "about:srcdoc?1"); |
| EXPECT_EQ(entry[2]->url(), "about:srcdoc?2"); |
| |
| // 4) history.back() to about:srcdoc#1. |
| EXPECT_TRUE(ExecJs(shell(), "history.back()")); |
| |
| ASSERT_EQ(3, controller.GetEntryCount()); |
| ASSERT_EQ(1, controller.GetCurrentEntryIndex()); |
| } |
| |
| // Make sure embedders are notified about visible URL changes in this scenario: |
| // 1. Navigate to A. |
| // 2. Navigate to B. |
| // 3. Add a forward entry in the history for later (same-document). |
| // 4. Start navigation to C. |
| // 5. Start history cross-document navigation, cancelling 4. |
| // 6. Start history same-document navigation, cancelling 5. |
| // |
| // Regression test for https://crbug.com/998284. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, |
| BackForwardInOldDocumentCancelPendingNavigation) { |
| // This test expects a new request to be made when navigating back, which is |
| // not happening with back-forward cache enabled. |
| // See BackForwardCacheBrowserTest.RestoreWhilePendingCommit which covers the |
| // same scenario for back-forward cache. |
| web_contents()->GetController().GetBackForwardCache().DisableForTesting( |
| BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING); |
| |
| using Response = net::test_server::ControllableHttpResponse; |
| Response response_A1(embedded_test_server(), "/A"); |
| Response response_A2(embedded_test_server(), "/A"); |
| Response response_B1(embedded_test_server(), "/B"); |
| Response response_C1(embedded_test_server(), "/C"); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a = embedded_test_server()->GetURL("a.com", "/A"); |
| GURL url_b = embedded_test_server()->GetURL("b.com", "/B"); |
| GURL url_c = embedded_test_server()->GetURL("c.com", "/C"); |
| |
| EmbedderVisibleUrlTracker embedder_url_tracker; |
| web_contents()->SetDelegate(&embedder_url_tracker); |
| |
| // 1. Navigate to A. |
| shell()->LoadURL(url_a); |
| response_A1.WaitForRequest(); |
| response_A1.Send(non_cacheable_html_response); |
| response_A1.Done(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // 2. Navigate to B. |
| shell()->LoadURL(url_b); |
| response_B1.WaitForRequest(); |
| response_B1.Send(non_cacheable_html_response); |
| response_B1.Done(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // 3. Add a forward entry in the history for later (same-document). |
| EXPECT_TRUE(ExecJs(web_contents(), R"( |
| history.pushState({},''); |
| history.back(); |
| )")); |
| |
| // 4. Start navigation to C. |
| { |
| EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); |
| EXPECT_EQ(url_b, embedder_url_tracker.url()); |
| } |
| shell()->LoadURL(url_c); |
| // TODO(arthursonzogni): The embedder_url_tracker should update to url_c at |
| // this point, but we currently rely on FrameTreeNode::DidStopLoading for |
| // invalidation and it does not occur when a prior navigation is already in |
| // progress. The browser is still waiting on the same-document |
| // "history.back()" to complete. |
| { |
| EXPECT_EQ(url_c, web_contents()->GetVisibleURL()); |
| EXPECT_EQ(url_b, embedder_url_tracker.url()); |
| } |
| embedder_url_tracker.WaitUntilUrlInvalidated(); |
| { |
| EXPECT_EQ(url_c, web_contents()->GetVisibleURL()); |
| EXPECT_EQ(url_c, embedder_url_tracker.url()); |
| } |
| response_C1.WaitForRequest(); |
| |
| // 5. Start history cross-document navigation, cancelling 4. |
| EXPECT_TRUE(ExecJs(web_contents(), "history.back()")); |
| { |
| EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); |
| EXPECT_EQ(url_b, embedder_url_tracker.url()); |
| } |
| response_A2.WaitForRequest(); |
| { |
| EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); |
| EXPECT_EQ(url_b, embedder_url_tracker.url()); |
| } |
| |
| // 6. Start history same-document navigation, cancelling 5. |
| EXPECT_TRUE(ExecJs(web_contents(), "history.forward()")); |
| { |
| EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); |
| EXPECT_EQ(url_b, embedder_url_tracker.url()); |
| } |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| { |
| EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); |
| EXPECT_EQ(url_b, embedder_url_tracker.url()); |
| } |
| } |
| |
| // Regression test for https://crbug.com/999932. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, CanceledNavigationBug999932) { |
| using Response = net::test_server::ControllableHttpResponse; |
| Response response_A1(embedded_test_server(), "/A"); |
| Response response_A2(embedded_test_server(), "/A"); |
| Response response_B1(embedded_test_server(), "/B"); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a = embedded_test_server()->GetURL("a.com", "/A"); |
| GURL url_b = embedded_test_server()->GetURL("b.com", "/B"); |
| |
| // 1. Navigate to A. |
| shell()->LoadURL(url_a); |
| response_A1.WaitForRequest(); |
| response_A1.Send(non_cacheable_html_response); |
| response_A1.Done(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // 2. Start pending navigation to B. |
| shell()->LoadURL(url_b); |
| EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); |
| EXPECT_TRUE(web_contents()->GetController().GetPendingEntry()); |
| |
| // 3. Cancel (2) with renderer-initiated reload with a UserGesture. |
| EXPECT_TRUE(ExecJs(web_contents(), "location.reload()")); |
| EXPECT_EQ(url_a, web_contents()->GetVisibleURL()); |
| EXPECT_FALSE(web_contents()->GetController().GetPendingEntry()); |
| |
| // 4. Cancel (3) using document.open(); |
| EXPECT_TRUE(ExecJs(web_contents(), "document.open()")); |
| EXPECT_EQ(url_a, web_contents()->GetVisibleURL()); |
| EXPECT_FALSE(web_contents()->GetController().GetPendingEntry()); |
| } |
| |
| // Regression test for https://crbug.com/1001283 |
| // 1) Load main document with CSP: script-src 'none' |
| // 2) Open an about:srcdoc iframe. It inherits the CSP. |
| // 3) The iframe navigates elsewhere. |
| // 4) The iframe navigates back to about:srcdoc. |
| // Check Javascript is never allowed. |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, |
| SrcDocCSPInheritedAfterSameSiteHistoryNavigation) { |
| using Response = net::test_server::ControllableHttpResponse; |
| Response main_document_response(embedded_test_server(), "/main_document"); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a = embedded_test_server()->GetURL("a.com", "/main_document"); |
| GURL url_b = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Refused to execute inline script *"); |
| |
| // 1) Load main document with CSP: script-src 'none' |
| // 2) Open an about:srcdoc iframe. It inherits the CSP from its parent. |
| shell()->LoadURL(url_a); |
| main_document_response.WaitForRequest(); |
| main_document_response.Send( |
| "HTTP/1.1 200 OK\n" |
| "content-type: text/html; charset=UTF-8\n" |
| "Content-Security-Policy: script-src 'none'\n" |
| "\n" |
| "<iframe name='theiframe' srcdoc='" |
| " <script>" |
| " console.error(\"CSP failure\");" |
| " </script>" |
| "'>" |
| "</iframe>"); |
| main_document_response.Done(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // Check Javascript was blocked the first time. |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // 3) The iframe navigates elsewhere. |
| shell()->LoadURLForFrame(url_b, "theiframe", |
| ui::PAGE_TRANSITION_MANUAL_SUBFRAME); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Refused to execute inline script *"); |
| |
| // 4) The iframe navigates back to about:srcdoc. |
| web_contents()->GetController().GoBack(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // Check Javascript was blocked the second time. |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, |
| SrcDocCSPInheritedAfterCrossSiteHistoryNavigation) { |
| using Response = net::test_server::ControllableHttpResponse; |
| Response main_document_response(embedded_test_server(), "/main_document"); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a = embedded_test_server()->GetURL("a.com", "/main_document"); |
| GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Refused to execute inline script *"); |
| |
| // 1) Load main document with CSP: script-src 'none' |
| // 2) Open an about:srcdoc iframe. It inherits the CSP from its parent. |
| shell()->LoadURL(url_a); |
| main_document_response.WaitForRequest(); |
| main_document_response.Send( |
| "HTTP/1.1 200 OK\n" |
| "content-type: text/html; charset=UTF-8\n" |
| "Content-Security-Policy: script-src 'none'\n" |
| "\n" |
| "<iframe name='theiframe' srcdoc='" |
| " <script>" |
| " console.error(\"CSP failure\");" |
| " </script>" |
| "'>" |
| "</iframe>"); |
| main_document_response.Done(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // Check Javascript was blocked the first time. |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // 3) The iframe navigates elsewhere. |
| shell()->LoadURLForFrame(url_b, "theiframe", |
| ui::PAGE_TRANSITION_MANUAL_SUBFRAME); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Refused to execute inline script *"); |
| |
| // 4) The iframe navigates back to about:srcdoc. |
| web_contents()->GetController().GoBack(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| // Check Javascript was blocked the second time. |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| } |
| |
| // Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual |
| // value of RenderFrameHost::GetPageUkmSourceId() --- unremarkable top-level |
| // navigation case. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| NavigationRequest_GetNextPageUkmSourceId_Basic) { |
| const GURL kUrl(embedded_test_server()->GetURL("/title1.html")); |
| TestNavigationManager manager(web_contents(), kUrl); |
| shell()->LoadURL(kUrl); |
| |
| EXPECT_TRUE(manager.WaitForRequestStart()); |
| ASSERT_TRUE(main_frame()->navigation_request()); |
| |
| ukm::SourceId nav_request_id = |
| main_frame()->navigation_request()->GetNextPageUkmSourceId(); |
| |
| EXPECT_TRUE(manager.WaitForResponse()); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), nav_request_id); |
| } |
| |
| // Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual |
| // value of RenderFrameHost::GetPageUkmSourceId() --- child frame case. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| NavigationRequest_GetNextPageUkmSourceId_ChildFrame) { |
| const GURL kUrl( |
| embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html")); |
| const GURL kDestUrl(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), kUrl)); |
| FrameTreeNode* subframe = main_frame()->child_at(0); |
| ASSERT_TRUE(subframe); |
| |
| TestNavigationManager manager(web_contents(), kDestUrl); |
| EXPECT_TRUE( |
| ExecJs(subframe, JsReplace("location.href = $1", kDestUrl.spec()))); |
| EXPECT_TRUE(manager.WaitForRequestStart()); |
| ASSERT_TRUE(subframe->navigation_request()); |
| |
| ukm::SourceId nav_request_id = |
| subframe->navigation_request()->GetNextPageUkmSourceId(); |
| |
| EXPECT_TRUE(manager.WaitForResponse()); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| |
| // Should have the same page UKM ID in navigation as page post commit, and as |
| // the top-level frame. |
| EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), nav_request_id); |
| EXPECT_EQ(subframe->current_frame_host()->GetPageUkmSourceId(), |
| nav_request_id); |
| } |
| |
| // Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual |
| // value of RenderFrameHost::GetPageUkmSourceId() --- same document navigation. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| NavigationRequest_GetNextPageUkmSourceId_SameDocument) { |
| const GURL kUrl(embedded_test_server()->GetURL("/title1.html")); |
| const GURL kFragment(kUrl.Resolve("#here")); |
| EXPECT_TRUE(NavigateToURL(shell(), kUrl)); |
| |
| NavigationHandleObserver handle_observer(web_contents(), kFragment); |
| EXPECT_TRUE( |
| ExecJs(main_frame(), JsReplace("location.href = $1", kFragment.spec()))); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| EXPECT_TRUE(handle_observer.is_same_document()); |
| EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), |
| handle_observer.next_page_ukm_source_id()); |
| } |
| |
| // Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual |
| // value of RenderFrameHost::GetPageUkmSourceId() --- back navigation; |
| // this case matters because of back-forward cache. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| NavigationRequest_GetNextPageUkmSourceId_Back) { |
| const GURL kUrl1(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| const GURL kUrl2(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), kUrl1)); |
| EXPECT_TRUE(NavigateToURL(shell(), kUrl2)); |
| |
| NavigationHandleObserver handle_observer(web_contents(), kUrl1); |
| web_contents()->GetController().GoBack(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), |
| handle_observer.next_page_ukm_source_id()); |
| } |
| |
| // Tests for cookies. Provides an HTTPS server. |
| class NavigationCookiesBrowserTest : public NavigationBaseBrowserTest { |
| protected: |
| NavigationCookiesBrowserTest() = default; |
| net::EmbeddedTestServer* https_server() { return &https_server_; } |
| |
| private: |
| void SetUpOnMainThread() override { |
| NavigationBaseBrowserTest::SetUpOnMainThread(); |
| mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); |
| https_server()->AddDefaultHandlers(GetTestDataFilePath()); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| NavigationBaseBrowserTest::SetUpCommandLine(command_line); |
| mock_cert_verifier_.SetUpCommandLine(command_line); |
| } |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| NavigationBaseBrowserTest::SetUpInProcessBrowserTestFixture(); |
| mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); |
| } |
| |
| void TearDownInProcessBrowserTestFixture() override { |
| NavigationBaseBrowserTest::TearDownInProcessBrowserTestFixture(); |
| mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); |
| } |
| |
| content::ContentMockCertVerifier mock_cert_verifier_; |
| net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; |
| }; |
| |
| // Test how cookies are inherited in about:srcdoc iframes. |
| // |
| // Regression test: https://crbug.com/1003167. |
| // Test is flaky on all platforms: https://crbug.com/339033006 |
| IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, |
| DISABLED_CookiesInheritedSrcDoc) { |
| using Response = net::test_server::ControllableHttpResponse; |
| Response response_1(https_server(), "/response_1"); |
| Response response_2(https_server(), "/response_2"); |
| Response response_3(https_server(), "/response_3"); |
| |
| ASSERT_TRUE(https_server()->Start()); |
| |
| GURL url_a(https_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(https_server()->GetURL("b.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| let iframe = document.createElement("iframe"); |
| iframe.srcdoc = "foo"; |
| document.body.appendChild(iframe); |
| )")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| RenderFrameHostImpl* main_document = current_frame_host(); |
| RenderFrameHostImpl* sub_document_1 = |
| main_document->child_at(0)->current_frame_host(); |
| EXPECT_EQ(url::kAboutSrcdocURL, sub_document_1->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(url_a), |
| sub_document_1->GetLastCommittedOrigin()); |
| EXPECT_EQ(main_document->GetSiteInstance(), |
| sub_document_1->GetSiteInstance()); |
| |
| // 0. The default state doesn't contain any cookies. |
| EXPECT_EQ("", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 1. Set a cookie in the main document, it affects its child too. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';")); |
| |
| EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 2. Set a cookie in the child, it affects its parent too. |
| EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';")); |
| |
| EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 3. Checks cookies are sent while requesting resources. |
| ExecuteScriptAsync(sub_document_1, "fetch('/response_1');"); |
| response_1.WaitForRequest(); |
| EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie")); |
| |
| // 4. Navigate the iframe elsewhere. |
| EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b))); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| RenderFrameHostImpl* sub_document_2 = |
| main_document->child_at(0)->current_frame_host(); |
| |
| EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 5. Set a cookie in the main document. It doesn't affect its child. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 6. Set a cookie in the child. It doesn't affect its parent. |
| EXPECT_TRUE(ExecJs(sub_document_2, |
| "document.cookie = 'd=0; SameSite=none; Secure';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 7. Checks cookies are sent while requesting resources. |
| ExecuteScriptAsync(sub_document_2, "fetch('/response_2');"); |
| response_2.WaitForRequest(); |
| EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie")); |
| |
| // 8. Navigate the iframe back to about:srcdoc. |
| web_contents()->GetController().GoBack(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| RenderFrameHostImpl* sub_document_3 = |
| main_document->child_at(0)->current_frame_host(); |
| EXPECT_EQ(url_a, main_document->GetLastCommittedURL()); |
| EXPECT_EQ(url::kAboutSrcdocURL, sub_document_3->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(url_a), |
| sub_document_3->GetLastCommittedOrigin()); |
| EXPECT_EQ(main_document->GetSiteInstance(), |
| sub_document_3->GetSiteInstance()); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie")); |
| |
| // 9. Set cookie in the main document. It should be inherited by the child. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie")); |
| |
| // 11. Set cookie in the child document. It should be reflected on its parent. |
| EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| EvalJs(sub_document_3, "document.cookie")); |
| |
| // 12. Checks cookies are sent while requesting resources. |
| ExecuteScriptAsync(sub_document_3, "fetch('/response_3');"); |
| response_3.WaitForRequest(); |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| response_3.http_request()->headers.at("Cookie")); |
| } |
| |
| // Test how cookies are inherited in about:blank iframes. |
| IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, |
| CookiesInheritedAboutBlank) { |
| // This test expects several cross-site navigation to happen. |
| if (!AreAllSitesIsolatedForTesting()) |
| return; |
| |
| using Response = net::test_server::ControllableHttpResponse; |
| Response response_1(https_server(), "/response_1"); |
| Response response_2(https_server(), "/response_2"); |
| Response response_3(https_server(), "/response_3"); |
| |
| ASSERT_TRUE(https_server()->Start()); |
| |
| GURL url_a(https_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(https_server()->GetURL("b.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| EXPECT_TRUE( |
| ExecJs(shell(), JsReplace("let iframe = document.createElement('iframe');" |
| "iframe.src = $1;" |
| "document.body.appendChild(iframe);", |
| url_b))); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| document.querySelector('iframe').src = "about:blank" |
| )")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| RenderFrameHostImpl* main_document = current_frame_host(); |
| RenderFrameHostImpl* sub_document_1 = |
| main_document->child_at(0)->current_frame_host(); |
| |
| EXPECT_EQ(url::kAboutBlankURL, sub_document_1->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(url_a), |
| sub_document_1->GetLastCommittedOrigin()); |
| EXPECT_EQ(main_document->GetSiteInstance(), |
| sub_document_1->GetSiteInstance()); |
| |
| // 0. The default state doesn't contain any cookies. |
| EXPECT_EQ("", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 1. Set a cookie in the main document, it affects its child too. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';")); |
| |
| EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 2. Set a cookie in the child, it affects its parent too. |
| EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';")); |
| |
| EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 3. Checks cookies are sent while requesting resources. |
| GURL url_response_1 = https_server()->GetURL("a.com", "/response_1"); |
| ExecuteScriptAsync(sub_document_1, JsReplace("fetch($1)", url_response_1)); |
| response_1.WaitForRequest(); |
| EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie")); |
| |
| // 4. Navigate the iframe elsewhere. |
| EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b))); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| RenderFrameHostImpl* sub_document_2 = |
| main_document->child_at(0)->current_frame_host(); |
| |
| EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 5. Set a cookie in the main document. It doesn't affect its child. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 6. Set a cookie in the child. It doesn't affect its parent. |
| EXPECT_TRUE(ExecJs(sub_document_2, |
| "document.cookie = 'd=0; SameSite=none; Secure';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 7. Checks cookies are sent while requesting resources. |
| ExecuteScriptAsync(sub_document_2, "fetch('/response_2');"); |
| response_2.WaitForRequest(); |
| EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie")); |
| |
| // 8. Navigate the iframe back to about:blank. |
| web_contents()->GetController().GoBack(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| RenderFrameHostImpl* sub_document_3 = |
| main_document->child_at(0)->current_frame_host(); |
| EXPECT_EQ(url_a, main_document->GetLastCommittedURL()); |
| EXPECT_EQ(url::kAboutBlankURL, sub_document_3->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(url_a), |
| sub_document_3->GetLastCommittedOrigin()); |
| EXPECT_EQ(main_document->GetSiteInstance(), |
| sub_document_3->GetSiteInstance()); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie")); |
| |
| // 9. Set cookie in the main document. It affects the iframe. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie")); |
| |
| // 10. Set cookie in the iframe. It affects the main frame. |
| EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';")); |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| EvalJs(sub_document_3, "document.cookie")); |
| |
| // 11. Even if document.cookie is empty, cookies are sent. |
| ExecuteScriptAsync(sub_document_3, "fetch('/response_3');"); |
| response_3.WaitForRequest(); |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| response_3.http_request()->headers.at("Cookie")); |
| } |
| |
| // Test how cookies are inherited in about:blank iframes. |
| // |
| // This is a variation of |
| // NavigationCookiesBrowserTest.CookiesInheritedAboutBlank. Instead of |
| // requesting an history navigation, a new navigation is requested from the main |
| // frame. The navigation is cross-site instead of being same-site. |
| IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, |
| CookiesInheritedAboutBlank2) { |
| // This test expects several cross-site navigation to happen. |
| if (!AreAllSitesIsolatedForTesting()) |
| return; |
| |
| using Response = net::test_server::ControllableHttpResponse; |
| Response response_1(https_server(), "/response_1"); |
| Response response_2(https_server(), "/response_2"); |
| Response response_3(https_server(), "/response_3"); |
| |
| ASSERT_TRUE(https_server()->Start()); |
| |
| GURL url_a(https_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(https_server()->GetURL("b.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| EXPECT_TRUE( |
| ExecJs(shell(), JsReplace("let iframe = document.createElement('iframe');" |
| "iframe.src = $1;" |
| "document.body.appendChild(iframe);", |
| url_b))); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| document.querySelector('iframe').src = "about:blank" |
| )")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| RenderFrameHostImpl* main_document = current_frame_host(); |
| RenderFrameHostImpl* sub_document_1 = |
| main_document->child_at(0)->current_frame_host(); |
| EXPECT_EQ(url::kAboutBlankURL, sub_document_1->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(url_a), |
| sub_document_1->GetLastCommittedOrigin()); |
| EXPECT_EQ(main_document->GetSiteInstance(), |
| sub_document_1->GetSiteInstance()); |
| |
| // 0. The default state doesn't contain any cookies. |
| EXPECT_EQ("", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 1. Set a cookie in the main document, it affects its child too. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';")); |
| |
| EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 2. Set a cookie in the child, it affects its parent too. |
| EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';")); |
| |
| EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie")); |
| |
| // 3. Checks cookies are sent while requesting resources. |
| ExecuteScriptAsync(sub_document_1, "fetch('/response_1');"); |
| response_1.WaitForRequest(); |
| EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie")); |
| |
| // 4. Navigate the iframe elsewhere. |
| EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b))); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| RenderFrameHostImpl* sub_document_2 = |
| main_document->child_at(0)->current_frame_host(); |
| |
| EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 5. Set a cookie in the main document. It doesn't affect its child. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 6. Set a cookie in the child. It doesn't affect its parent. |
| EXPECT_TRUE(ExecJs(sub_document_2, |
| "document.cookie = 'd=0; SameSite=none; Secure';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie")); |
| |
| // 7. Checks cookies are sent while requesting resources. |
| ExecuteScriptAsync(sub_document_2, "fetch('/response_2');"); |
| response_2.WaitForRequest(); |
| EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie")); |
| |
| // 8. Ask the top-level, a.com frame to navigate the subframe to about:blank. |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| document.querySelector('iframe').src = "about:blank"; |
| )")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| RenderFrameHostImpl* sub_document_3 = |
| main_document->child_at(0)->current_frame_host(); |
| EXPECT_EQ(url::kAboutBlankURL, sub_document_3->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(url_a), |
| sub_document_3->GetLastCommittedOrigin()); |
| EXPECT_EQ(main_document->GetSiteInstance(), |
| sub_document_3->GetSiteInstance()); |
| |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie")); |
| |
| // 9. Set cookie in the main document. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie")); |
| |
| // 10. Set cookie in the child document. |
| EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';")); |
| |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| EvalJs(main_document, "document.cookie")); |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| EvalJs(sub_document_3, "document.cookie")); |
| |
| // 11. Checks cookies are sent while requesting resources. |
| ExecuteScriptAsync(sub_document_3, "fetch('/response_3');"); |
| response_3.WaitForRequest(); |
| EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", |
| response_3.http_request()->headers.at("Cookie")); |
| } |
| |
| // Test how cookies are inherited in data-URL iframes. |
| IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, CookiesInheritedDataUrl) { |
| using Response = net::test_server::ControllableHttpResponse; |
| Response response_1(https_server(), "/response_1"); |
| Response response_2(https_server(), "/response_2"); |
| Response response_3(https_server(), "/response_3"); |
| |
| ASSERT_TRUE(https_server()->Start()); |
| |
| GURL url_a(https_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(https_server()->GetURL("b.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| let iframe = document.createElement("iframe"); |
| iframe.src = "data:text/html,"; |
| document.body.appendChild(iframe); |
| )")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| RenderFrameHostImpl* main_document = current_frame_host(); |
| RenderFrameHostImpl* sub_document_1 = |
| main_document->child_at(0)->current_frame_host(); |
| EXPECT_EQ("data:text/html,", sub_document_1->GetLastCommittedURL()); |
| EXPECT_TRUE(sub_document_1->GetLastCommittedOrigin().opaque()); |
| if (ShouldCreateSiteInstanceForDataUrls()) { |
| EXPECT_NE(main_document->GetSiteInstance(), |
| sub_document_1->GetSiteInstance()); |
| EXPECT_EQ(main_document->GetSiteInstance()->group(), |
| sub_document_1->GetSiteInstance()->group()); |
| } else { |
| EXPECT_EQ(main_document->GetSiteInstance(), |
| sub_document_1->GetSiteInstance()); |
| } |
| |
| // 1. Writing a cookie inside a data-URL document is forbidden. |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "*Failed to set the 'cookie' property on 'Document': Cookies are " |
| "disabled inside 'data:' URLs.*"); |
| ExecuteScriptAsync(sub_document_1, "document.cookie = 'a=0';"); |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // 2. Reading a cookie inside a data-URL document is forbidden. |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "*Failed to read the 'cookie' property from 'Document': Cookies are " |
| "disabled inside 'data:' URLs.*"); |
| ExecuteScriptAsync(sub_document_1, "document.cookie"); |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // 3. Set cookie in the main document. No cookies are sent when requested from |
| // the data-URL. |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0;SameSite=Lax'")); |
| EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'b=0;SameSite=Strict'")); |
| GURL url_response_1 = https_server()->GetURL("a.com", "/response_1"); |
| ExecuteScriptAsync(sub_document_1, JsReplace("fetch($1)", url_response_1)); |
| response_1.WaitForRequest(); |
| EXPECT_EQ(0u, response_1.http_request()->headers.count("Cookie")); |
| |
| // 4. Navigate the iframe elsewhere and back using history navigation. |
| EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b))); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| web_contents()->GetController().GoBack(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| RenderFrameHostImpl* sub_document_2 = |
| main_document->child_at(0)->current_frame_host(); |
| EXPECT_EQ(url_a, main_document->GetLastCommittedURL()); |
| EXPECT_EQ("data:text/html,", sub_document_2->GetLastCommittedURL()); |
| EXPECT_TRUE(sub_document_2->GetLastCommittedOrigin().opaque()); |
| if (ShouldCreateSiteInstanceForDataUrls()) { |
| EXPECT_NE(main_document->GetSiteInstance(), |
| sub_document_2->GetSiteInstance()); |
| EXPECT_EQ(main_document->GetSiteInstance()->GetSiteInstanceGroupId(), |
| sub_document_2->GetSiteInstance()->GetSiteInstanceGroupId()); |
| } else { |
| EXPECT_EQ(main_document->GetSiteInstance(), |
| sub_document_2->GetSiteInstance()); |
| } |
| |
| // 5. Writing a cookie inside a data-URL document is still forbidden. |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "*Failed to set the 'cookie' property on 'Document': Cookies are " |
| "disabled inside 'data:' URLs.*"); |
| ExecuteScriptAsync(sub_document_2, "document.cookie = 'c=0';"); |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // 6. Reading a cookie inside a data-URL document is still forbidden. |
| { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "*Failed to read the 'cookie' property from 'Document': Cookies are " |
| "disabled inside 'data:' URLs.*"); |
| ExecuteScriptAsync(sub_document_2, "document.cookie"); |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // 7. No cookies are sent when requested from the data-URL. |
| GURL url_response_2 = https_server()->GetURL("a.com", "/response_2"); |
| ExecuteScriptAsync(sub_document_2, JsReplace("fetch($1)", url_response_2)); |
| response_2.WaitForRequest(); |
| EXPECT_EQ(0u, response_2.http_request()->headers.count("Cookie")); |
| } |
| |
| // Tests for validating URL rewriting behavior like chrome://newtab to |
| // chrome-native://newtab. |
| class NavigationUrlRewriteBrowserTest : public NavigationBaseBrowserTest { |
| protected: |
| static constexpr const char* kRewriteURL = "http://a.com/rewrite"; |
| static constexpr const char* kNoAccessScheme = "no-access"; |
| static constexpr const char* kNoAccessURL = "no-access://testing/"; |
| |
| class BrowserClient : public ContentBrowserTestContentBrowserClient { |
| public: |
| void BrowserURLHandlerCreated(BrowserURLHandler* handler) override { |
| handler->AddHandlerPair(RewriteUrl, |
| BrowserURLHandlerImpl::null_handler()); |
| fake_url_loader_factory_ = std::make_unique<FakeNetworkURLLoaderFactory>( |
| "HTTP/1.1 200 OK\nContent-Type: text/html\n\n", "This is a test", |
| /* network_accessed */ true, net::OK); |
| } |
| |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> |
| CreateNonNetworkNavigationURLLoaderFactory( |
| const std::string& scheme, |
| FrameTreeNodeId frame_tree_node_id) override { |
| if (scheme == kNoAccessScheme) { |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote; |
| fake_url_loader_factory_->Clone( |
| pending_remote.InitWithNewPipeAndPassReceiver()); |
| return pending_remote; |
| } |
| return {}; |
| } |
| |
| static bool RewriteUrl(GURL* url, BrowserContext* browser_context) { |
| if (*url == GURL(kRewriteURL)) { |
| *url = GURL(kNoAccessURL); |
| return true; |
| } |
| return false; |
| } |
| |
| private: |
| std::unique_ptr<FakeNetworkURLLoaderFactory> fake_url_loader_factory_; |
| }; |
| |
| NavigationUrlRewriteBrowserTest() { |
| url::AddStandardScheme(kNoAccessScheme, url::SCHEME_WITH_HOST); |
| url::AddNoAccessScheme(kNoAccessScheme); |
| |
| // This test needs to use an unassigned SiteInstance for kNoAccessScheme, |
| // which requires adding it as an empty document scheme. |
| url::AddEmptyDocumentScheme(kNoAccessScheme); |
| } |
| |
| void SetUpOnMainThread() override { |
| NavigationBaseBrowserTest::SetUpOnMainThread(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| browser_client_ = std::make_unique<BrowserClient>(); |
| } |
| |
| void TearDownOnMainThread() override { |
| browser_client_.reset(); |
| |
| NavigationBaseBrowserTest::TearDownOnMainThread(); |
| } |
| |
| GURL GetRewriteToNoAccessURL() const { return GURL(kRewriteURL); } |
| |
| private: |
| std::unique_ptr<BrowserClient> browser_client_; |
| url::ScopedSchemeRegistryForTests scoped_registry_; |
| }; |
| |
| // Tests navigating to a URL that gets rewritten to a "no access" URL. This |
| // mimics the behavior of navigating to special URLs like chrome://newtab and |
| // chrome://history which get rewritten to "no access" chrome-native:// URLs. |
| IN_PROC_BROWSER_TEST_F(NavigationUrlRewriteBrowserTest, RewriteToNoAccess) { |
| // Perform an initial navigation. |
| { |
| TestNavigationObserver observer(web_contents()); |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| EXPECT_EQ(url, observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_FALSE(observer.last_initiator_origin().has_value()); |
| } |
| |
| // Navigate to the URL that will get rewritten to a "no access" URL. |
| { |
| TestNavigationObserver observer(web_contents()); |
| |
| // Note: We are using LoadURLParams here because we need to have the |
| // initiator_origin set and NavigateToURL() does not do that. |
| NavigationController::LoadURLParams params(GetRewriteToNoAccessURL()); |
| params.initiator_origin = current_frame_host()->GetLastCommittedOrigin(); |
| web_contents()->GetController().LoadURLWithParams(params); |
| web_contents()->Focus(); |
| observer.Wait(); |
| |
| EXPECT_EQ(GURL(kNoAccessURL), observer.last_navigation_url()); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_TRUE(observer.last_initiator_origin().has_value()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentNavigation) { |
| WebContents* wc = shell()->web_contents(); |
| GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#frag1"); |
| GURL url2 = embedded_test_server()->GetURL("a.com", "/title1.html#frag2"); |
| NavigationHandleCommitObserver navigation_0(wc, url1); |
| NavigationHandleCommitObserver navigation_1(wc, url2); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url1)); |
| NavigationEntry* entry = |
| web_contents()->GetController().GetLastCommittedEntry(); |
| EXPECT_TRUE(NavigateToURL(shell(), url2)); |
| // The NavigationEntry changes on a same-document navigation. |
| EXPECT_NE(web_contents()->GetController().GetLastCommittedEntry(), entry); |
| |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| EXPECT_TRUE(navigation_1.was_same_document()); |
| } |
| |
| // Some navigations are not allowed, such as when they fail the content security |
| // policy, or for trying to load about:srcdoc in the main frame. These result in |
| // us redirecting the navigation to an error page via |
| // RenderFrameHostImpl::FailedNavigation(). |
| // Repeating the request with a different URL fragment results in attempting a |
| // same-document navigation, but error pages do not support such navigations. In |
| // this case treat each failed navigation request as a separate load, with the |
| // resulting navigation being performed as a cross-document navigation. This is |
| // regression test for https://crbug.com/1018385. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentNavigationOnBlockedPage) { |
| GURL url1("about:srcdoc#0"); |
| GURL url2("about:srcdoc#1"); |
| NavigationHandleCommitObserver navigation_0(web_contents(), url1); |
| NavigationHandleCommitObserver navigation_1(web_contents(), url2); |
| |
| // Big warning: about:srcdoc is not supposed to be valid browser-initiated |
| // main-frame navigation, it is currently blocked by the NavigationRequest. |
| // It is used here to reproduce bug https://crbug.com/1018385. Please avoid |
| // copying this kind of navigation in your own tests. |
| EXPECT_FALSE(NavigateToURL(shell(), url1)); |
| EXPECT_FALSE(NavigateToURL(shell(), url2)); |
| |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| EXPECT_FALSE(navigation_1.was_same_document()); |
| } |
| |
| // This navigation is allowed by the browser, but the network will not be able |
| // to connect to the site, so the NavigationRequest fails on the browser side |
| // and is redirected to an error page. Performing another navigation should |
| // make the full attempt again, in case the network request succeeds this time. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentNavigationOnBadServerErrorPage) { |
| GURL url1("http://badserver.com:9/"); |
| GURL url2("http://badserver.com:9/#1"); |
| NavigationHandleCommitObserver navigation_0(web_contents(), url1); |
| NavigationHandleCommitObserver navigation_1(web_contents(), url2); |
| |
| // The navigation is okay from the browser's perspective, so NavigateToURL() |
| // will return true. But the network request ultimately fails, so the request |
| // is redirected to an error page. |
| EXPECT_FALSE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| |
| // The 2nd request shares a URL but it should be another cross-document |
| // navigation, rather than trying to navigate inside the error page. |
| EXPECT_FALSE(NavigateToURL(shell(), url2)); |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_FALSE(navigation_1.was_same_document()); |
| } |
| |
| // This navigation is allowed by the browser, and the request to the server is |
| // successful, but it returns 404 error headers, and (optionally) an error page. |
| // When another request is made for the same page but with a different fragment, |
| // the browser will attempt to perform a same-document navigation but that |
| // navigation is intended for the actual document not the error page that has |
| // been loaded instead. A same-document navigation in the renderer-loaded error |
| // page should be performed as a cross-document navigation in order to attempt |
| // to reload the page. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentNavigationOn404ErrorPage) { |
| // This case is a non-empty 404 page. It makes different choices about where |
| // to load the page on a same-document navigation. |
| { |
| GURL url1 = embedded_test_server()->GetURL("a.com", "/page404.html"); |
| GURL url2 = embedded_test_server()->GetURL("a.com", "/page404.html#1"); |
| NavigationHandleCommitObserver navigation_0(web_contents(), url1); |
| NavigationHandleCommitObserver navigation_1(web_contents(), url2); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| |
| // This is another navigation to the non-existent URL, but with a different |
| // fragment. We have successfully loaded content from a.com. The fact that |
| // it is 404 response does not mean it is an error page, since the term |
| // "error page" is used for cases where the browser encounters an error |
| // loading a document from the origin. HTTP responses with >400 status codes |
| // are just like regular documents from the origin and we render their |
| // response body just like we would a 200 response. This is why it can make |
| // sense for a same document navigation to be performed from a 404 page. |
| EXPECT_TRUE(NavigateToURL(shell(), url2)); |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_TRUE(navigation_1.was_same_document()); |
| } |
| // This case is an empty 404 page. It makes different choices about where |
| // to load the page on a same-document navigation. Since the server has only |
| // replied with an error, the browser will display its own error page and |
| // therefore it is not one coming from the server's origin. |
| { |
| GURL url1 = embedded_test_server()->GetURL("a.com", "/empty404.html"); |
| GURL url2 = embedded_test_server()->GetURL("a.com", "/empty404.html#1"); |
| NavigationHandleCommitObserver navigation_0(web_contents(), url1); |
| NavigationHandleCommitObserver navigation_1(web_contents(), url2); |
| |
| EXPECT_FALSE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| |
| // This is another navigation to the non-existent URL, but with a different |
| // fragment. Since we did not load a document from the server (we got |
| // `false` from `NavigateToURL()`) there is no server-provided document to |
| // navigate within. The result should be a cross-document navigation in |
| // order to attempt to load the document at the given path from the server |
| // again. |
| EXPECT_FALSE(NavigateToURL(shell(), url2)); |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_FALSE(navigation_1.was_same_document()); |
| } |
| |
| // This case is also an empty 404 page, but we do replaceState and pushState |
| // afterwards, creating successful same-document navigations. |
| { |
| // Navigate to empty 404, committing an error page. |
| GURL url1 = embedded_test_server()->GetURL("a.com", "/empty404.html"); |
| NavigationHandleCommitObserver navigation(web_contents(), url1); |
| EXPECT_FALSE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(navigation.has_committed()); |
| EXPECT_FALSE(navigation.was_same_document()); |
| |
| // replaceState on an error page, without changing the URL. |
| { |
| FrameNavigateParamsCapturer capturer(main_frame()); |
| capturer.set_wait_for_load(false); |
| EXPECT_TRUE(ExecJs(shell(), "history.replaceState('foo', '')")); |
| capturer.Wait(); |
| EXPECT_TRUE(capturer.is_same_document()); |
| } |
| |
| // pushState on an error page, without changing the URL. |
| { |
| FrameNavigateParamsCapturer capturer(main_frame()); |
| capturer.set_wait_for_load(false); |
| EXPECT_TRUE(ExecJs(shell(), "history.pushState('foo', '')")); |
| capturer.Wait(); |
| EXPECT_TRUE(capturer.is_same_document()); |
| } |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentNavigationFromCrossDocumentRedirect) { |
| WebContents* wc = shell()->web_contents(); |
| GURL url0 = embedded_test_server()->GetURL("/title1.html#frag1"); |
| GURL url1 = |
| embedded_test_server()->GetURL("/server-redirect?title1.html#frag2"); |
| GURL url2 = embedded_test_server()->GetURL("/title1.html#frag2"); |
| NavigationHandleCommitObserver navigation_0(wc, url0); |
| NavigationHandleCommitObserver navigation_1(wc, url1); |
| NavigationHandleCommitObserver navigation_2(wc, url2); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url0)); |
| // Since the redirect does not land at the URL we passed in, we get a false |
| // return here. |
| EXPECT_FALSE(NavigateToURL(shell(), url1)); |
| |
| // The navigation to |url1| is redirected and so |url1| does not commit. Then |
| // the resulting navigation to |url2| lands at the same document URL as |url0| |
| // which would be a same-document navigation if there wasn't a redirect |
| // involved. But since it started as a cross-document navigation it results in |
| // loading a new document instead of doing a same-document navigation. |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_FALSE(navigation_1.has_committed()); |
| EXPECT_TRUE(navigation_2.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| EXPECT_FALSE(navigation_1.was_same_document()); |
| EXPECT_FALSE(navigation_2.was_same_document()); |
| |
| EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url2); |
| |
| // Redirect should not record a ReceivedResponse event. |
| EXPECT_EQ(1u, test_ukm_recorder() |
| .GetEntriesByName("Navigation.ReceivedResponse") |
| .size()); |
| } |
| |
| // 1. The browser navigates to a.html. |
| // 2. The renderer uses history.pushState() to change the URL of the current |
| // document from a.html to b.html. |
| // 3. The browser tries to perform a same-document navigation to a.html#foo, |
| // since it did not hear about the document's URL changing yet. When it gets |
| // to the renderer, we discover a race has happened. |
| // 4. Meanwhile, the browser hears about the URL change to b.html and applies |
| // it. |
| // Now - how do we resolve the race? |
| // 5. We will reorder the a.html#foo navigation to start over in the browser |
| // after the b.html navigation. |
| // Technically, this is still a same-document navigation! The URL changed but |
| // the document did not. Currently, however, the browser only considers the URL |
| // when performing a non-history navigation to decide if it's a same-document |
| // navigation, so.. |
| // 6. The browser will perform a cross-document navigation to a.html#foo. |
| // |
| // TODO(crbug.com/40799231): Test is flaky on various platforms. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| DISABLED_SameDocumentNavigationRacesPushStateURLChange) { |
| WebContents* wc = shell()->web_contents(); |
| GURL url0 = embedded_test_server()->GetURL("/title1.html"); |
| GURL url1 = embedded_test_server()->GetURL("/title2.html"); |
| GURL url2 = embedded_test_server()->GetURL("/title1.html#frag2"); |
| NavigationHandleCommitObserver navigation_0(wc, url0); |
| NavigationHandleCommitObserver navigation_1(wc, url1); |
| NavigationHandleCommitObserver navigation_2(wc, url2); |
| |
| // Start at `url0`. |
| EXPECT_TRUE(NavigateToURL(shell(), url0)); |
| |
| // Have the renderer `history.pushState()` to `url1`, which leaves it on the |
| // `url0` document, but with a different URL now. |
| ExecuteScriptAsync(shell(), JsReplace("history.pushState('', '', $1);" |
| "window.location.href == $1;", |
| url1)); |
| |
| // The browser didn't hear about the change yet. |
| EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url0); |
| |
| { |
| // We will wait for 2 navigations: one will be the pushState() and the other |
| // will be the navigation to `url2` started below. |
| TestNavigationObserver nav_observer(wc, 2); |
| |
| // Start a same-document navigation to url2 that is racing with the |
| // renderer's history.pushState(). |
| shell()->LoadURL(url2); |
| |
| nav_observer.Wait(); |
| } |
| |
| // The last navigation to resolve is the one to `url2` as it's reordered to |
| // come after the race with the already-completed history.pushState(). |
| EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url2); |
| |
| // Navigation 0 was a cross-document navigation, to initially load the |
| // document. |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| |
| // Navigation 1 was a same-document navigation, from the renderer's |
| // history.pushState() call. |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_TRUE(navigation_1.was_same_document()); |
| |
| // Navigation 2 was restarted and came after. When it restarted, it saw the |
| // URL did not match and did a cross-document navigation. Technically the same |
| // document was still loaded from `url0`, but the browser makes its choice |
| // on the document's current URL. |
| EXPECT_TRUE(navigation_2.has_committed()); |
| EXPECT_FALSE(navigation_2.was_same_document()); |
| } |
| |
| class GetEffectiveUrlClient : public ContentBrowserTestContentBrowserClient { |
| public: |
| GURL GetEffectiveURL(content::BrowserContext* browser_context, |
| const GURL& url) override { |
| if (effective_url_) |
| return *effective_url_; |
| return url; |
| } |
| |
| bool IsSuitableHost(RenderProcessHost* process_host, |
| const GURL& site_url) override { |
| if (!disallowed_process_id_) |
| return true; |
| return process_host->GetDeprecatedID() != disallowed_process_id_; |
| } |
| |
| void set_effective_url(const GURL& url) { effective_url_ = url; } |
| |
| void set_disallowed_process(int id) { disallowed_process_id_ = id; } |
| |
| private: |
| std::optional<GURL> effective_url_; |
| int disallowed_process_id_ = 0; |
| }; |
| |
| // While a document is open, state in the browser may change such that loading |
| // the document would choose a different SiteInstance. A cross-document |
| // navigation would pick up this different SiteInstance, but a same-document |
| // navigation should not. It should just navigate inside the currently loaded |
| // document instead of reloading the document. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentNavigationWhenSiteInstanceWouldChange) { |
| auto* wc = static_cast<WebContentsImpl*>(shell()->web_contents()); |
| GURL url0 = embedded_test_server()->GetURL("a.com", "/title1.html#ref1"); |
| GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#ref2"); |
| |
| GetEffectiveUrlClient new_client; |
| |
| NavigationHandleCommitObserver navigation_0(wc, url0); |
| EXPECT_TRUE(NavigateToURL(shell(), url0)); |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| |
| RenderFrameHost* main_frame_host = wc->GetPrimaryMainFrame(); |
| RenderProcessHost* main_frame_process_host = main_frame_host->GetProcess(); |
| |
| // When we both change the effective URL and also disallow the current |
| // renderer process, a new load of the current document would get a different |
| // SiteInstance. |
| GURL modified_url0 = |
| embedded_test_server()->GetURL("c.com", "/title1.html#ref1"); |
| new_client.set_effective_url(modified_url0); |
| new_client.set_disallowed_process(main_frame_process_host->GetDeprecatedID()); |
| |
| NavigationHandleCommitObserver navigation_1(wc, url1); |
| EXPECT_TRUE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_TRUE(navigation_1.was_same_document()); |
| |
| // The RenderFrameHost should not have changed, we should perform the |
| // navigation in the currently loaded document. |
| EXPECT_EQ(main_frame_host, wc->GetPrimaryMainFrame()); |
| EXPECT_EQ(main_frame_process_host, wc->GetPrimaryMainFrame()->GetProcess()); |
| } |
| |
| // This tests the same ideas as the above test except in this case the same- |
| // document navigation is done through a history navigation, which exercises |
| // different codepaths in the NavigationControllerImpl. |
| IN_PROC_BROWSER_TEST_F( |
| NavigationBrowserTest, |
| SameDocumentHistoryNavigationWhenSiteInstanceWouldChange) { |
| auto* wc = static_cast<WebContentsImpl*>(shell()->web_contents()); |
| GURL url0 = embedded_test_server()->GetURL("a.com", "/title1.html#ref1"); |
| GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#ref2"); |
| NavigationHandleCommitObserver navigation_0(wc, url0); |
| NavigationHandleCommitObserver navigation_1(wc, url1); |
| |
| GetEffectiveUrlClient new_client; |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url0)); |
| EXPECT_TRUE(navigation_0.has_committed()); |
| EXPECT_FALSE(navigation_0.was_same_document()); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_TRUE(navigation_1.was_same_document()); |
| |
| RenderFrameHost* main_frame_host = wc->GetPrimaryMainFrame(); |
| RenderProcessHost* main_frame_process_host = main_frame_host->GetProcess(); |
| |
| // When we both change the effective URL and also disallow the current |
| // renderer process, a new load of the current document would get a different |
| // SiteInstance. |
| GURL modified_url0 = |
| embedded_test_server()->GetURL("c.com", "/title1.html#ref1"); |
| new_client.set_effective_url(modified_url0); |
| new_client.set_disallowed_process(main_frame_process_host->GetDeprecatedID()); |
| |
| // Navigates to the same-document. Since the SiteInstance changed, we would |
| // normally try isolate this navigation by using a different RenderProcessHost |
| // and RenderFrameHost. But since it is same-document, we want to avoid that |
| // and perform the navigation inside the loaded |url0| document. |
| wc->GetController().GoBack(); |
| EXPECT_TRUE(WaitForLoadStop(wc)); |
| |
| // The RenderFrameHost should not have changed, we should perform the |
| // navigation in the currently loaded document. |
| EXPECT_EQ(main_frame_host, wc->GetPrimaryMainFrame()); |
| EXPECT_EQ(main_frame_process_host, wc->GetPrimaryMainFrame()->GetProcess()); |
| } |
| |
| // Verify that actual renderer-initiated navigations to about:blank#blocked |
| // are respected, even though both the browser and renderer rewrite some illegal |
| // navigations to that URL as well. See https://crbug.com/40066983. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentHashNavigationToBlockedFragmentAllowed) { |
| const GURL url(embedded_test_server()->GetURL("/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| TestNavigationObserver blank_observer(web_contents()); |
| EXPECT_TRUE(ExecJs(shell(), "location.href = 'about:blank';")); |
| blank_observer.Wait(); |
| |
| GURL blocked_url = GURL(kBlockedURL); |
| TestNavigationObserver blocked_observer(web_contents()); |
| EXPECT_TRUE(ExecJs(shell(), JsReplace("location.href = $1", blocked_url))); |
| blocked_observer.Wait(); |
| |
| // If the browser process receives a request to same-document navigate to |
| // about:blank#blocked, the URL should be used and not ignored (as in a |
| // blocked case like the SameDocumentLongURLHashNavigation test). |
| EXPECT_EQ(blocked_url, web_contents()->GetLastCommittedURL()); |
| } |
| |
| // Verify that same-document navigations from about:blank to an excessively long |
| // fragment do not crash the browser. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentAboutBlankLongURLHashNavigation) { |
| const GURL blank_url(url::kAboutBlankURL); |
| EXPECT_TRUE(NavigateToURL(shell(), blank_url)); |
| |
| std::string long_url = "#"; |
| long_url.append(2 * url::kMaxURLChars, 'a'); |
| EXPECT_TRUE(ExecJs(shell(), JsReplace("location = $1", long_url))); |
| |
| // If the renderer attempts to navigate same-document from about:blank to a |
| // too long hash (>2 MB), the URL (and base URL) will be blocked and rewritten |
| // in the renderer to avoid the Mojo serialization limit. Ensure that the |
| // browser process does not crash due to an empty base URL, and that the |
| // blocked URL is used, unlike in the non-about:blank case in the |
| // SameDocumentLongURLHashNavigation test. |
| // TODO(crbug.com/40067230): Ideally this would be blocked earlier in the |
| // renderer process, failing the navigation. |
| EXPECT_EQ(GURL(kBlockedURL), web_contents()->GetLastCommittedURL()); |
| // The renderer process considers the same-document navigation to the long URL |
| // to have successfully completed. |
| EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentLongURLHashNavigation) { |
| const GURL url(embedded_test_server()->GetURL("/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| std::string long_url = "#"; |
| long_url.append(2 * url::kMaxURLChars, 'a'); |
| EXPECT_TRUE(ExecJs(shell(), JsReplace("location = $1", long_url))); |
| |
| // If the browser process receives a request to same-document navigate to a |
| // too long URL (>2 MB), it simply pretends that the renderer performed a |
| // same-document navigation to the currently committed URL (previously, it was |
| // mapped to about:blank#blocked, which could be confusing). |
| // TODO(crbug.com/40067230): Ideally this would be blocked in the renderer |
| // instead of having special browser-side handling. |
| EXPECT_EQ(url, web_contents()->GetLastCommittedURL()); |
| // The renderer process enforces no such limit and should consider the |
| // same-document navigation to have successfully completed. |
| EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentLongURLPushState) { |
| const GURL url(embedded_test_server()->GetURL("/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| std::string long_url = "#"; |
| long_url.append(2 * url::kMaxURLChars, 'a'); |
| EXPECT_TRUE(ExecJs( |
| shell(), JsReplace("history.pushState('state', '', $1)", long_url))); |
| |
| // If the browser process receives a request to same-document navigate to a |
| // too long URL (>2 MB), it simply pretends that the renderer performed a |
| // same-document navigation to the currently committed URL (previously, it was |
| // mapped to about:blank#blocked, which could be confusing). |
| // TODO(crbug.com/40067230): Ideally this would be blocked in the renderer |
| // instead of having special browser-side handling. |
| EXPECT_EQ(url, web_contents()->GetLastCommittedURL()); |
| // The renderer process enforces no such limit and should consider the |
| // same-document navigation to have successfully completed. |
| EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentLongURL204PopupHashNavigation) { |
| const GURL url(embedded_test_server()->GetURL("/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Open a popup window with a navigation that will result in a 204. This will |
| // result in a WebContents where the last committed URL is the empty URL. |
| const GURL nocontent_url(embedded_test_server()->GetURL("/nocontent")); |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", nocontent_url))); |
| Shell* opened_shell = new_shell_observer.GetShell(); |
| EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents())); |
| |
| std::string long_url = "#"; |
| long_url.append(2 * url::kMaxURLChars, 'a'); |
| EXPECT_TRUE(ExecJs(opened_shell, JsReplace("location = $1", long_url))); |
| |
| // Hash navigations in a popup in this state (incorrectly) perform a |
| // cross-document navigation. This is because the check for whether or not to |
| // perform a same-document navigation uses the initial empty Document's actual |
| // URL (which is, surprisingly enough, the empty URL) rather than URL the web |
| // platform generally sees (which is about:blank). As a result, the check ends |
| // up comparing the empty URL against the completed URL of about:blank#..., |
| // which means the URLs are not equal ignoring fragments, and Blink performs a |
| // cross-document navigation instead. |
| // |
| // TODO(crbug.com/40922971): This probably should be fixed to be treated as a |
| // same-document navigation. |
| EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents())); |
| EXPECT_EQ(GURL(kBlockedURL), |
| opened_shell->web_contents()->GetLastCommittedURL()); |
| EXPECT_EQ(kBlockedURL, EvalJs(opened_shell->web_contents(), "location.href")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameDocumentLongURL204PopupPushState) { |
| const GURL url(embedded_test_server()->GetURL("/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Open a popup window with a navigation that will result in a 204. This will |
| // result in a WebContents where the last committed URL is the empty URL. |
| const GURL nocontent_url(embedded_test_server()->GetURL("/nocontent")); |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", nocontent_url))); |
| Shell* opened_shell = new_shell_observer.GetShell(); |
| EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents())); |
| |
| std::string long_url = "#"; |
| long_url.append(2 * url::kMaxURLChars, 'a'); |
| // Blink incorrectly disallows pushState() because the security check is |
| // broken, since the security check uses the initial empty Document's actual |
| // URL (which is, surprisingly enough, the empty URL) rather than the URL the |
| // web platform generally sees (which is about:blank). |
| // |
| // TODO(crbug.com/40922971): This pushState() should probably be allowed. |
| EXPECT_EQ( |
| "SecurityError", |
| EvalJs( |
| opened_shell, |
| JsReplace( |
| "try { history.pushState('state', '', $1) } catch (e) { e.name }", |
| long_url))); |
| } |
| |
| // Ensure that no crash occurs when doing a same-document navigation within a |
| // site-less SiteInstance, such as for a browser-initiated about:blank. |
| // See https://crbug.com/359807735. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentSitelessNavigation) { |
| WebContents* web_contents = shell()->web_contents(); |
| GURL url1 = GURL("about:blank#1"); |
| GURL url2 = GURL("about:blank#2"); |
| NavigationHandleCommitObserver navigation_1(web_contents, url1); |
| NavigationHandleCommitObserver navigation_2(web_contents, url2); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(NavigateToURL(shell(), url2)); |
| |
| EXPECT_TRUE(navigation_1.has_committed()); |
| EXPECT_TRUE(navigation_2.has_committed()); |
| EXPECT_FALSE(navigation_1.was_same_document()); |
| EXPECT_TRUE(navigation_2.was_same_document()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| NonDeterministicUrlRewritesUseLastUrl) { |
| // Lambda expressions cannot be assigned to function pointers if they use |
| // captures, so track how many times the handler is called using a non-const |
| // static variable. |
| static int rewrite_count; |
| rewrite_count = 0; |
| |
| BrowserURLHandler::URLHandler handler_method = |
| [](GURL* url, BrowserContext* browser_context) { |
| GURL::Replacements replace_path; |
| if (rewrite_count > 0) { |
| replace_path.SetPathStr("title2.html"); |
| } else { |
| replace_path.SetPathStr("title1.html"); |
| } |
| *url = url->ReplaceComponents(replace_path); |
| rewrite_count++; |
| return true; |
| }; |
| BrowserURLHandler::GetInstance()->AddHandlerPair( |
| handler_method, BrowserURLHandler::null_handler()); |
| |
| TestNavigationObserver observer(web_contents()); |
| shell()->LoadURL(embedded_test_server()->GetURL("/virtual-url.html")); |
| observer.Wait(); |
| EXPECT_EQ("/title2.html", observer.last_navigation_url().path()); |
| EXPECT_EQ(2, rewrite_count); |
| } |
| |
| // Create two windows. When the second is deleted, it initiates a navigation in |
| // the first. This is a situation where the navigation has an initiator frame |
| // token, but no corresponding RenderFrameHost. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| RendererInitiatedCrossWindowNavigationInPagehide) { |
| GURL url(embedded_test_server()->GetURL("/empty.html")); |
| GURL always_referrer_url(embedded_test_server()->GetURL( |
| "/set-header?Referrer-Policy: unsafe-url")); |
| |
| // Setup the opener window. |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Setup the openee window; |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE( |
| ExecJs(shell(), JsReplace("window.open($1);", always_referrer_url))); |
| Shell* openee_shell = new_shell_observer.GetShell(); |
| EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents())); |
| |
| // When deleted, the openee will initiate a navigation in its opener. |
| EXPECT_TRUE(ExecJs(openee_shell, R"( |
| window.addEventListener("pagehide", () => { |
| opener.location.href = "about:blank"; |
| }) |
| )")); |
| |
| RenderFrameHost* openee_rfh = |
| static_cast<WebContentsImpl*>(openee_shell->web_contents()) |
| ->GetPrimaryMainFrame(); |
| // Issue a KeepAlive for the navigation state so that the PolicyContainerHost |
| // will still exist after the initiator RenderFrameHost is gone. |
| mojo::PendingRemote<blink::mojom::NavigationStateKeepAliveHandle> keep_alive; |
| static_cast<RenderFrameHostImpl*>(openee_rfh) |
| ->IssueKeepAliveHandle(keep_alive.InitWithNewPipeAndPassReceiver()); |
| |
| auto initiator_global_token = openee_rfh->GetGlobalFrameToken(); |
| base::RunLoop loop; |
| DidStartNavigationCallback callback( |
| web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| auto* request = NavigationRequest::From(handle); |
| |
| const std::optional<blink::LocalFrameToken>& frame_token = |
| request->GetInitiatorFrameToken(); |
| EXPECT_TRUE(frame_token.has_value()); |
| EXPECT_EQ(initiator_global_token.frame_token, frame_token.value()); |
| EXPECT_EQ(initiator_global_token.child_id, |
| request->GetInitiatorProcessId()); |
| |
| auto* initiator_rfh = RenderFrameHostImpl::FromFrameToken( |
| request->GetInitiatorProcessId(), *frame_token); |
| ASSERT_FALSE(initiator_rfh); |
| |
| // Even if the initiator RenderFrameHost is gone, its policy container |
| // should still be around since the LocalFrame has not been destroyed |
| // yet. |
| PolicyContainerHost* initiator_policy_container = |
| RenderFrameHostImpl::GetPolicyContainerHost( |
| base::OptionalToPtr(frame_token), |
| request->GetInitiatorProcessId(), |
| web_contents()->GetPrimaryMainFrame()->GetStoragePartition()); |
| ASSERT_TRUE(initiator_policy_container); |
| ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways, |
| initiator_policy_container->referrer_policy()); |
| |
| // Even if the initiator RenderFrameHost is gone, the navigation request |
| // (to "about:blank") should have inherited its policy container. |
| auto* initiator_policies = |
| request->GetInitiatorPolicyContainerPolicies(); |
| ASSERT_TRUE(initiator_policies); |
| ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways, |
| initiator_policies->referrer_policy); |
| |
| loop.Quit(); |
| })); |
| |
| // Delete the openee, which trigger the navigation in the opener. |
| openee_shell->Close(); |
| loop.Run(); |
| } |
| |
| // A document initiates a form submission in another frame, then deletes itself. |
| // Check the initiator frame token. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FormSubmissionThenDeleteFrame) { |
| GURL url(embedded_test_server()->GetURL("/empty.html")); |
| GURL always_referrer_url(embedded_test_server()->GetURL( |
| "/set-header?Referrer-Policy: unsafe-url")); |
| |
| // Setup the opener window. |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Setup the openee window; |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", url))); |
| Shell* openee_shell = new_shell_observer.GetShell(); |
| |
| // Create a 'named' iframe in the first window. This will be the target of the |
| // form submission. |
| EXPECT_TRUE(ExecJs(shell(), R"( |
| new Promise(resolve => { |
| let iframe = document.createElement("iframe"); |
| iframe.onload = resolve; |
| iframe.name = 'form-submission-target'; |
| iframe.src = location.href; |
| document.body.appendChild(iframe); |
| }); |
| )")); |
| |
| // Create an iframe in the second window. It will be initiating a form |
| // submission and removing itself before the scheduled form navigation occurs. |
| // This iframe will have referrer policy "unsafe-url". |
| EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents())); |
| EXPECT_TRUE(ExecJs(openee_shell, JsReplace(R"( |
| new Promise(resolve => { |
| let iframe = document.createElement('iframe'); |
| iframe.onload = resolve; |
| iframe.src = $1; |
| document.body.appendChild(iframe); |
| }); |
| )", |
| always_referrer_url))); |
| EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents())); |
| |
| RenderFrameHost* initiator_rfh = |
| static_cast<WebContentsImpl*>(openee_shell->web_contents()) |
| ->GetPrimaryMainFrame() |
| ->child_at(0) |
| ->current_frame_host(); |
| auto initiator_global_token = initiator_rfh->GetGlobalFrameToken(); |
| base::RunLoop loop; |
| DidStartNavigationCallback callback( |
| web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| auto* request = NavigationRequest::From(handle); |
| ASSERT_TRUE(request->IsPost()); |
| |
| const std::optional<blink::LocalFrameToken>& frame_token = |
| request->GetInitiatorFrameToken(); |
| EXPECT_TRUE(frame_token.has_value()); |
| EXPECT_EQ(initiator_global_token.frame_token, frame_token.value()); |
| EXPECT_EQ(initiator_global_token.child_id, |
| request->GetInitiatorProcessId()); |
| |
| auto* deleted_initiator_rfh = RenderFrameHostImpl::FromFrameToken( |
| request->GetInitiatorProcessId(), frame_token.value()); |
| ASSERT_FALSE(deleted_initiator_rfh); |
| |
| // Even if the initiator RenderFrameHost is gone, its policy container |
| // should still be around since the LocalFrame has not been destroyed |
| // yet. |
| PolicyContainerHost* initiator_policy_container = |
| RenderFrameHostImpl::GetPolicyContainerHost( |
| base::OptionalToPtr(frame_token), |
| request->GetInitiatorProcessId(), |
| web_contents()->GetPrimaryMainFrame()->GetStoragePartition()); |
| ASSERT_TRUE(initiator_policy_container); |
| EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways, |
| initiator_policy_container->referrer_policy()); |
| |
| auto* initiator_policies = |
| request->GetInitiatorPolicyContainerPolicies(); |
| ASSERT_TRUE(initiator_policies); |
| ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways, |
| initiator_policies->referrer_policy); |
| |
| loop.Quit(); |
| })); |
| |
| // Initiate a form submission into the first window and delete the initiator. |
| EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents())); |
| ExecuteScriptAsync(initiator_rfh, R"( |
| let input = document.createElement("input"); |
| input.setAttribute("type", "hidden"); |
| input.setAttribute("name", "my_token"); |
| input.setAttribute("value", "my_value"); |
| |
| // Schedule a form submission navigation (which will occur in a separate |
| // task). |
| let form = document.createElement('form'); |
| form.appendChild(input); |
| form.setAttribute("method", "POST"); |
| form.setAttribute("action", "about:blank"); |
| form.setAttribute("target", "form-submission-target"); |
| document.body.appendChild(form); |
| form.submit(); |
| |
| // Delete this frame before the scheduled navigation occurs in the target |
| // frame. |
| parent.document.querySelector("iframe").remove(); |
| )"); |
| loop.Run(); |
| } |
| |
| // Same as the previous test, but for a remote frame navigation: |
| // A document initiates a form submission in a cross-origin frame, then deletes |
| // itself. Check the initiator frame token. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| FormSubmissionInRemoteFrameThenDeleteFrame) { |
| GURL url(embedded_test_server()->GetURL("/empty.html")); |
| GURL cross_origin_always_referrer_url(embedded_test_server()->GetURL( |
| "foo.com", "/set-header?Referrer-Policy: unsafe-url")); |
| |
| // Setup the main page. |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Create a cross origin child iframe. This iframe will embed another iframe, |
| // which will initiate the navigation. The only purpose of this iframe is to |
| // allow its child to delete itself by issuing |
| // parent.document.querySelector("iframe").remove(); |
| // (The main frame cannot do it because it is cross-origin.) |
| EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| EXPECT_TRUE(ExecJs(shell(), JsReplace(R"( |
| let iframe = document.createElement('iframe'); |
| iframe.src = $1; |
| document.body.appendChild(iframe); |
| )", |
| cross_origin_always_referrer_url))); |
| EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| RenderFrameHostImpl* middle_rfh = |
| current_frame_host()->child_at(0)->current_frame_host(); |
| |
| // Now create a grandchild iframe, which is same-origin with the parent (but |
| // cross-origin with the grandparent). The grandchild will initiate a form |
| // submission in the top frame and remove itself before the scheduled form |
| // navigation occurs. This iframe will have referrer policy "unsafe-url". |
| EXPECT_TRUE(ExecJs(middle_rfh, JsReplace(R"( |
| let iframe = document.createElement('iframe'); |
| iframe.src = $1; |
| document.body.appendChild(iframe); |
| )", |
| cross_origin_always_referrer_url))); |
| EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| RenderFrameHost* initiator_rfh = |
| middle_rfh->child_at(0)->current_frame_host(); |
| auto initiator_global_token = initiator_rfh->GetGlobalFrameToken(); |
| |
| base::RunLoop loop; |
| DidStartNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| auto* request = NavigationRequest::From(handle); |
| ASSERT_TRUE(request->IsPost()); |
| |
| const std::optional<blink::LocalFrameToken>& frame_token = |
| request->GetInitiatorFrameToken(); |
| EXPECT_TRUE(frame_token.has_value()); |
| EXPECT_EQ(initiator_global_token.frame_token, frame_token.value()); |
| EXPECT_EQ(initiator_global_token.child_id, |
| request->GetInitiatorProcessId()); |
| |
| auto* deleted_initiator_rfh = RenderFrameHostImpl::FromFrameToken( |
| request->GetInitiatorProcessId(), frame_token.value()); |
| ASSERT_FALSE(deleted_initiator_rfh); |
| |
| // Even if the initiator RenderFrameHost is gone, its policy container |
| // should still be around since the LocalFrame has not been destroyed |
| // yet. |
| PolicyContainerHost* initiator_policy_container = |
| RenderFrameHostImpl::GetPolicyContainerHost( |
| base::OptionalToPtr(frame_token), |
| request->GetInitiatorProcessId(), |
| web_contents()->GetPrimaryMainFrame()->GetStoragePartition()); |
| ASSERT_TRUE(initiator_policy_container); |
| EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways, |
| initiator_policy_container->referrer_policy()); |
| EXPECT_EQ( |
| network::mojom::ReferrerPolicy::kAlways, |
| request->GetInitiatorPolicyContainerPolicies()->referrer_policy); |
| |
| loop.Quit(); |
| })); |
| |
| // Initiate a form submission into the main frame and delete the initiator. |
| ExecuteScriptAsync(initiator_rfh, R"( |
| let input = document.createElement("input"); |
| input.setAttribute("type", "hidden"); |
| input.setAttribute("name", "my_token"); |
| input.setAttribute("value", "my_value"); |
| |
| // Schedule a form submission navigation (which will occur in a separate |
| // task). |
| let form = document.createElement('form'); |
| form.appendChild(input); |
| form.setAttribute("method", "POST"); |
| form.setAttribute("action", "about:blank"); |
| form.setAttribute("target", "_top"); |
| document.body.appendChild(form); |
| form.submit(); |
| |
| // Delete this frame before the scheduled navigation occurs in the main |
| // frame. |
| parent.document.querySelector("iframe").remove(); |
| )"); |
| loop.Run(); |
| } |
| |
| // A class to intercept RemoteFrameHost IPCs, specifically OpenURL. When an |
| // OpenURL IPC is received, this interceptor closes the initiator's Shell, |
| // `shell_to_close`, and ensures the corresponding process exits before |
| // proceeding with the OpenURL call. |
| class InitiatorClosingOpenURLInterceptor |
| : public blink::mojom::RemoteFrameHostInterceptorForTesting { |
| public: |
| // `this` takes ownership of `shell_to_close` and eventually deletes it. |
| InitiatorClosingOpenURLInterceptor(content::RenderFrameProxyHost* proxy_host, |
| std::unique_ptr<Shell> shell_to_close, |
| RenderProcessHost* renderer_to_exit) |
| : shell_to_close_(std::move(shell_to_close)), |
| renderer_to_exit_(renderer_to_exit), |
| swapped_impl_(std::make_unique<mojo::test::ScopedSwapImplForTesting< |
| blink::mojom::RemoteFrameHost>>( |
| proxy_host->frame_host_receiver_for_testing(), |
| this)) {} |
| ~InitiatorClosingOpenURLInterceptor() override = default; |
| |
| blink::mojom::RemoteFrameHost* GetForwardingInterface() override { |
| return swapped_impl_->old_impl(); |
| } |
| |
| // This closes `shell_to_close_` and causes `renderer_to_exit_` to exit |
| // before forwarding the call to the RenderFrameProxyHost. This mimics the |
| // case where the frame that sent the OpenURL gets closed before the IPC |
| // reaches its destination. Once the OpenURL IPC is sent, the proxy should |
| // receive it, even if the sender is gone. |
| void OpenURL(blink::mojom::OpenURLParamsPtr params) override { |
| // `Close()` internally deletes the pointer, so it must be released so |
| // `shell_to_close_` doesn't point to a deleted value. |
| shell_to_close_.release()->Close(); |
| renderer_to_exit_->Shutdown(content::RESULT_CODE_KILLED); |
| |
| GetForwardingInterface()->OpenURL(std::move(params)); |
| |
| // Delete the swapped impl while the real RenderFrameProxyHost still exists, |
| // since we only need to intercept a single OpenURL call. The next task may |
| // delete the real impl. |
| swapped_impl_.reset(); |
| |
| // Clear the other raw_ptrs to avoid dangling pointers. |
| renderer_to_exit_ = nullptr; |
| } |
| |
| private: |
| std::unique_ptr<Shell> shell_to_close_; |
| raw_ptr<RenderProcessHost> renderer_to_exit_; |
| |
| // The `swapped_impl_` is a unique_ptr, so the member can be deleted before |
| // `this` gets destroyed. The original implementation would normally be |
| // swapped back in when `this` is destroyed. However, in this test, the |
| // `RenderFrameProxyHost` is deleted shortly after the OpenURL IPC is handled, |
| // and relying on normal scoper cleanup would cause a use-after-free. To avoid |
| // this, we early delete the `swapped_impl_` to swap back the original |
| // implementation as soon as `OpenURL()` has been processed. |
| std::unique_ptr< |
| mojo::test::ScopedSwapImplForTesting<blink::mojom::RemoteFrameHost>> |
| swapped_impl_; |
| }; |
| |
| // Test the case that once an OpenURL IPC is sent, it is received and the |
| // navigation occurs even if the sender is deleted while the IPC is in flight. |
| // This test opens a main frame, which opens a cross-site popup. The test then |
| // does a form submission to the popup and closes the main frame. |
| // Unlike FormSubmissionInRemoteFrameThenDeleteFrame, the initiator is the last |
| // (and only) frame of that SiteInstance. Deleting it usually causes proxies in |
| // the same SiteInstanceGroup to be deleted, meaning the OpenURL IPC may never |
| // be received. |
| // |
| // Fails on linux-bfcache-rel and android-bfcache-rel. See crbug.com/336671248. |
| #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_ANDROID) |
| #define MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL \ |
| DISABLED_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL |
| #else |
| #define MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL \ |
| FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL |
| #endif |
| IN_PROC_BROWSER_TEST_F( |
| NavigationBrowserTest, |
| MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL) { |
| // We crash a renderer in the OpenURL interceptor. |
| content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes; |
| content::IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| |
| // Get a unique_ptr to the shell, which will be used to transfer ownership |
| // later on. |
| std::unique_ptr<Shell> shell_a = base::WrapUnique(CreateBrowser()); |
| |
| // Setup the main page, a.com. The referrer policy is needed to test the |
| // keep alive part of PolicyContainerHost. |
| GURL always_referrer_url_a(embedded_test_server()->GetURL( |
| "a.com", "/set-header?Referrer-Policy: unsafe-url")); |
| EXPECT_TRUE(NavigateToURL(shell_a.get(), always_referrer_url_a)); |
| EXPECT_TRUE(WaitForLoadStop(shell_a->web_contents())); |
| |
| // The a.com's RenderFrameHost will be the initiator of the form submission. |
| RenderFrameHostImpl* rfh_a = static_cast<RenderFrameHostImpl*>( |
| shell_a->web_contents()->GetPrimaryMainFrame()); |
| |
| // Create a cross origin popup that will be the target of the form submission. |
| // This is cross-site so we can test the case where the last RenderFrameHost |
| // for the initiator's site is gone when the initiator deletes itself, causing |
| // all proxies in its SiteInstanceGroup to potentially delete themselves |
| // before the OpenURL call can be received. |
| // However, we also need to be able to navigate the target frame, so this is |
| // opened as a popup. |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/empty.html")); |
| WebContentsImpl* web_contents_b = nullptr; |
| { |
| WebContentsAddedObserver observer_b; |
| ASSERT_TRUE(ExecJs(rfh_a, JsReplace("window.open($1, '_bpopup')", url_b))); |
| web_contents_b = static_cast<WebContentsImpl*>(observer_b.GetWebContents()); |
| } |
| WaitForLoadStop(web_contents_b); |
| |
| base::RunLoop loop; |
| auto initiator_global_token = rfh_a->GetGlobalFrameToken(); |
| |
| // Register a callback to make sure the script below triggers a |
| // DidStartNavigation event to fire. This indicates that the popup main |
| // frame's proxy in A's SiteInstanceGroup received and ran the OpenURL call. |
| DidStartNavigationCallback callback( |
| web_contents_b, base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| auto* request = NavigationRequest::From(handle); |
| ASSERT_TRUE(request->IsPost()); |
| |
| const std::optional<blink::LocalFrameToken>& frame_token = |
| request->GetInitiatorFrameToken(); |
| EXPECT_TRUE(frame_token.has_value()); |
| EXPECT_EQ(initiator_global_token.frame_token, frame_token.value()); |
| EXPECT_EQ(initiator_global_token.child_id, |
| request->GetInitiatorProcessId()); |
| |
| // This is the RenderFrameHost in the WebContents that was forced to |
| // `Close()` in the interceptor, so it should be deleted. |
| auto* initiator_rfh = RenderFrameHostImpl::FromFrameToken( |
| request->GetInitiatorProcessId(), frame_token.value()); |
| EXPECT_FALSE(initiator_rfh); |
| |
| // Even if the initiator RenderFrameHost is gone, its |
| // PolicyContainerHost should still be around since the LocalFrame has |
| // not been destroyed yet. |
| PolicyContainerHost* initiator_policy_container = |
| RenderFrameHostImpl::GetPolicyContainerHost( |
| base::OptionalToPtr(frame_token), |
| request->GetInitiatorProcessId(), |
| web_contents()->GetPrimaryMainFrame()->GetStoragePartition()); |
| ASSERT_TRUE(initiator_policy_container); |
| EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways, |
| initiator_policy_container->referrer_policy()); |
| EXPECT_EQ( |
| network::mojom::ReferrerPolicy::kAlways, |
| request->GetInitiatorPolicyContainerPolicies()->referrer_policy); |
| |
| loop.Quit(); |
| })); |
| |
| // Intercept the OpenURL call to the proxy for the popup in A's |
| // SiteInstanceGroup, which will happen in a separate navigation task posted |
| // from the form submission task in the script below. |
| // Ownership of `shell_a` is being transferred to the interceptor. The |
| // interceptor will delete `shell_a` so it should not be used after this. |
| SiteInstanceGroup* a_sig = rfh_a->GetSiteInstance()->group(); |
| auto proxy_host_interceptor = |
| std::make_unique<InitiatorClosingOpenURLInterceptor>( |
| web_contents_b->GetPrimaryMainFrame() |
| ->browsing_context_state() |
| ->GetRenderFrameProxyHost(a_sig), |
| std::move(shell_a), a_sig->process()); |
| |
| // Initiate a form submission into the b.com popup that will navigate the |
| // popup to about:blank. |
| // We want the initiator to be closed between the time the about:blank OpenURL |
| // IPC is sent and received. This is done in the interceptor by closing the |
| // shell the initiator belongs to. The timing means window.close() is not a |
| // viable option: it would post a task after the OpenURL navigation task, so |
| // we can't ensure the window is closed before OpenURL runs. |
| ExecuteScriptAsync(rfh_a, R"( |
| let input = document.createElement("input"); |
| input.setAttribute("type", "hidden"); |
| input.setAttribute("name", "my_token"); |
| input.setAttribute("value", "my_value"); |
| |
| // Schedule a form submission navigation (which will occur in a separate |
| // task). |
| let form = document.createElement('form'); |
| form.appendChild(input); |
| form.setAttribute("method", "POST"); |
| form.setAttribute("action", "about:blank"); |
| form.setAttribute("target", "_bpopup"); |
| document.body.appendChild(form); |
| form.submit(); |
| )"); |
| loop.Run(); |
| |
| // Make sure the about:blank navigation finishes successfully. |
| WaitForLoadStop(web_contents_b); |
| EXPECT_EQ(GURL("about:blank"), web_contents_b->GetLastCommittedURL()); |
| } |
| |
| // Check that when RenderProcessHostImpl::DisableRefCounts is called while a |
| // NavigationStateKeepAlive exists, the navigation still succeeds. This is a |
| // regression test for crbug.com/348150830. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| DisableRefCountsWhileKeepAliveExists) { |
| GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // This test needs the browser process to call DisableRefCounts after the form |
| // submission's NavigationStateKeepAlive is created and before the task that |
| // sends the BeginNavigation IPC. To do this, use EvalJS to return a string to |
| // the test framework between those two renderer-side tasks, allowing the |
| // browser process to reset the counts before the BeginNavigation IPC is |
| // received and the NavigationStateKeepAlive is destroyed. |
| std::string expected_str("Placeholder value"); |
| std::string js_str = base::StringPrintf( |
| "f = document.createElement('form');" |
| "f.action = 'about:blank';" |
| "document.body.appendChild(f);" |
| "f.submit();" |
| "'%s';", |
| expected_str.c_str()); |
| |
| TestNavigationObserver observer(shell()->web_contents()); |
| EXPECT_EQ(expected_str, EvalJs(shell(), js_str).ExtractString()); |
| |
| // Expect at this point that a NavigationStateKeepAlive has been created for |
| // the form submission. |
| NavigationStateKeepAlive* keep_alive = |
| current_frame_host()->GetStoragePartition()->GetNavigationStateKeepAlive( |
| current_frame_host()->GetFrameToken()); |
| ASSERT_TRUE(keep_alive); |
| |
| // Disable ref counts on the process, which resets all ref counts to 0. This |
| // seems to happen in practice in https://crbug.com/348150830 when a |
| // BrowserContext is closed before all of its frames are properly cleaned up, |
| // but the exact repro steps for this aren't known, so simulate this behavior |
| // with an explicit DisableRefCounts() call. |
| current_frame_host()->GetProcess()->DisableRefCounts(); |
| |
| // Wait for the navigation to complete. At that point, the |
| // NavigationStateKeepAlive goes away, which can possibly decrement the |
| // associated ref count. Since DisableRefCounts() was called, the ref count |
| // should not be further decremented, and the navigation should complete |
| // successfully. |
| observer.Wait(); |
| EXPECT_TRUE(observer.last_navigation_succeeded()); |
| EXPECT_TRUE(current_frame_host()->GetLastCommittedURL().IsAboutBlank()); |
| } |
| |
| using MediaNavigationBrowserTest = NavigationBaseBrowserTest; |
| |
| // Media navigations synchronously complete the time of the `CommitNavigation` |
| // IPC call. Ensure that the renderer does not crash if the media navigation |
| // results in an HTTP error with no body, since the renderer will reentrantly |
| // commit an error page while handling the `CommitNavigation` IPC. |
| IN_PROC_BROWSER_TEST_F(MediaNavigationBrowserTest, FailedNavigation) { |
| embedded_test_server()->RegisterRequestHandler(base::BindRepeating( |
| [](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_NOT_FOUND); |
| response->set_content_type("video/mp4"); |
| return response; |
| })); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| const GURL error_url(embedded_test_server()->GetURL("/moo.mp4")); |
| EXPECT_FALSE(NavigateToURL(shell(), error_url)); |
| EXPECT_EQ(error_url, current_frame_host()->GetLastCommittedURL()); |
| NavigationEntry* entry = |
| web_contents()->GetController().GetLastCommittedEntry(); |
| EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType()); |
| } |
| |
| using DocumentPolicyBrowserTest = NavigationBaseBrowserTest; |
| |
| // Test that scroll restoration can be disabled with |
| // Document-Policy: force-load-at-top |
| IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest, |
| ScrollRestorationDisabledByDocumentPolicy) { |
| net::test_server::ControllableHttpResponse response(embedded_test_server(), |
| "/target.html"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url(embedded_test_server()->GetURL("/target.html")); |
| TestNavigationManager navigation_manager(web_contents(), url); |
| // This test expects the document is freshly loaded on the back navigation |
| // so that the document policy to force-load-at-top will run. This will not |
| // happen if the document is back-forward cached, so we need to disable it. |
| DisableBackForwardCacheForTesting(web_contents(), |
| BackForwardCache::TEST_REQUIRES_NO_CACHING); |
| |
| // Load the document with document policy force-load-at-top |
| shell()->LoadURL(url); |
| EXPECT_TRUE(navigation_manager.WaitForRequestStart()); |
| navigation_manager.ResumeNavigation(); |
| response.WaitForRequest(); |
| response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Document-Policy: force-load-at-top\r\n" |
| "\r\n" |
| "<p style='position: absolute; top: 10000px;'>Some text</p>"); |
| response.Done(); |
| |
| EXPECT_TRUE(navigation_manager.WaitForResponse()); |
| navigation_manager.ResumeNavigation(); |
| ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); |
| |
| { |
| RenderFrameSubmissionObserver frame_observer(web_contents()); |
| // Scroll down the page a bit |
| EXPECT_TRUE(ExecJs(web_contents(), "window.scrollTo(0, 1000)")); |
| frame_observer.WaitForScrollOffsetAtTop(false); |
| } |
| |
| // Navigate away |
| EXPECT_TRUE(ExecJs(web_contents(), "window.location = 'about:blank'")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); |
| |
| // Navigate back |
| EXPECT_TRUE(ExecJs(web_contents(), "history.back()")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); |
| |
| // Wait a short amount of time to ensure the page does not scroll. |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); |
| run_loop.Run(); |
| RunUntilInputProcessed(RenderWidgetHostImpl::From( |
| web_contents()->GetRenderViewHost()->GetWidget())); |
| const cc::RenderFrameMetadata& last_metadata = |
| RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata(); |
| EXPECT_TRUE(last_metadata.is_scroll_offset_at_top); |
| } |
| |
| // Test that scroll restoration works as expected with |
| // Document-Policy: force-load-at-top=?0 |
| IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest, |
| ScrollRestorationEnabledByDocumentPolicy) { |
| net::test_server::ControllableHttpResponse response(embedded_test_server(), |
| "/target.html"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url(embedded_test_server()->GetURL("/target.html")); |
| RenderFrameSubmissionObserver frame_observer(web_contents()); |
| TestNavigationManager navigation_manager(web_contents(), url); |
| |
| // Load the document with document policy force-load-at-top set to false. |
| shell()->LoadURL(url); |
| EXPECT_TRUE(navigation_manager.WaitForRequestStart()); |
| navigation_manager.ResumeNavigation(); |
| response.WaitForRequest(); |
| response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Document-Policy: force-load-at-top=?0\r\n" |
| "\r\n" |
| "<p style='position: absolute; top: 10000px;'>Some text</p>"); |
| response.Done(); |
| |
| EXPECT_TRUE(navigation_manager.WaitForResponse()); |
| navigation_manager.ResumeNavigation(); |
| ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); |
| |
| // Scroll down the page a bit |
| EXPECT_TRUE(ExecJs(web_contents(), "window.scrollTo(0, 1000)")); |
| frame_observer.WaitForScrollOffsetAtTop(false); |
| |
| // Navigate away |
| EXPECT_TRUE(ExecJs(web_contents(), "window.location = 'about:blank'")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); |
| |
| // Navigate back |
| EXPECT_TRUE(ExecJs(web_contents(), "history.back()")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); |
| |
| // Ensure scroll restoration activated |
| frame_observer.WaitForScrollOffsetAtTop(false); |
| const cc::RenderFrameMetadata& last_metadata = |
| RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata(); |
| EXPECT_FALSE(last_metadata.is_scroll_offset_at_top); |
| } |
| |
| // Test that element fragment anchor scrolling can be disabled with |
| // Document-Policy: force-load-at-top |
| IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest, |
| FragmentAnchorDisabledByDocumentPolicy) { |
| net::test_server::ControllableHttpResponse response(embedded_test_server(), |
| "/target.html"); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url(embedded_test_server()->GetURL("/target.html#text")); |
| |
| // Load the target document |
| TestNavigationManager navigation_manager(web_contents(), url); |
| shell()->LoadURL(url); |
| |
| // Start navigation |
| EXPECT_TRUE(navigation_manager.WaitForRequestStart()); |
| navigation_manager.ResumeNavigation(); |
| |
| // Send Document-Policy header |
| response.WaitForRequest(); |
| response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Document-Policy: force-load-at-top\r\n" |
| "\r\n" |
| "<p id='text' style='position: absolute; top: 10000px;'>Some text</p>"); |
| response.Done(); |
| |
| EXPECT_TRUE(navigation_manager.WaitForResponse()); |
| navigation_manager.ResumeNavigation(); |
| ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); |
| // Wait a short amount of time to ensure the page does not scroll. |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); |
| run_loop.Run(); |
| RunUntilInputProcessed(RenderWidgetHostImpl::From( |
| web_contents()->GetRenderViewHost()->GetWidget())); |
| const cc::RenderFrameMetadata& last_metadata = |
| RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata(); |
| EXPECT_TRUE(last_metadata.is_scroll_offset_at_top); |
| } |
| |
| // Test that element fragment anchor scrolling works as expected with |
| // Document-Policy: force-load-at-top=?0 |
| IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest, |
| FragmentAnchorEnabledByDocumentPolicy) { |
| net::test_server::ControllableHttpResponse response(embedded_test_server(), |
| "/target.html"); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url(embedded_test_server()->GetURL("/target.html#text")); |
| RenderFrameSubmissionObserver frame_observer(web_contents()); |
| |
| // Load the target document |
| TestNavigationManager navigation_manager(web_contents(), url); |
| shell()->LoadURL(url); |
| |
| // Start navigation |
| EXPECT_TRUE(navigation_manager.WaitForRequestStart()); |
| navigation_manager.ResumeNavigation(); |
| |
| // Send Document-Policy header |
| response.WaitForRequest(); |
| response.Send( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Type: text/html; charset=utf-8\r\n" |
| "Document-Policy: force-load-at-top=?0\r\n" |
| "\r\n" |
| "<p id='text' style='position: absolute; top: 10000px;'>Some text</p>"); |
| response.Done(); |
| |
| EXPECT_TRUE(navigation_manager.WaitForResponse()); |
| navigation_manager.ResumeNavigation(); |
| ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); |
| frame_observer.WaitForScrollOffsetAtTop( |
| /*expected_scroll_offset_at_top=*/false); |
| const cc::RenderFrameMetadata& last_metadata = |
| RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata(); |
| EXPECT_FALSE(last_metadata.is_scroll_offset_at_top); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommitBasic) { |
| GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); |
| auto origin_expected = url::Origin::Create(url); |
| TestNavigationManager manager(web_contents(), url); |
| shell()->LoadURL(url); |
| EXPECT_TRUE(manager.WaitForResponse()); |
| NavigationRequest* navigation = main_frame()->navigation_request(); |
| std::optional<url::Origin> origin_to_commit = navigation->GetOriginToCommit(); |
| ASSERT_TRUE(origin_to_commit.has_value()); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| url::Origin origin_committed = current_frame_host()->GetLastCommittedOrigin(); |
| |
| EXPECT_FALSE(origin_to_commit->opaque()); |
| EXPECT_FALSE(origin_committed.opaque()); |
| EXPECT_EQ(origin_expected, *origin_to_commit); |
| EXPECT_EQ(origin_expected, origin_committed); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommit204) { |
| GURL url = embedded_test_server()->GetURL("a.com", "/nocontent"); |
| TestNavigationManager manager(web_contents(), url); |
| shell()->LoadURL(url); |
| EXPECT_TRUE(manager.WaitForResponse()); |
| NavigationRequest* navigation = main_frame()->navigation_request(); |
| std::optional<url::Origin> origin_to_commit = navigation->GetOriginToCommit(); |
| EXPECT_FALSE(origin_to_commit.has_value()); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| OriginToCommitSandboxFromResponse) { |
| GURL url = embedded_test_server()->GetURL( |
| "a.com", "/set-header?Content-Security-Policy: sandbox"); |
| TestNavigationManager manager(web_contents(), url); |
| shell()->LoadURL(url); |
| EXPECT_TRUE(manager.WaitForResponse()); |
| NavigationRequest* navigation = main_frame()->navigation_request(); |
| url::Origin origin_to_commit = navigation->GetOriginToCommit().value(); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| url::Origin origin_committed = current_frame_host()->GetLastCommittedOrigin(); |
| |
| EXPECT_TRUE(origin_to_commit.opaque()); |
| EXPECT_TRUE(origin_committed.opaque()); |
| // TODO(crbug.com/40092527). The nonce must match. |
| EXPECT_NE(origin_to_commit, origin_committed); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| OriginToCommitSandboxFromParentDocument) { |
| GURL url_top = embedded_test_server()->GetURL( |
| "a.com", "/set-header?Content-Security-Policy: sandbox allow-scripts"); |
| EXPECT_TRUE(NavigateToURL(shell(), url_top)); |
| GURL url_iframe = embedded_test_server()->GetURL("a.com", "/empty.html"); |
| TestNavigationManager manager(web_contents(), url_iframe); |
| ExecuteScriptAsync(current_frame_host(), R"( |
| let iframe = document.createElement("iframe"); |
| iframe.src = "./empty.html"; |
| document.body.appendChild(iframe); |
| )"); |
| EXPECT_TRUE(manager.WaitForResponse()); |
| FrameTreeNode* iframe = current_frame_host()->child_at(0); |
| NavigationRequest* navigation = iframe->navigation_request(); |
| url::Origin origin_to_commit = navigation->GetOriginToCommit().value(); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| url::Origin origin_committed = |
| iframe->current_frame_host()->GetLastCommittedOrigin(); |
| |
| EXPECT_TRUE(origin_to_commit.opaque()); |
| EXPECT_TRUE(origin_committed.opaque()); |
| // TODO(crbug.com/40092527). The nonce must match. |
| EXPECT_NE(origin_to_commit, origin_committed); |
| |
| // Both document have the same URL. Only the first sets CSP:sandbox, but both |
| // are sandboxed. They get an opaque origin different from each others. |
| EXPECT_NE(current_frame_host()->GetLastCommittedOrigin(), origin_committed); |
| } |
| |
| // Regression test for https://crbug.com/1158306. |
| // Navigate to a response, which set Content-Security-Policy: sandbox AND block |
| // the response. The error page shouldn't set sandbox flags. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ErrorPageFromCspSandboxResponse) { |
| // Block every navigation in WillProcessResponse. |
| std::unique_ptr<content::TestNavigationThrottleInserter> blocker = |
| BlockNavigationWillProcessResponse(web_contents()); |
| |
| // Navigate toward a document witch sets CSP:sandbox. |
| GURL url = embedded_test_server()->GetURL( |
| "a.com", "/set-header?Content-Security-Policy: sandbox"); |
| TestNavigationManager manager(web_contents(), url); |
| shell()->LoadURL(url); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| |
| // An error page committed. It doesn't have any sandbox flags, despite the |
| // original response headers. |
| EXPECT_TRUE(current_frame_host()->IsErrorDocument()); |
| EXPECT_EQ(network::mojom::WebSandboxFlags::kNone, |
| current_frame_host()->active_sandbox_flags()); |
| |
| EXPECT_EQ(url, current_frame_host()->GetLastCommittedURL()); |
| EXPECT_TRUE(current_frame_host()->GetLastCommittedOrigin().opaque()); |
| EXPECT_TRUE( |
| current_frame_host()->GetLastCommittedOrigin().CanBeDerivedFrom(url)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| ProcessShutdownDuringDeferredNavigationThrottle) { |
| GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| class ShutdownThrottle : public TaskRunnerDeferringThrottle, |
| WebContentsObserver { |
| public: |
| explicit ShutdownThrottle(WebContents* web_contents, |
| NavigationHandle* handle) |
| : TaskRunnerDeferringThrottle( |
| base::SingleThreadTaskRunner::GetCurrentDefault(), |
| /*defer_start=*/false, |
| /*defer_redirect=*/false, |
| /*defer_response=*/true, |
| handle), |
| web_contents_(web_contents) { |
| WebContentsObserver::Observe(web_contents_); |
| } |
| |
| void AsyncResume() override { |
| // Shutdown the renderer and delay Resume() until then. |
| web_contents_->GetPrimaryMainFrame()->GetProcess()->Shutdown(1); |
| } |
| |
| void RenderFrameDeleted(RenderFrameHost* frame_host) override { |
| TaskRunnerDeferringThrottle::AsyncResume(); |
| } |
| |
| private: |
| raw_ptr<WebContents> web_contents_; |
| }; |
| |
| auto inserter = std::make_unique<TestNavigationThrottleInserter>( |
| shell()->web_contents(), |
| base::BindLambdaForTesting( |
| [&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { |
| return std::make_unique<ShutdownThrottle>(shell()->web_contents(), |
| handle); |
| })); |
| |
| class DoesNotReadyToCommitObserver : public WebContentsObserver { |
| public: |
| explicit DoesNotReadyToCommitObserver(WebContents* contents) |
| : WebContentsObserver(contents) {} |
| |
| // WebContentsObserver overrides. |
| void ReadyToCommitNavigation(NavigationHandle* handle) override { |
| // This method should not happen. Since the process is destroyed before |
| // we become ready to commit, we can not ever reach |
| // ReadyToCommitNavigation. Doing so would fail because the renderer is |
| // gone. |
| ADD_FAILURE() << "ReadyToCommitNavigation but renderer has crashed. " |
| "IsRenderFrameLive: " |
| << handle->GetRenderFrameHost()->IsRenderFrameLive(); |
| navigation_was_ready_to_commit_ = true; |
| } |
| |
| void DidFinishNavigation(NavigationHandle* handle) override { |
| navigation_finished_ = true; |
| navigation_committed_ = handle->HasCommitted(); |
| } |
| |
| bool navigation_was_ready_to_commit() { |
| return navigation_was_ready_to_commit_; |
| } |
| bool navigation_finished() { return navigation_finished_; } |
| bool navigation_committed() { return navigation_committed_; } |
| |
| private: |
| bool navigation_was_ready_to_commit_ = false; |
| bool navigation_finished_ = false; |
| bool navigation_committed_ = false; |
| }; |
| |
| // Watch that ReadyToCommitNavigation() will not happen when the renderer is |
| // gone. |
| DoesNotReadyToCommitObserver no_commit_obs(shell()->web_contents()); |
| |
| // We will shutdown the renderer during this navigation. |
| ScopedAllowRendererCrashes scoped_allow_renderer_crashes; |
| |
| // Important: This is a browser-initiated navigation, so the NavigationRequest |
| // does not have an open connection (NavigationClient) to the renderer that it |
| // is listening to for termination while running NavigationThrottles. |
| // |
| // Expect this navigation to be aborted, so we stop waiting after the |
| // uncommitted navigation is done. |
| GURL url2 = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| NavigateToURLBlockUntilNavigationsComplete( |
| shell(), url2, /*number_of_navigations=*/1, |
| /*ignore_uncommitted_navigations=*/false); |
| |
| // The renderer was shutdown mid-navigation. |
| EXPECT_FALSE( |
| shell()->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive()); |
| |
| // The navigation was aborted, which means it finished but did not commit, and |
| // _importantly_ it never reported "ReadyToCommitNavigation" without a live |
| // renderer. |
| EXPECT_TRUE(no_commit_obs.navigation_finished()); |
| EXPECT_FALSE(no_commit_obs.navigation_was_ready_to_commit()); |
| EXPECT_FALSE(no_commit_obs.navigation_committed()); |
| } |
| |
| // Sandbox flags defined by the parent must not apply to Chrome's error page. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ErrorPageFromInSandboxedIframe) { |
| GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Block every navigation in WillProcessResponse. |
| std::unique_ptr<content::TestNavigationThrottleInserter> blocker = |
| BlockNavigationWillProcessResponse(web_contents()); |
| |
| TestNavigationManager manager(web_contents(), url); |
| ExecuteScriptAsync(current_frame_host(), R"( |
| let iframe = document.createElement("iframe"); |
| iframe.src = location.href; |
| iframe.sandbox = "allow-orientation-lock"; |
| document.body.appendChild(iframe); |
| )"); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| |
| RenderFrameHostImpl* child_rfh = |
| current_frame_host()->child_at(0)->current_frame_host(); |
| |
| EXPECT_TRUE(child_rfh->IsErrorDocument()); |
| EXPECT_EQ(network::mojom::WebSandboxFlags::kNone, |
| child_rfh->active_sandbox_flags()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommitSandboxFromFrame) { |
| GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| TestNavigationManager manager(web_contents(), url); |
| ExecuteScriptAsync(current_frame_host(), R"( |
| let iframe = document.createElement("iframe"); |
| iframe.src = location.href; |
| iframe.sandbox = ""; |
| document.body.appendChild(iframe); |
| )"); |
| EXPECT_TRUE(manager.WaitForResponse()); |
| FrameTreeNode* iframe = current_frame_host()->child_at(0); |
| NavigationRequest* navigation = iframe->navigation_request(); |
| url::Origin origin_to_commit = navigation->GetOriginToCommit().value(); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| url::Origin origin_committed = |
| iframe->current_frame_host()->GetLastCommittedOrigin(); |
| |
| EXPECT_TRUE(origin_to_commit.opaque()); |
| EXPECT_TRUE(origin_committed.opaque()); |
| // TODO(crbug.com/40092527). Make the nonce to match. |
| EXPECT_NE(origin_to_commit, origin_committed); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| NavigateToAboutBlankWhileFirstNavigationPending) { |
| GURL url_a = embedded_test_server()->GetURL("a.com", "/empty.html"); |
| GURL url_b = embedded_test_server()->GetURL("b.com", "/empty.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| ShellAddedObserver new_shell_observer; |
| ExecuteScriptAsync( |
| current_frame_host(), |
| JsReplace("window.open($1, '_blank').location = 'about:blank'", url_b)); |
| |
| WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); |
| TestNavigationManager manager_1(popup_contents, url_b); |
| TestNavigationManager manager_2(popup_contents, GURL("about:blank")); |
| |
| ASSERT_TRUE(manager_1.WaitForNavigationFinished()); |
| ASSERT_TRUE(manager_2.WaitForNavigationFinished()); |
| |
| EXPECT_EQ(popup_contents->GetLastCommittedURL(), "about:blank"); |
| } |
| |
| class NetworkIsolationSplitCacheAppendIframeOrigin |
| : public NavigationBaseBrowserTest { |
| public: |
| NetworkIsolationSplitCacheAppendIframeOrigin() { |
| feature_list_.InitAndEnableFeature( |
| net::features::kSplitCacheByNetworkIsolationKey); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Make a main document, have it request a cacheable subresources. Then make a |
| // same-site document in an iframe that serves the CSP:Sandbox header. Stop the |
| // test server, have the sandboxed document requests the same subresource. The |
| // request should fail. To make sure the request is actually in the cache, the |
| // main document should be able to request it again. |
| IN_PROC_BROWSER_TEST_F(NetworkIsolationSplitCacheAppendIframeOrigin, |
| SandboxedUsesDifferentCache) { |
| auto server = std::make_unique<net::EmbeddedTestServer>(); |
| server->AddDefaultHandlers(GetTestDataFilePath()); |
| EXPECT_TRUE(server->Start()); |
| |
| GURL url_main_document = server->GetURL("a.com", "/empty.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_main_document)); |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| new Promise(resolve => { |
| let iframe = document.createElement("iframe"); |
| iframe.onload = resolve; |
| iframe.src = "/set-header?Content-Security-Policy: sandbox allow-scripts"; |
| document.body.appendChild(iframe); |
| }) |
| )")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| |
| RenderFrameHostImpl* main_rfh = current_frame_host(); |
| RenderFrameHostImpl* sub_rfh = main_rfh->child_at(0)->current_frame_host(); |
| |
| EXPECT_FALSE(main_rfh->GetLastCommittedOrigin().opaque()); |
| EXPECT_TRUE(sub_rfh->GetLastCommittedOrigin().opaque()); |
| |
| const char* fetch_cacheable = R"( |
| fetch("cacheable.svg") |
| .then(() => "success") |
| .catch(() => "error") |
| )"; |
| |
| EXPECT_EQ("success", EvalJs(main_rfh, fetch_cacheable)); |
| |
| server.reset(); |
| |
| EXPECT_EQ("error", EvalJs(sub_rfh, fetch_cacheable)); |
| EXPECT_EQ("success", EvalJs(main_rfh, fetch_cacheable)); |
| } |
| |
| // The Content Security Policy directive 'treat-as-public-address' is parsed |
| // into the parsed headers by services/network and applied there. That directive |
| // is ignored in report-only policies. Here we check that Blink reports a |
| // console message if 'treat-as-public-address' is delivered in a report-only |
| // policy. This serves also as a regression test for https://crbug.com/1150314 |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| TreatAsPublicAddressInReportOnly) { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "The Content Security Policy directive 'treat-as-public-address' is " |
| "ignored when delivered in a report-only policy."); |
| |
| GURL url = embedded_test_server()->GetURL( |
| "/set-header?" |
| "Content-Security-Policy-Report-Only: treat-as-public-address"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| // The Content Security Policy directive 'plugin-types' has been removed. Here |
| // we check that Blink reports a console message if 'plugin-type' is delivered |
| // in a policy. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| ContentSecurityPolicyErrorPluginTypes) { |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern( |
| "The Content-Security-Policy directive 'plugin-types' has been removed " |
| "from the specification. " |
| "If you want to block plugins, consider specifying \"object-src 'none'\" " |
| "instead."); |
| |
| GURL url = embedded_test_server()->GetURL( |
| "/set-header?" |
| "Content-Security-Policy: plugin-types application/pdf"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| class SubresourceLoadingTest : public NavigationBrowserTest { |
| public: |
| SubresourceLoadingTest() = default; |
| SubresourceLoadingTest(const SubresourceLoadingTest&) = delete; |
| SubresourceLoadingTest& operator=(const SubresourceLoadingTest&) = delete; |
| |
| void DontTestNetworkServiceCrashes() { |
| test_network_service_crashes_ = false; |
| } |
| |
| void VerifyResultsOfAboutBlankNavigation(RenderFrameHost* target_frame, |
| RenderFrameHost* initiator_frame) { |
| // Verify that `target_frame` has been navigated to "about:blank". |
| EXPECT_EQ(GURL(url::kAboutBlankURL), target_frame->GetLastCommittedURL()); |
| |
| // Verify that "about:blank" committed with the expected origin, and in the |
| // expected SiteInstance. |
| EXPECT_EQ(target_frame->GetLastCommittedOrigin(), |
| initiator_frame->GetLastCommittedOrigin()); |
| EXPECT_EQ(target_frame->GetSiteInstance(), |
| initiator_frame->GetSiteInstance()); |
| |
| // Ask for cookies in the `target_frame`. One implicit verification here |
| // is whether this step will hit any `cookie_url`-related NOTREACHED or DwoC |
| // in RestrictedCookieManager::ValidateAccessToCookiesAt. This verification |
| // is non-racey, because `document.cookie` must have heard back from the |
| // RestrictedCookieManager before returning the value of cookies (this |
| // ignores possible Blink-side caching, but this is the first time the |
| // renderer needs the cookies and so this is okay for this test). |
| EXPECT_EQ("", EvalJs(target_frame, "document.cookie")); |
| |
| // Verify that the "about:blank" frame is able to load an image. |
| VerifyImageSubresourceLoads(target_frame); |
| } |
| |
| void VerifyImageSubresourceLoads( |
| const ToRenderFrameHost& target, |
| const std::string& target_document = "document") { |
| RenderFrameHostImpl* target_frame = |
| static_cast<RenderFrameHostImpl*>(target.render_frame_host()); |
| VerifySingleImageSubresourceLoad(target_frame, target_document); |
| |
| // Verify detecting and recovering from a NetworkService crash (e.g. via the |
| // `network_service_disconnect_handler_holder_mojo` field and the |
| // UpdateSubresourceLoaderFactories method of RenderFrameHostImpl). |
| if (!IsInProcessNetworkService() && test_network_service_crashes_) { |
| SimulateNetworkServiceCrash(); |
| |
| // In addition to waiting (inside SimulateNetworkServiceCrash above) for |
| // getting notified about being disconnected from |
| // network::mojom::NetworkServiceTest, we also want to make sure that the |
| // relevant RenderFrameHost realizes that the NetworkService has crashed. |
| // Which RenderFrameHost is relevant varies from test to test, so we |
| // flush multiple frames and use kDoNothingIfNoNetworkServiceConnection. |
| FlushNetworkInterfacesInOpenerChain(target_frame); |
| |
| // Rerun the test after the NetworkService crash. |
| VerifySingleImageSubresourceLoad(target_frame, target_document); |
| } |
| } |
| |
| private: |
| void FlushNetworkInterfacesInOpenerChain(RenderFrameHostImpl* current_frame) { |
| std::set<WebContents*> visited_contents; |
| while (true) { |
| // Check if we've already visited the current frame tree. |
| DCHECK(current_frame); |
| WebContents* current_contents = |
| WebContents::FromRenderFrameHost(current_frame); |
| DCHECK(current_contents); |
| if (base::Contains(visited_contents, current_contents)) |
| break; |
| visited_contents.insert(current_contents); |
| |
| // Flush all the frames in the `current_contents's active page. |
| current_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost( |
| [](RenderFrameHost* frame_to_flush) { |
| constexpr bool kDoNothingIfNoNetworkServiceConnection = true; |
| frame_to_flush->FlushNetworkAndNavigationInterfacesForTesting( |
| kDoNothingIfNoNetworkServiceConnection); |
| }); |
| |
| // Traverse the `current_frame`'s opener chain. |
| if (FrameTreeNode* opener_node = |
| current_frame->frame_tree_node()->opener()) { |
| current_frame = opener_node->current_frame_host(); |
| } else { |
| break; // Break out of the loop if there is no opener. |
| } |
| } |
| } |
| |
| void VerifySingleImageSubresourceLoad(RenderFrameHost* target, |
| std::string_view target_document) { |
| // Use a random, GUID-based hostname, to avoid hitting the network cache. |
| GURL image_url = embedded_test_server()->GetURL( |
| base::Uuid::GenerateRandomV4().AsLowercaseString() + ".com", |
| "/blank.jpg"); |
| const std::string script = base::StrCat({ |
| R"(new Promise(resolve => { |
| let img = document.createElement('img'); )", |
| JsReplace("img.src = $1;", image_url), |
| R"( img.addEventListener('load', () => { |
| resolve('allowed'); |
| }); |
| img.addEventListener('error', err => { |
| resolve(`error: ${err}`); |
| }); )", |
| target_document, R"(.body.appendChild(img); |
| }); )"}); |
| EXPECT_EQ("allowed", EvalJs(target, script)); |
| } |
| |
| bool test_network_service_crashes_ = true; |
| }; |
| |
| // The test below verifies that an "about:blank" navigation commits with the |
| // right origin, even when the initiator of the navigation is not the parent or |
| // opener of the frame targeted by the navigation. In the |
| // GrandchildToAboutBlank... testcases, the navigation is initiated by the |
| // grandparent of the target frame. |
| // |
| // In this test case there are no process swaps and the parent of the navigated |
| // frame is a local frame (even in presence of site-per-process). See also |
| // GrandchildToAboutBlank_ABA_CrossSite and |
| // GrandchildToAboutBlank_ABB_CrossSite. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, |
| GrandchildToAboutBlank_ABA_SameSite) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.example.com", |
| "/cross_site_iframe_factory.html?" |
| "a.example.com(b.example.com(a.example.com))")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Verify the desired properties of the test setup. |
| RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderFrameHostImpl* child_frame = |
| main_frame->child_at(0)->current_frame_host(); |
| RenderFrameHostImpl* grandchild_frame = |
| child_frame->child_at(0)->current_frame_host(); |
| EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); |
| EXPECT_EQ(main_frame->GetSiteInstance(), grandchild_frame->GetSiteInstance()); |
| EXPECT_EQ(main_frame->GetLastCommittedOrigin(), |
| grandchild_frame->GetLastCommittedOrigin()); |
| EXPECT_NE(main_frame->GetLastCommittedOrigin(), |
| child_frame->GetLastCommittedOrigin()); |
| |
| // Navigate the grandchild frame to about:blank |
| ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'")); |
| TestNavigationObserver nav_observer(shell()->web_contents(), 1); |
| ASSERT_TRUE( |
| ExecJs(main_frame, |
| "grandchild_window = window.open('about:blank', 'grandchild')")); |
| nav_observer.Wait(); |
| |
| // Verify that the grandchild has the same origin as the main frame (*not* the |
| // origin of the parent frame). |
| main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| child_frame = main_frame->child_at(0)->current_frame_host(); |
| grandchild_frame = child_frame->child_at(0)->current_frame_host(); |
| VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame); |
| } |
| |
| // The test below verifies that an "about:blank" navigation commits with the |
| // right origin, even when the initiator of the navigation is not the parent or |
| // opener of the frame targeted by the navigation. In the |
| // GrandchildToAboutBlank... testcases, the navigation is initiated by the |
| // grandparent of the target frame. |
| // |
| // In this test case there are no process swaps and the parent of the navigated |
| // frame is a remote frame (in presence of site-per-process). See also |
| // GrandchildToAboutBlank_ABA_SameSite and GrandchildToAboutBlank_ABB_CrossSite. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, |
| GrandchildToAboutBlank_ABA_CrossSite) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(a))")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Verify the desired properties of the test setup. |
| RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderFrameHostImpl* child_frame = |
| main_frame->child_at(0)->current_frame_host(); |
| RenderFrameHostImpl* grandchild_frame = |
| child_frame->child_at(0)->current_frame_host(); |
| if (AreStrictSiteInstancesEnabled()) { |
| EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); |
| } else { |
| EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); |
| } |
| EXPECT_EQ(main_frame->GetSiteInstance(), grandchild_frame->GetSiteInstance()); |
| EXPECT_EQ(main_frame->GetLastCommittedOrigin(), |
| grandchild_frame->GetLastCommittedOrigin()); |
| EXPECT_NE(main_frame->GetLastCommittedOrigin(), |
| child_frame->GetLastCommittedOrigin()); |
| |
| // Navigate the grandchild frame to about:blank |
| ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'")); |
| TestNavigationObserver nav_observer(shell()->web_contents(), 1); |
| ASSERT_TRUE( |
| ExecJs(main_frame, |
| "grandchild_window = window.open('about:blank', 'grandchild')")); |
| nav_observer.Wait(); |
| |
| // Verify that the grandchild has the same origin as the main frame (*not* the |
| // origin of the parent frame). |
| main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| child_frame = main_frame->child_at(0)->current_frame_host(); |
| grandchild_frame = child_frame->child_at(0)->current_frame_host(); |
| VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame); |
| } |
| |
| // The test below verifies that an "about:blank" navigation commits with the |
| // right origin, even when the initiator of the navigation is not the parent or |
| // opener of the frame targeted by the navigation. In the |
| // GrandchildToAboutBlank... testcases, the navigation is initiated by the |
| // grandparent of the target frame. |
| // |
| // In this test case the navigation forces a process swap of the target frame. |
| // See also GrandchildToAboutBlank_ABA_SameSite and |
| // GrandchildToAboutBlank_ABA_CrossSite. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, |
| GrandchildToAboutBlank_ABB_CrossSite) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(b))")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Verify the desired properties of the test setup. |
| RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderFrameHostImpl* child_frame = |
| main_frame->child_at(0)->current_frame_host(); |
| RenderFrameHostImpl* grandchild_frame = |
| child_frame->child_at(0)->current_frame_host(); |
| if (AreStrictSiteInstancesEnabled()) { |
| EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); |
| } else { |
| EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); |
| } |
| EXPECT_EQ(child_frame->GetSiteInstance(), |
| grandchild_frame->GetSiteInstance()); |
| EXPECT_EQ(child_frame->GetLastCommittedOrigin(), |
| grandchild_frame->GetLastCommittedOrigin()); |
| EXPECT_NE(main_frame->GetLastCommittedOrigin(), |
| grandchild_frame->GetLastCommittedOrigin()); |
| |
| // Navigate the grandchild frame to about:blank |
| ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'")); |
| TestNavigationObserver nav_observer(shell()->web_contents(), 1); |
| ASSERT_TRUE( |
| ExecJs(main_frame, |
| "grandchild_window = window.open('about:blank', 'grandchild')")); |
| nav_observer.Wait(); |
| |
| // Verify that the grandchild has the same origin as the main frame (*not* the |
| // origin of the parent frame). |
| main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| child_frame = main_frame->child_at(0)->current_frame_host(); |
| grandchild_frame = child_frame->child_at(0)->current_frame_host(); |
| VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame); |
| } |
| |
| // The test below verifies that an "about:blank" navigation commits with the |
| // right origin, even when the initiator of the navigation is not the parent or |
| // opener of the frame targeted by the navigation. In the |
| // TopToAboutBlank_CrossSite testcase, the top-level navigation is initiated by |
| // a cross-site subframe. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, TopToAboutBlank_CrossSite) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Verify the desired properties of the test setup. |
| RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderFrameHostImpl* child_frame = |
| main_frame->child_at(0)->current_frame_host(); |
| if (AreStrictSiteInstancesEnabled()) { |
| EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); |
| } else { |
| EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); |
| } |
| url::Origin a_origin = |
| url::Origin::Create(embedded_test_server()->GetURL("a.com", "/")); |
| url::Origin b_origin = |
| url::Origin::Create(embedded_test_server()->GetURL("b.com", "/")); |
| EXPECT_EQ(a_origin, main_frame->GetLastCommittedOrigin()); |
| EXPECT_EQ(b_origin, child_frame->GetLastCommittedOrigin()); |
| |
| // Have the subframe initiate navigation of the main frame to about:blank. |
| // |
| // (Note that this scenario is a bit artificial/silly, because the final |
| // about:blank frame won't have any same-origin friends that could populate |
| // it. OTOH, it is still important to maintain all the invariants in this |
| // scenario. And it is still possible that a same-origin frame (e.g. in |
| // another window in the same BrowsingInstance) exists and can populate the |
| // about:blank frame. |
| TestNavigationObserver nav_observer(shell()->web_contents(), 1); |
| ASSERT_TRUE(ExecJs(child_frame, "window.top.location = 'about:blank'")); |
| nav_observer.Wait(); |
| |
| // Verify that the main frame is the only remaining frame and that it has the |
| // same origin as the navigation initiator. |
| main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| EXPECT_EQ(0u, main_frame->child_count()); |
| EXPECT_EQ(b_origin, main_frame->GetLastCommittedOrigin()); |
| EXPECT_EQ(GURL(url::kAboutBlankURL), main_frame->GetLastCommittedURL()); |
| } |
| |
| // The test below verifies that an "about:blank" navigation commits with the |
| // right origin, even when the initiator of the navigation is not the parent or |
| // opener of the frame targeted by the navigation. In the |
| // SameSiteSiblingToAboutBlank_CrossSiteTop testcase, the navigation is |
| // initiated by a same-origin sibling (notably, not by one of target frame's |
| // ancestors) and both siblings are subframes of a cross-site main frame. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, |
| SameSiteSiblingToAboutBlank_CrossSiteTop) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b,b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Name the 2nd child. |
| RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderFrameHostImpl* child_frame1 = |
| main_frame->child_at(0)->current_frame_host(); |
| RenderFrameHostImpl* child_frame2 = |
| main_frame->child_at(1)->current_frame_host(); |
| ASSERT_TRUE(ExecJs(child_frame2, "window.name = 'child2'")); |
| |
| // Grab `child2` window from the 1st child... |
| ASSERT_TRUE(ExecJs(child_frame1, "child2 = window.open('', 'child2')")); |
| // ...but make sure that child2's opener doesn't point to child1. |
| ASSERT_TRUE(ExecJs(main_frame, "child2 = window.open('', 'child2')")); |
| EXPECT_EQ(true, EvalJs(child_frame2, "window.opener == window.top")); |
| |
| // From child1 initiate navigation of child2 to about:blank. |
| TestNavigationObserver nav_observer(shell()->web_contents(), 1); |
| ASSERT_TRUE(ExecJs(child_frame1, "child2.location = 'about:blank'")); |
| nav_observer.Wait(); |
| |
| // Verify that child2 has the origin of the initiator of the navigation. |
| main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| child_frame1 = main_frame->child_at(0)->current_frame_host(); |
| child_frame2 = main_frame->child_at(1)->current_frame_host(); |
| VerifyResultsOfAboutBlankNavigation(child_frame2, child_frame1); |
| } |
| |
| // The test below verifies that an initial empty document has a functional |
| // URLLoaderFactory. Note that some aspects of the current behavior (e.g. the |
| // synchronous re-navigation) are not spec-compliant - see |
| // https://crbug.com/778318 and https://github.com/whatwg/html/issues/3267. |
| // Note that the same behavior is expected in the ...NewFrameWithoutSrc and |
| // in the ...NewFrameWithAboutBlank testcases. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_NewFrameWithoutSrc) { |
| GURL opener_url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), opener_url)); |
| |
| // This inserts an `iframe` element without an `src` attribute. According to |
| // some specs "the browsing context will remain at the initial about:blank |
| // page", although other specs suggest that there is an explicit, separate |
| // navigation. See: |
| // https://html.spec.whatwg.org/dev/iframe-embed-object.html#the-iframe-element |
| // https://html.spec.whatwg.org/multipage/iframe-embed-object.html#shared-attribute-processing-steps-for-iframe-and-frame-elements |
| ASSERT_TRUE(ExecJs(shell(), R"( let ifr = document.createElement('iframe'); |
| document.body.appendChild(ifr); )")); |
| WaitForLoadStop(shell()->web_contents()); |
| RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHost* subframe = ChildFrameAt(main_frame, 0); |
| |
| VerifyResultsOfAboutBlankNavigation(subframe, main_frame); |
| } |
| |
| // See the doc comment for the |
| // URLLoaderFactoryInInitialEmptyDoc_NewFrameWithoutSrc test case. |
| IN_PROC_BROWSER_TEST_F( |
| SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_NewFrameWithAboutBlank) { |
| GURL opener_url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), opener_url)); |
| |
| ASSERT_TRUE(ExecJs(shell(), R"( ifr = document.createElement('iframe'); |
| ifr.src = 'about:blank'; |
| document.body.appendChild(ifr); )")); |
| WaitForLoadStop(shell()->web_contents()); |
| RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHost* subframe = ChildFrameAt(main_frame, 0); |
| |
| VerifyResultsOfAboutBlankNavigation(subframe, main_frame); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameOriginFlagOfSameOriginAboutBlankNavigation) { |
| GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html")); |
| GURL iframe_url(embedded_test_server()->GetURL("a.com", "/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), parent_url)); |
| |
| EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"( |
| let iframe = document.createElement('iframe'); |
| iframe.src = $1; |
| document.body.appendChild(iframe); |
| )", |
| iframe_url))); |
| WaitForLoadStop(shell()->web_contents()); |
| |
| base::RunLoop loop; |
| DidFinishNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| ASSERT_TRUE(handle->HasCommitted()); |
| EXPECT_TRUE(handle->IsSameOrigin()); |
| loop.Quit(); |
| })); |
| |
| // Changing the src to trigger DidFinishNavigationCallback |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| document.querySelector("iframe").src = 'about:blank'; |
| )")); |
| loop.Run(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameOriginFlagOfCrossOriginAboutBlankNavigation) { |
| GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html")); |
| GURL iframe_url(embedded_test_server()->GetURL("b.com", "/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), parent_url)); |
| |
| EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"( |
| let iframe = document.createElement('iframe'); |
| iframe.src = $1; |
| document.body.appendChild(iframe); |
| )", |
| iframe_url))); |
| WaitForLoadStop(shell()->web_contents()); |
| |
| base::RunLoop loop; |
| DidFinishNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| ASSERT_TRUE(handle->HasCommitted()); |
| EXPECT_FALSE(handle->IsSameOrigin()); |
| loop.Quit(); |
| })); |
| |
| // Changing the src to trigger DidFinishNavigationCallback |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| document.querySelector("iframe").src = 'about:blank'; |
| )")); |
| loop.Run(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameOriginFlagOfSrcdocNavigation) { |
| GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); |
| GURL cross_origin = embedded_test_server()->GetURL("b.com", "/empty.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Navigating to about:srcdoc from the initial empty document is always a |
| // same-origin navigation: |
| // - about:srcdoc is same-origin with the parent. |
| // - the initial empty document is same-origin with the parent. |
| { |
| base::RunLoop loop; |
| DidFinishNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| ASSERT_TRUE(handle->HasCommitted()); |
| EXPECT_TRUE(handle->IsSameOrigin()); |
| loop.Quit(); |
| })); |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| let iframe = document.createElement('iframe'); |
| iframe.srcdoc = "dummy content"; |
| document.body.appendChild(iframe); |
| )")); |
| loop.Run(); |
| } |
| |
| // Now, navigate cross-origin, and back to about:srcdoc with a brand new |
| // iframe. The navigation is now considered cross-origin. |
| // - the previous document is cross-origin with the parent. |
| // - about:srcdoc is same-origin with the parent. |
| { |
| EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"( |
| let iframe2 = document.createElement('iframe'); |
| iframe2.src = $1; |
| iframe2.id = 'iframe2'; |
| document.body.appendChild(iframe2); |
| )", |
| cross_origin))); |
| WaitForLoadStop(shell()->web_contents()); |
| |
| base::RunLoop loop; |
| DidFinishNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| ASSERT_TRUE(handle->HasCommitted()); |
| EXPECT_FALSE(handle->IsSameOrigin()); |
| loop.Quit(); |
| })); |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| document.getElementById("iframe2").srcdoc = "dummy content"; |
| )")); |
| loop.Run(); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| SameOriginFlagOfAboutBlankToAboutBlankNavigation) { |
| GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html")); |
| GURL iframe_url(embedded_test_server()->GetURL("b.com", "/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), parent_url)); |
| |
| EXPECT_TRUE(ExecJs(main_frame(), JsReplace(R"( |
| let iframe = document.createElement('iframe'); |
| iframe.src = $1; |
| document.body.appendChild(iframe); |
| )", |
| iframe_url))); |
| WaitForLoadStop(shell()->web_contents()); |
| |
| // Test a same-origin about:blank navigation |
| { |
| base::RunLoop loop; |
| DidFinishNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| ASSERT_TRUE(handle->HasCommitted()); |
| EXPECT_TRUE(handle->IsSameOrigin()); |
| loop.Quit(); |
| })); |
| RenderFrameHostImpl* child_document = |
| current_frame_host()->child_at(0)->current_frame_host(); |
| EXPECT_TRUE(ExecJs(child_document, R"(location.href = "about:blank";)")); |
| loop.Run(); |
| } |
| |
| // Test another same-origin about:blank navigation |
| { |
| base::RunLoop loop; |
| DidFinishNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| ASSERT_TRUE(handle->HasCommitted()); |
| EXPECT_TRUE(handle->IsSameOrigin()); |
| loop.Quit(); |
| })); |
| RenderFrameHostImpl* child_document = |
| current_frame_host()->child_at(0)->current_frame_host(); |
| EXPECT_TRUE(ExecJs(child_document, R"(location.href = "about:blank";)")); |
| loop.Run(); |
| } |
| |
| // Test a cross-origin about:blank navigation |
| { |
| base::RunLoop loop; |
| DidFinishNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| ASSERT_TRUE(handle->HasCommitted()); |
| EXPECT_FALSE(handle->IsSameOrigin()); |
| loop.Quit(); |
| })); |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| document.querySelector('iframe').src = "about:blank"; |
| )")); |
| loop.Run(); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameOriginOfSandboxedIframe) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/empty.html"))); |
| |
| base::RunLoop loop; |
| DidFinishNavigationCallback callback( |
| shell()->web_contents(), |
| base::BindLambdaForTesting([&](NavigationHandle* handle) { |
| ASSERT_TRUE(handle->HasCommitted()); |
| // TODO(crbug.com/40092527) Take sandbox into account. Same Origin |
| // should be true |
| EXPECT_FALSE(handle->IsSameOrigin()); |
| loop.Quit(); |
| })); |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| let iframe = document.createElement('iframe'); |
| iframe.sandbox = "allow-scripts"; |
| iframe.src = "/empty.html"; |
| document.body.appendChild(iframe); |
| )")); |
| loop.Run(); |
| } |
| |
| // The test below verifies that an initial empty document has a functional |
| // URLLoaderFactory. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_NewPopupToEmptyUrl) { |
| GURL opener_url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), opener_url)); |
| |
| WebContentsImpl* popup = nullptr; |
| { |
| WebContentsAddedObserver popup_observer; |
| ASSERT_TRUE(ExecJs(shell(), "window.open('', '_blank')")); |
| popup = static_cast<WebContentsImpl*>(popup_observer.GetWebContents()); |
| } |
| WaitForLoadStop(popup); |
| |
| // Verify that we are at the initial empty document. |
| EXPECT_EQ(1, popup->GetController().GetEntryCount()); |
| EXPECT_TRUE(popup->GetController().GetLastCommittedEntry()->IsInitialEntry()); |
| EXPECT_TRUE( |
| popup->GetPrimaryFrameTree().root()->is_on_initial_empty_document()); |
| |
| // Verify that the `popup` is at "about:blank", with expected origin, with |
| // working `document.cookie`, and with working subresource loads. |
| VerifyResultsOfAboutBlankNavigation( |
| popup->GetPrimaryMainFrame(), |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| } |
| |
| // See the doc comment for the |
| // URLLoaderFactoryInInitialEmptyDoc_NewPopupToEmptyUrl test case. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_NewPopupToAboutBlank) { |
| GURL opener_url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), opener_url)); |
| |
| WebContentsImpl* popup = nullptr; |
| { |
| WebContentsAddedObserver popup_observer; |
| ASSERT_TRUE(ExecJs(shell(), "window.open('about:blank', '_blank')")); |
| popup = static_cast<WebContentsImpl*>(popup_observer.GetWebContents()); |
| } |
| WaitForLoadStop(popup); |
| |
| // Verify that we are at the synchronously committed about:blank document. |
| EXPECT_EQ(1, popup->GetController().GetEntryCount()); |
| EXPECT_TRUE(popup->GetController().GetLastCommittedEntry()->IsInitialEntry()); |
| EXPECT_TRUE( |
| popup->GetPrimaryFrameTree().root()->is_on_initial_empty_document()); |
| |
| // Verify other about:blank things. |
| VerifyResultsOfAboutBlankNavigation( |
| popup->GetPrimaryMainFrame(), |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| } |
| |
| // The test below verifies that error pages have a functional URLLoaderFactory. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, URLLoaderFactoryInErrorPage) { |
| GURL error_url(embedded_test_server()->GetURL("/close-socket")); |
| EXPECT_FALSE(NavigateToURL(shell(), error_url)); |
| VerifyImageSubresourceLoads(shell()->web_contents()->GetPrimaryMainFrame()); |
| } |
| |
| // The test below verifies that an initial empty document has a functional |
| // URLLoaderFactory. |
| IN_PROC_BROWSER_TEST_F( |
| SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_HungNavigationInSubframe) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Add a subframe that will never commit a navigation (i.e. that will be stuck |
| // on the initial empty document). |
| const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung"); |
| ASSERT_TRUE( |
| ExecJs(shell(), JsReplace(R"(ifr = document.createElement('iframe'); |
| ifr.src = $1; |
| document.body.appendChild(ifr); )", |
| hung_url))); |
| |
| // No process swaps are expected before ReadyToCommit (which will never happen |
| // for a navigation to "/hung"). This test assertion double-checks that the |
| // test will cover inheriting URLLoaderFactory from the creator/opener/parent |
| // frame. |
| RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHost* subframe = ChildFrameAt(main_frame, 0); |
| EXPECT_EQ(main_frame->GetProcess()->GetDeprecatedID(), |
| subframe->GetProcess()->GetDeprecatedID()); |
| |
| // Ask the parent to script the same-origin subframe and trigger some HTTP |
| // subresource loads within the subframe. |
| // |
| // This tests the functionality of the URLLoaderFactory that gets used by the |
| // initial empty document. In this test, the `request_initiator` will be a |
| // non-opaque origin - it requires that the URLLoaderFactory will have a |
| // matching `request_initiator_origin_lock` (e.g. inherited from the parent). |
| VerifyImageSubresourceLoads(shell(), "ifr.contentDocument"); |
| } |
| |
| // The test below verifies that an initial empty document has a functional |
| // URLLoaderFactory. |
| IN_PROC_BROWSER_TEST_F( |
| SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_HungNavigationInPopup) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Open a popup window that will never commit a navigation (i.e. that will be |
| // stuck on the initial empty document). |
| const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung"); |
| WebContents* popup = nullptr; |
| { |
| WebContentsAddedObserver popup_observer; |
| ASSERT_TRUE( |
| ExecJs(shell(), JsReplace("popup = window.open($1)", hung_url))); |
| popup = popup_observer.GetWebContents(); |
| } |
| |
| // No process swaps are expected before ReadyToCommit (which will never happen |
| // for a navigation to "/hung"). This test assertion double-checks that the |
| // test will cover inheriting URLLoaderFactory from the creator/opener/parent |
| // frame. |
| RenderFrameHost* opener_frame = |
| shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame(); |
| EXPECT_EQ(opener_frame->GetProcess()->GetDeprecatedID(), |
| popup_frame->GetProcess()->GetDeprecatedID()); |
| |
| // Ask the opener to script the (same-origin) popup window and trigger some |
| // HTTP subresource loads within the popup. |
| // |
| // This tests the functionality of the URLLoaderFactory that gets used by the |
| // initial empty document. In this test, the `request_initiator` will be a |
| // non-opaque origin - it requires that the URLLoaderFactory will have a |
| // matching `request_initiator_origin_lock` (e.g. inherited from the opener). |
| VerifyImageSubresourceLoads(shell(), "popup.document"); |
| |
| // TODO(crbug.com/40758605): Crash recovery doesn't work when there is |
| // no opener. |
| DontTestNetworkServiceCrashes(); |
| // Test again after closing the opener.. |
| shell()->Close(); |
| VerifyImageSubresourceLoads(popup); |
| } |
| |
| // The test below verifies that an initial empty document has a functional |
| // URLLoaderFactory. The ...WithClearedOpener testcase is a regression test for |
| // https://crbug.com/1191203. |
| IN_PROC_BROWSER_TEST_F( |
| SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_HungNavigationInPopupWithClearedOpener) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Open a new window that will never commit a navigation (i.e. that will be |
| // stuck on the initial empty document). Clearing of `popup.opener` tests if |
| // inheriting of URLLoaderFactory from the opener will work when the opener |
| // has been cleared in DOM/Javascript. |
| const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung"); |
| const char kScriptTemplate[] = R"( |
| popup = window.open($1); |
| popup.opener = null; |
| )"; |
| content::WebContents* popup = nullptr; |
| { |
| WebContentsAddedObserver popup_observer; |
| ASSERT_TRUE(ExecJs(shell(), JsReplace(kScriptTemplate, hung_url))); |
| popup = popup_observer.GetWebContents(); |
| } |
| |
| // No process swaps are expected before ReadyToCommit (which will never happen |
| // for a navigation to "/hung"). This test assertion double-checks that the |
| // test will cover inheriting URLLoaderFactory from the creator/opener/parent |
| // frame. This differentiates the test from the "noopener" case covered in |
| // another testcase. |
| RenderFrameHost* opener_frame = |
| shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame(); |
| EXPECT_EQ(opener_frame->GetProcess()->GetDeprecatedID(), |
| popup_frame->GetProcess()->GetDeprecatedID()); |
| |
| // Double-check that the popup didn't commit any navigation and that it has |
| // an the same origin as the initial opener. |
| EXPECT_EQ(GURL(), popup->GetPrimaryMainFrame()->GetLastCommittedURL()); |
| EXPECT_NE("null", EvalJs(popup, "window.origin")); |
| EXPECT_EQ(shell() |
| ->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetLastCommittedOrigin() |
| .Serialize(), |
| EvalJs(popup, "window.origin")); |
| |
| // Use the parent frame's `popup` reference to script the same-origin popup |
| // window and trigger some HTTP subresource loads within the popup. |
| // |
| // This tests the functionality of the URLLoaderFactory that gets used by the |
| // initial empty document. In this test, the `request_initiator` will be a |
| // non-opaque origin - it requires that the URLLoaderFactory will have a |
| // matching `request_initiator_origin_lock` (e.g. inherited from the opener). |
| VerifyImageSubresourceLoads(popup); |
| |
| // TODO(crbug.com/40758605): Crash recovery doesn't work when there is |
| // no opener. |
| DontTestNetworkServiceCrashes(); |
| // Test again after closing the opener.. |
| shell()->Close(); |
| VerifyImageSubresourceLoads(popup); |
| } |
| |
| // The test below verifies that an initial empty document has a functional |
| // URLLoaderFactory. |
| IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_204NoOpenerPopup) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Open a new window by following a no-opener link to /nocontent (204). |
| const GURL no_content_url = |
| embedded_test_server()->GetURL("a.com", "/nocontent"); |
| const char kScriptTemplate[] = R"( |
| let anchor = document.createElement('a'); |
| anchor.href = $1; |
| anchor.rel = 'noopener'; |
| anchor.target = '_blank'; |
| anchor.innerText = 'test link'; |
| document.body.appendChild(anchor); |
| anchor.click(); |
| )"; |
| content::WebContents* popup = nullptr; |
| { |
| WebContentsAddedObserver popup_observer; |
| ASSERT_TRUE(ExecJs(shell(), JsReplace(kScriptTemplate, no_content_url))); |
| popup = popup_observer.GetWebContents(); |
| } |
| WaitForLoadStop(popup); |
| |
| // Double-check that the `popup` didn't commit any navigation and that it has |
| // an opaque origin. |
| EXPECT_EQ(GURL(), popup->GetPrimaryMainFrame()->GetLastCommittedURL()); |
| EXPECT_EQ("null", EvalJs(popup, "window.origin")); |
| |
| // Process swap is expected because of 'noopener'. This test assertion |
| // double-checks that in the test it is not possible to inheriting |
| // URLLoaderFactory from the creator/opener/parent frame (because the popup is |
| // in another process). |
| RenderFrameHost* opener_frame = |
| shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame(); |
| EXPECT_NE(opener_frame->GetProcess()->GetDeprecatedID(), |
| popup_frame->GetProcess()->GetDeprecatedID()); |
| |
| // Inject Javascript that triggers some subresource loads over HTTP. |
| // |
| // To some extent, this simulates an ability of 1) Android WebView (see |
| // https://crbug.com/1189838) and 2) Chrome Extensions, to inject Javascript |
| // into an initial empty document (even when no web/renderer content has |
| // access to the document). |
| // |
| // This tests the functionality of the URLLoaderFactory that gets used by the |
| // initial empty document. In this test, the `request_initiator` will be an |
| // opaque, unique origin (since nothing has committed yet) and will be |
| // compatible with `request_initiator_origin_lock` of the URLLoaderFactory. |
| VerifyImageSubresourceLoads(popup); |
| } |
| |
| // The test below verifies that an initial empty document has a functional |
| // URLLoaderFactory. |
| IN_PROC_BROWSER_TEST_F( |
| SubresourceLoadingTest, |
| URLLoaderFactoryInInitialEmptyDoc_HungNavigationInNewWindow) { |
| // Open a new shell, starting at the "/hung" URL. |
| const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung"); |
| Shell* new_shell = |
| Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(), |
| hung_url, nullptr, gfx::Size()); |
| |
| // Wait until the renderer process launches (this will flush the CreateView |
| // IPC and make sure that ExecJs and EvalJs are able to work). |
| RenderFrameHost* main_frame = |
| new_shell->web_contents()->GetPrimaryMainFrame(); |
| { |
| RenderProcessHostWatcher process_watcher( |
| main_frame->GetProcess(), |
| RenderProcessHostWatcher::WATCH_FOR_PROCESS_READY); |
| process_watcher.Wait(); |
| } |
| |
| // Double-check that the new shell didn't commit any navigation and that it |
| // has an opaque origin. |
| EXPECT_EQ(1, new_shell->web_contents()->GetController().GetEntryCount()); |
| EXPECT_TRUE(new_shell->web_contents() |
| ->GetController() |
| .GetLastCommittedEntry() |
| ->IsInitialEntry()); |
| EXPECT_EQ(GURL(), main_frame->GetLastCommittedURL()); |
| EXPECT_EQ("null", EvalJs(main_frame, "window.origin")); |
| |
| // Inject Javascript that triggers some subresource loads over HTTP. |
| // |
| // To some extent, this simulates an ability of 1) Android WebView (see |
| // https://crbug.com/1189838) and 2) Chrome Extensions, to inject Javascript |
| // into an initial empty document (even when no web/renderer content has |
| // access to the document). |
| // |
| // This tests the functionality of the URLLoaderFactory that gets used by the |
| // initial empty document. In this test, the `request_initiator` will be an |
| // opaque, unique origin (since nothing has committed yet) and will be |
| // compatible with `request_initiator_origin_lock` of the URLLoaderFactory. |
| VerifyImageSubresourceLoads(main_frame); |
| } |
| |
| namespace { |
| |
| struct Result { |
| GURL url; |
| std::optional<url::Origin> origin; |
| bool committed; |
| }; |
| |
| class NavigationLogger : public WebContentsObserver { |
| public: |
| explicit NavigationLogger(WebContents* contents) |
| : WebContentsObserver(contents) {} |
| |
| // WebContentsObserver overrides: |
| void DidFinishNavigation(NavigationHandle* handle) override { |
| if (handle->HasCommitted()) { |
| EXPECT_EQ(handle->GetRenderFrameHost()->GetLastCommittedURL(), |
| handle->GetURL()); |
| RenderFrameHost* rfh = handle->GetRenderFrameHost(); |
| results_.push_back({.url = rfh->GetLastCommittedURL(), |
| .origin = rfh->GetLastCommittedOrigin(), |
| .committed = true}); |
| } else { |
| results_.push_back({.url = handle->GetURL(), .committed = false}); |
| } |
| } |
| |
| const std::vector<Result>& results() const { return results_; } |
| |
| private: |
| std::vector<Result> results_; |
| }; |
| |
| } // namespace |
| |
| class UndoCommitNavigationBrowserTest : public NavigationBrowserTest { |
| public: |
| UndoCommitNavigationBrowserTest() { |
| std::map<std::string, std::string> parameters = { |
| {"queueing_level", "none"}, |
| }; |
| // Note that RenderDocument needs to be disabled so that it won't enable |
| // navigation queueing automatically. |
| feature_list_.InitWithFeaturesAndParameters( |
| /*enabled_features=*/{{features::kQueueNavigationsWhileWaitingForCommit, |
| parameters}}, |
| /*disabled_features=*/{features::kRenderDocument}); |
| } |
| |
| void SetUpOnMainThread() override { |
| // These navigation tests require full site isolation since they test races |
| // with committing a navigation in a speculative RenderFrameHost.. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP() << "Site isolation is not enabled!"; |
| } |
| |
| NavigationBrowserTest::SetUpOnMainThread(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| NavigationBrowserTest::SetUpCommandLine(command_line); |
| |
| // PerformanceManager maintains its own parallel frame tree and has |
| // sometimes been confused by things like `UndoCommitNavigation()`. |
| // Force-enable it for test coverage; otherwise, by default, |
| // PerformanceManager uses the dummy implementation. |
| // |
| // TODO(crbug.com/40187286): Enable this by default in content_shell. |
| command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, |
| "PerformanceManagerInstrumentation"); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // A helper that invokes `functor` on the next `DidStartNavigation()`. |
| template <typename F> |
| void OnNextDidStartNavigation(WebContents* web_contents, F&& functor) { |
| class Observer : public WebContentsObserver { |
| public: |
| using Callback = base::OnceCallback<void(NavigationHandle*)>; |
| |
| explicit Observer(WebContents* web_contents, Callback callback) |
| : WebContentsObserver(web_contents), callback_(std::move(callback)) {} |
| |
| // WebContentsObserver overrides: |
| void DidStartNavigation(NavigationHandle* handle) override { |
| std::move(callback_).Run(handle); |
| delete this; |
| } |
| |
| private: |
| Callback callback_; |
| }; |
| |
| new Observer(web_contents, |
| base::BindLambdaForTesting(std::forward<F>(functor))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(UndoCommitNavigationBrowserTest, |
| PerformanceManagerFrameTreeConsistency) { |
| // PerformanceManager reports when a remote frame is attached to a local |
| // parent, and it was previously getting confused by the fact that a |
| // `blink::RemoteFrame` with matching RemoteFrameTokens was being reported as |
| // attached twice: once by the initial page loaded in the next statement, and |
| // the next when the browser needs to send a `UndoCommitNavigation()` to the |
| // a.com renderer. |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* first_subframe_node = |
| web_contents->GetPrimaryMainFrame()->child_at(0); |
| RenderProcessHost* const a_com_render_process_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->current_frame_host() |
| ->GetProcess(); |
| |
| NavigationLogger logger(web_contents); |
| |
| // Start a navigation that will create a speculative RFH in the existing |
| // render process for a.com. |
| const GURL infinitely_loading_url = |
| embedded_test_server()->GetURL("a.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(web_contents, |
| infinitely_loading_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, |
| infinitely_loading_url)); |
| rfh_observer.Wait(); |
| |
| // Ensure the speculative RFH is in the expected process. |
| RenderFrameHostImpl* speculative_render_frame_host = |
| first_subframe_node->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(a_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and ignore) the next `DidCommitProvisionalLoad()` for a.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| // Update the id attribute to exercise a PerformanceManager-specific code |
| // path: when the renderer swaps in a `blink::RemoteFrame` to undo the |
| // `CommitNavigation()`, it will report the iframe attribution data again. |
| // PerformanceManager should not complain that V8ContextTracker already has |
| // the iframe attribution data, nor should it update the iframe attribution |
| // data, to preserve existing behavior (unfortunately, the latter part is not |
| // really tested in this browser test). |
| EXPECT_TRUE(ExecJs(web_contents, |
| "document.querySelector('iframe').id = 'new-name';")); |
| |
| // Now begin a new navigation to c.com while the previous a.com navigation |
| // above is paused in the pending commit state. |
| const GURL final_url = |
| embedded_test_server()->GetURL("c.com", "/title1.html"); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url)); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| EXPECT_EQ(final_url, first_subframe_node->render_manager() |
| ->current_frame_host() |
| ->GetLastCommittedURL()); |
| |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| // This test always uses UndoCommitNavigation, so navigation corresponding to |
| // the paused commit should never commit. |
| EXPECT_FALSE(results[0].committed); |
| EXPECT_EQ(std::nullopt, results[0].origin); |
| EXPECT_EQ(infinitely_loading_url, results[0].url); |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin); |
| EXPECT_EQ(final_url, results[1].url); |
| } |
| |
| class ResumeCommitClosureSetWaiter { |
| public: |
| explicit ResumeCommitClosureSetWaiter(NavigationHandle* handle) { |
| // `Helper` "observes" the set via its own destruction, since the navigation |
| // code setting a resume commit closure will replace the testing one |
| // installed here. |
| NavigationRequest::From(handle)->set_resume_commit_closure( |
| base::DoNothingWithBoundArgs(std::make_unique<Helper>(loop_))); |
| } |
| |
| void Wait() { loop_.Run(); } |
| |
| private: |
| class Helper { |
| public: |
| explicit Helper(base::RunLoop& loop) : loop_(loop) {} |
| |
| ~Helper() { loop_->Quit(); } |
| |
| private: |
| raw_ref<base::RunLoop> loop_; |
| }; |
| |
| base::RunLoop loop_; |
| }; |
| |
| class NavigationQueueingBrowserTest : public NavigationBrowserTest { |
| public: |
| NavigationQueueingBrowserTest() { |
| feature_list_.InitAndEnableFeatureWithParameters( |
| features::kQueueNavigationsWhileWaitingForCommit, |
| {{"queueing_level", "full"}}); |
| } |
| |
| void SetUpOnMainThread() override { |
| // These navigation tests require full site isolation since they test races |
| // with committing a navigation in a speculative RenderFrameHost. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP(); |
| } |
| |
| NavigationBrowserTest::SetUpOnMainThread(); |
| } |
| |
| const base::HistogramTester& histogram_tester() const { |
| return histogram_tester_; |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| base::HistogramTester histogram_tester_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(NavigationQueueingBrowserTest, Regular) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| NavigationLogger logger(shell()->web_contents()); |
| |
| // Start a navigation that will create a speculative RFH. |
| const GURL infinitely_loading_url = |
| embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), |
| infinitely_loading_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url)); |
| rfh_observer.Wait(); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* speculative_render_frame_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| |
| // Pause the next `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| // Now begin a new navigation to c.com while the previous b.com navigation |
| // above is paused in the pending commit state. |
| { |
| const GURL next_url = |
| embedded_test_server()->GetURL("c.com", "/title1.html"); |
| |
| std::optional<ResumeCommitClosureSetWaiter> |
| resume_commit_closure_set_waiter; |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), next_url)); |
| resume_commit_closure_set_waiter->Wait(); |
| } |
| |
| // Cancel the c.com navigation that was previously queued and (nearly) ready |
| // for commit, to make sure one pending commit navigation that causes multiple |
| // subsequent navigations to queue is correctly counted in the metrics. |
| const GURL final_url = |
| embedded_test_server()->GetURL("d.com", "/title1.html"); |
| std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), final_url)); |
| resume_commit_closure_set_waiter->Wait(); |
| |
| commit_pauser.ResumePausedCommit(); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); |
| |
| // Both the b.com commit and the d.com commit should record the PendingCommit |
| // time. |
| histogram_tester().ExpectTotalCount( |
| "Navigation.PendingCommit.Duration.Regular", 2); |
| // The d.com navigation should not have blocked any navigation requests from |
| // making progress. |
| histogram_tester().ExpectBucketCount( |
| "Navigation.PendingCommit.DidBlockGetFrameHostForNavigation.Regular", |
| false, 1); |
| // But the b.com navigation blocked c.com and d.com. |
| histogram_tester().ExpectBucketCount( |
| "Navigation.PendingCommit.DidBlockGetFrameHostForNavigation.Regular", |
| true, 1); |
| if (base::FeatureList::IsEnabled(features::kDeferSpeculativeRFHCreation)) { |
| // For 2 blocked navigations, 2 total blocks are expected when trying to |
| // pick a final RenderFrameHost to commit the navigation. The attempt to |
| // create a RenderFrameHost when starting the navigation will be skipped. |
| histogram_tester().ExpectBucketCount( |
| "Navigation.PendingCommit.BlockedCount.Regular", 2, 1); |
| } else { |
| // For 2 blocked navigations, 4 total blocks are expected: 2 when trying to |
| // assign a RenderFrameHost when starting a navigation, and 2 when trying to |
| // pick a final RenderFrameHost to commit the navigation. |
| histogram_tester().ExpectBucketCount( |
| "Navigation.PendingCommit.BlockedCount.Regular", 4, 1); |
| } |
| histogram_tester().ExpectBucketCount( |
| "Navigation.PendingCommit.BlockedCommitCount.Regular", 2, 1); |
| } |
| |
| class CommitNavigationRaceBrowserTest |
| : public NavigationBrowserTest, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| CommitNavigationRaceBrowserTest() { |
| std::map<std::string, std::string> parameters = { |
| {"queueing_level", GetParam() ? "full" : "none"}, |
| }; |
| feature_list_.InitAndEnableFeatureWithParameters( |
| features::kQueueNavigationsWhileWaitingForCommit, parameters); |
| } |
| |
| void SetUpOnMainThread() override { |
| // These navigation tests require full site isolation since they test races |
| // with committing a navigation in a speculative RenderFrameHost.. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP(); |
| } |
| |
| NavigationBrowserTest::SetUpOnMainThread(); |
| } |
| |
| static std::string DescribeParams( |
| const testing::TestParamInfo<ParamType>& info) { |
| return info.param ? "NavigationQueueing" : "UndoCommitNavigation"; |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Test for https://crbug.com/40187807 and https://crbug.com/332746903. |
| // |
| // Ensure that racing a navigation commit in a speculative/provisional child |
| // frame in render process B with a detach IPC from render process A (i.e. the |
| // child frame's parent is in render process A and has removed the frame owner |
| // element—e.g. <iframe>—from the DOM) does not result in the detach IPC being |
| // discarded and never received by render process B. |
| IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, |
| DetachAfterCommitNavigationInSubFrame) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b,a)"))); |
| |
| WebContentsImpl* const web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* const first_subframe_node = |
| web_contents->GetPrimaryMainFrame()->child_at(0); |
| FrameTreeNode* const second_subframe_node = |
| web_contents->GetPrimaryMainFrame()->child_at(1); |
| RenderProcessHost* const b_com_render_process_host = |
| first_subframe_node->render_manager()->current_frame_host()->GetProcess(); |
| |
| // Start a navigation in the second child frame that will create a speculative |
| // RFH in the existing render process for b.com. The first child frame is |
| // already hosted in the render process for b.com: this is to ensure the |
| // render process remains live even after the second child frame is detached |
| // later in this test. |
| GURL b_url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| SpeculativeRenderFrameHostObserver observer(shell()->web_contents(), b_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(second_subframe_node, b_url)); |
| |
| // Ensure the speculative RFH is in the expected process. |
| observer.Wait(); |
| RenderFrameHostImpl* speculative_render_frame_host = |
| second_subframe_node->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and ignore) the next `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| // At this point, the b.com renderer has already committed the RenderFrame, |
| // but on the browser side, the RenderFrameHost is still speculative. |
| |
| // Intentionally do not wait for script completion here. This runs an event |
| // loop that pumps incoming messages, but IPCs from b.com would be processed |
| // out of order, since the `DidCommitProvisionalLoad()` attempt was previously |
| // paused above. |
| ExecuteScriptAsync( |
| web_contents, |
| JsReplace("document.querySelectorAll('iframe')[1].remove()")); |
| |
| // However, since it's not possible to wait for `remove()` to take effect, |
| // the test must cheat a little and directly call the Mojo IPC that the JS |
| // above would eventually trigger. |
| second_subframe_node->render_manager()->current_frame_host()->Detach(); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| // Validate that render process for b.com has handled the detach message for |
| // the provisional frame that was committing. Before the fix: |
| // - without navigation queueing, the render process for b.com still had the |
| // proxy for the second child frame, because the browser process's request |
| // to delete it was sent via a broken message pipe. Thus, the frame tree in |
| // the render process for b.com incorrectly thought there were still two |
| // child frames. |
| // - with navigation queueing, the render process for b.com has already |
| // committed the navigation in the second child frame, so the renderer-side |
| // proxy has already been destroyed and replaced. However, the browser |
| // process has not heard about the commit yet and sends a detach to the |
| // proxy, which the renderer ignores since it no longer exists. |
| EXPECT_EQ(1, EvalJs(first_subframe_node, "top.length")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, |
| BeginNewNavigationDuringCommitNavigationInMainFrame) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Prior to implementing the UndoCommitNavigation() workaround, the race |
| // condition being tested would result in a crash in the b.com renderer. Open |
| // another b.com window in the same browsing instance to verify that the b.com |
| // renderer does not unexpectedly crash even if the b.com speculative |
| // RenderFrameHost is discarded. |
| ASSERT_TRUE(ExecJs( |
| shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL( |
| "b.com", "/title1.html")))); |
| ASSERT_EQ(2u, Shell::windows().size()); |
| WebContentsImpl* new_web_contents = |
| static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); |
| WaitForLoadStop(new_web_contents); |
| RenderProcessHost* const b_com_render_process_host = |
| new_web_contents->GetPrimaryMainFrame()->GetProcess(); |
| |
| NavigationLogger logger(shell()->web_contents()); |
| |
| // Start a navigation that will create a speculative RFH in the existing |
| // render process for b.com. |
| const GURL infinitely_loading_url = |
| embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), |
| infinitely_loading_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url)); |
| rfh_observer.Wait(); |
| |
| // Ensure the speculative RFH is in the expected process (i.e. the b.com |
| // process that was created for the navigation in the new window earlier). |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* speculative_render_frame_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and potentially ignore, if navigation queueing is disabled) the next |
| // `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // If navigation queueing is enabled, the test should verify that a resume |
| // commit closure is actually set. Install a watcher now, before beginning |
| // the next navigation, since the resume commit closure may be synchronously |
| // set while handling the `BeginNavigation()` IPC in the browser. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| |
| // Now begin a new navigation to c.com while the previous b.com navigation |
| // above is paused in the pending commit state. |
| const GURL final_url = |
| embedded_test_server()->GetURL("c.com", "/title1.html"); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url)); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| resume_commit_closure_set_waiter->Wait(); |
| commit_pauser.ResumePausedCommit(); |
| } |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); |
| |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| EXPECT_EQ(infinitely_loading_url, results[0].url); |
| // If navigation queueing is enabled, the first navigation will complete the |
| // commit as the new navigation gets queued until the first navigation's |
| // commit finished. If navigation queueing is disabled, the pending commit |
| // navigation will be cancelled. |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| EXPECT_TRUE(results[0].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); |
| } else { |
| EXPECT_FALSE(results[0].committed); |
| EXPECT_EQ(std::nullopt, results[0].origin); |
| } |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin); |
| EXPECT_EQ(final_url, results[1].url); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, |
| BeginNewNavigationDuringCommitNavigationInSubFrame) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b(a)"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* first_subframe_node = |
| web_contents->GetPrimaryMainFrame()->child_at(0); |
| RenderProcessHost* const b_com_render_process_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->current_frame_host() |
| ->GetProcess(); |
| |
| NavigationLogger logger(web_contents); |
| |
| // Start a navigation that will create a speculative RFH in the existing |
| // render process for b.com. |
| const GURL infinitely_loading_url = |
| embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), |
| infinitely_loading_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, |
| infinitely_loading_url)); |
| rfh_observer.Wait(); |
| |
| // Ensure the speculative RFH is in the expected process. |
| RenderFrameHostImpl* speculative_render_frame_host = |
| first_subframe_node->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and potentially ignore, if navigation queueing is disabled) the next |
| // `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // If navigation queueing is enabled, the test should verify that a resume |
| // commit closure is actually set. Install a watcher now, before beginning |
| // the next navigation, since the resume commit closure may be synchronously |
| // set while handling the `BeginNavigation()` IPC in the browser. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| |
| // Now begin a new navigation to c.com while the previous b.com navigation |
| // above is paused in the pending commit state. |
| const GURL final_url = |
| embedded_test_server()->GetURL("c.com", "/title1.html"); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url)); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| resume_commit_closure_set_waiter->Wait(); |
| commit_pauser.ResumePausedCommit(); |
| } |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| EXPECT_EQ(final_url, first_subframe_node->render_manager() |
| ->current_frame_host() |
| ->GetLastCommittedURL()); |
| |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| // If navigation queueing is enabled, the first navigation will complete the |
| // commit as the new navigation gets queued until the first navigation's |
| // commit finished. If navigation queueing is disabled, the pending commit |
| // navigation will be cancelled. |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| EXPECT_TRUE(results[0].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); |
| } else { |
| EXPECT_FALSE(results[0].committed); |
| EXPECT_EQ(std::nullopt, results[0].origin); |
| } |
| EXPECT_EQ(infinitely_loading_url, results[0].url); |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin); |
| EXPECT_EQ(final_url, results[1].url); |
| } |
| |
| // Test the behavior of a navigation that gets suspended in the pending commit |
| // state followed by another navigation that results in a failed navigation. A |
| // failed navigation is not a navigation that results in an HTTP error page; it |
| // is a situation where the network request itself fails, e.g. DNS resolution |
| // failed, and Chrome commits an error page instead. |
| IN_PROC_BROWSER_TEST_P( |
| CommitNavigationRaceBrowserTest, |
| BeginNewNavigationDuringCommitFailedNavigationInMainFrame) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Prior to implementing the UndoCommitNavigation() workaround, the race |
| // condition being tested would result in a crash in the b.com renderer. Open |
| // another b.com window in the same browsing instance to verify that the b.com |
| // renderer does not unexpectedly crash even if the b.com speculative |
| // RenderFrameHost is discarded. |
| ASSERT_TRUE(ExecJs( |
| shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL( |
| "b.com", "/title1.html")))); |
| ASSERT_EQ(2u, Shell::windows().size()); |
| WebContentsImpl* new_web_contents = |
| static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); |
| WaitForLoadStop(new_web_contents); |
| RenderProcessHost* const b_com_render_process_host = |
| new_web_contents->GetPrimaryMainFrame()->GetProcess(); |
| |
| NavigationLogger logger(shell()->web_contents()); |
| |
| // Start a navigation that will create a speculative RFH in the existing |
| // render process for b.com. |
| const GURL infinitely_loading_url = |
| embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), |
| infinitely_loading_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url)); |
| rfh_observer.Wait(); |
| |
| // Ensure the speculative RFH is in the expected process (i.e. the b.com |
| // process that was created for the navigation in the new window earlier). |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* speculative_render_frame_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and potentially ignore, if navigation queueing is disabled) the next |
| // `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // If navigation queueing is enabled, the test should verify that a resume |
| // commit closure is actually set. Install a watcher now, before beginning |
| // the next navigation, since the resume commit closure may be synchronously |
| // set while handling the `BeginNavigation()` IPC in the browser. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| |
| // Now begin a new navigation to c.com while the previous b.com navigation |
| // above is paused in the pending commit state. This navigation will fail and |
| // commit an error page. |
| const GURL final_url = |
| embedded_test_server()->GetURL("c.com", "/title1.html"); |
| std::unique_ptr<URLLoaderInterceptor> url_interceptor = |
| URLLoaderInterceptor::SetupRequestFailForURL(final_url, |
| net::ERR_DNS_TIMED_OUT); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url)); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| resume_commit_closure_set_waiter->Wait(); |
| commit_pauser.ResumePausedCommit(); |
| } |
| |
| // The top-level page completes loading but is an error page, so |
| // `WaitForLoadStop()` should return false, since the navigation entry will |
| // have `PAGE_TYPE_ERROR`. |
| EXPECT_FALSE(WaitForLoadStop(web_contents)); |
| EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); |
| EXPECT_TRUE(web_contents->GetPrimaryMainFrame()->IsErrorDocument()); |
| |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| EXPECT_EQ(infinitely_loading_url, results[0].url); |
| // If navigation queueing is enabled, the first navigation will complete the |
| // commit as the new navigation gets queued until the first navigation's |
| // commit finished. If navigation queueing is disabled, the pending commit |
| // navigation will be cancelled. |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| EXPECT_TRUE(results[0].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); |
| } else { |
| EXPECT_FALSE(results[0].committed); |
| EXPECT_EQ(std::nullopt, results[0].origin); |
| } |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_TRUE(results[1].origin->opaque()); |
| EXPECT_EQ(embedded_test_server() |
| ->GetOrigin("c.com") |
| .GetTupleOrPrecursorTupleIfOpaque(), |
| results[1].origin->GetTupleOrPrecursorTupleIfOpaque()); |
| EXPECT_EQ(final_url, results[1].url); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| CommitNavigationRaceBrowserTest, |
| BeginNewNavigationDuringCommitFailedNavigationInSubFrame) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b(a)"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* first_subframe_node = |
| web_contents->GetPrimaryMainFrame()->child_at(0); |
| RenderProcessHost* const b_com_render_process_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->current_frame_host() |
| ->GetProcess(); |
| |
| NavigationLogger logger(web_contents); |
| |
| // Start a navigation that will create a speculative RFH in the existing |
| // render process for b.com. |
| const GURL infinitely_loading_url = |
| embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(web_contents, |
| infinitely_loading_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, |
| infinitely_loading_url)); |
| rfh_observer.Wait(); |
| |
| // Ensure the speculative RFH is in the expected process. |
| RenderFrameHostImpl* speculative_render_frame_host = |
| first_subframe_node->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and potentially ignore, if navigation queueing is disabled) the next |
| // `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // If navigation queueing is enabled, the test should verify that a resume |
| // commit closure is actually set. Install a watcher now, before beginning |
| // the next navigation, since the resume commit closure may be synchronously |
| // set while handling the `BeginNavigation()` IPC in the browser. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| |
| // Now begin a new navigation to c.com while the previous b.com navigation |
| // above is paused in the pending commit state. This navigation will fail and |
| // commit an error page. |
| const GURL final_url = |
| embedded_test_server()->GetURL("c.com", "/title1.html"); |
| std::unique_ptr<URLLoaderInterceptor> url_interceptor = |
| URLLoaderInterceptor::SetupRequestFailForURL(final_url, |
| net::ERR_DNS_TIMED_OUT); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url)); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| resume_commit_closure_set_waiter->Wait(); |
| commit_pauser.ResumePausedCommit(); |
| } |
| |
| // The top-level page completes loading. Unlike the main frame variant of this |
| // test, `WaitForLoadStop()` should return true, since the navigation entry |
| // will have `PAGE_TYPE_NORMAL` as only a subframe failed to load. |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| EXPECT_EQ(final_url, first_subframe_node->render_manager() |
| ->current_frame_host() |
| ->GetLastCommittedURL()); |
| EXPECT_TRUE(first_subframe_node->render_manager() |
| ->current_frame_host() |
| ->IsErrorDocument()); |
| |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| // If navigation queueing is enabled, the first navigation will complete the |
| // commit as the new navigation gets queued until the first navigation's |
| // commit finished. If navigation queueing is disabled, the pending commit |
| // navigation will be cancelled. |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| EXPECT_TRUE(results[0].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); |
| } else { |
| EXPECT_FALSE(results[0].committed); |
| EXPECT_EQ(std::nullopt, results[0].origin); |
| } |
| EXPECT_EQ(infinitely_loading_url, results[0].url); |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_TRUE(results[1].origin->opaque()); |
| EXPECT_EQ(embedded_test_server() |
| ->GetOrigin("c.com") |
| .GetTupleOrPrecursorTupleIfOpaque(), |
| results[1].origin->GetTupleOrPrecursorTupleIfOpaque()); |
| EXPECT_EQ(final_url, results[1].url); |
| } |
| |
| // about:blank navigations do not require a URL loader and go through a |
| // different path to commit the navigation in the renderer. |
| IN_PROC_BROWSER_TEST_P( |
| CommitNavigationRaceBrowserTest, |
| BeginNewNavigationWithNoUrlLoaderDuringCommitNavigationInMainFrame) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Prior to implementing the UndoCommitNavigation() workaround, the race |
| // condition being tested would result in a crash in the b.com renderer. Open |
| // another b.com window in the same browsing instance to verify that the b.com |
| // renderer does not unexpectedly crash even if the b.com speculative |
| // RenderFrameHost is discarded. |
| ASSERT_TRUE(ExecJs( |
| shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL( |
| "b.com", "/title1.html")))); |
| ASSERT_EQ(2u, Shell::windows().size()); |
| WebContentsImpl* new_web_contents = |
| static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); |
| EXPECT_TRUE(WaitForLoadStop(new_web_contents)); |
| RenderProcessHost* const b_com_render_process_host = |
| new_web_contents->GetPrimaryMainFrame()->GetProcess(); |
| |
| NavigationLogger logger(shell()->web_contents()); |
| |
| // Start a navigation that will create a speculative RFH in the existing |
| // render process for b.com. |
| const GURL infinitely_loading_url = |
| embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), |
| infinitely_loading_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url)); |
| rfh_observer.Wait(); |
| |
| // Ensure the speculative RFH is in the expected process (i.e. the b.com |
| // process that was created for the navigation in the new window earlier). |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* speculative_render_frame_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and potentially ignore, if navigation queueing is disabled) the next |
| // `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // If navigation queueing is enabled, the test should verify that a resume |
| // commit closure is actually set. Install a watcher now, before beginning |
| // the next navigation, since the resume commit closure may be synchronously |
| // set while handling the `BeginNavigation()` IPC in the browser. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| |
| // Note that this navigation is initiated by the a.com renderer, as the a.com |
| // renderer is still the current frame host for the main frame. |
| const GURL final_url("about:blank"); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url)); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| resume_commit_closure_set_waiter->Wait(); |
| commit_pauser.ResumePausedCommit(); |
| } |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); |
| |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| EXPECT_EQ(infinitely_loading_url, results[0].url); |
| // If navigation queueing is enabled, the first navigation will complete the |
| // commit as the new navigation gets queued until the first navigation's |
| // commit finished. If navigation queueing is disabled, the pending commit |
| // navigation will be cancelled. |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| EXPECT_TRUE(results[0].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); |
| } else { |
| EXPECT_FALSE(results[0].committed); |
| EXPECT_EQ(std::nullopt, results[0].origin); |
| } |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin); |
| EXPECT_EQ(final_url, results[1].url); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| CommitNavigationRaceBrowserTest, |
| BeginNewNavigationWithNoUrlLoaderDuringCommitNavigationInSubFrame) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b(a)"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* first_subframe_node = |
| web_contents->GetPrimaryMainFrame()->child_at(0); |
| RenderProcessHost* const b_com_render_process_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->current_frame_host() |
| ->GetProcess(); |
| |
| NavigationLogger logger(web_contents); |
| |
| // Start a navigation that will create a speculative RFH in the existing |
| // render process for b.com. |
| const GURL infinitely_loading_url = |
| embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), |
| infinitely_loading_url); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, |
| infinitely_loading_url)); |
| rfh_observer.Wait(); |
| |
| // Ensure the speculative RFH is in the expected process. |
| RenderFrameHostImpl* speculative_render_frame_host = |
| first_subframe_node->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // If navigation queueing is enabled, the test should verify that a resume |
| // commit closure is actually set. Install a watcher now, before beginning |
| // the next navigation, since the resume commit closure may be synchronously |
| // set while handling the `BeginNavigation()` IPC in the browser. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| |
| // Note that this navigation is initiated by the a.com renderer, as the a.com |
| // renderer is still the current frame host for the main frame. |
| const GURL final_url("about:blank"); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url)); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| resume_commit_closure_set_waiter->Wait(); |
| commit_pauser.ResumePausedCommit(); |
| } |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| EXPECT_EQ(final_url, first_subframe_node->render_manager() |
| ->current_frame_host() |
| ->GetLastCommittedURL()); |
| |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| // If navigation queueing is enabled, the first navigation will complete the |
| // commit as the new navigation gets queued until the first navigation's |
| // commit finished. If navigation queueing is disabled, the pending commit |
| // navigation will be cancelled. |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| EXPECT_TRUE(results[0].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); |
| } else { |
| EXPECT_FALSE(results[0].committed); |
| EXPECT_EQ(std::nullopt, results[0].origin); |
| } |
| EXPECT_EQ(infinitely_loading_url, results[0].url); |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin); |
| EXPECT_EQ(final_url, results[1].url); |
| } |
| |
| // Tests when a navigation is pending commit, two new navigations start one |
| // after another in the same frame. |
| IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, |
| BeginTwoNavigationsDuringCommitNavigation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| // Prior to implementing the UndoCommitNavigation() workaround, the race |
| // condition being tested would result in a crash in the b.com renderer. Open |
| // another b.com window in the same browsing instance to verify that the b.com |
| // renderer does not unexpectedly crash even if the b.com speculative |
| // RenderFrameHost is discarded. |
| ASSERT_TRUE(ExecJs( |
| shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL( |
| "b.com", "/title1.html")))); |
| ASSERT_EQ(2u, Shell::windows().size()); |
| WebContentsImpl* new_web_contents = |
| static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); |
| WaitForLoadStop(new_web_contents); |
| RenderProcessHost* const b_com_render_process_host = |
| new_web_contents->GetPrimaryMainFrame()->GetProcess(); |
| |
| NavigationLogger logger(shell()->web_contents()); |
| |
| // Start a navigation that will create a speculative RFH in the existing |
| // render process for b.com. |
| const GURL url_b = |
| embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); |
| SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), |
| url_b); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), url_b)); |
| rfh_observer.Wait(); |
| |
| // Ensure the speculative RFH is in the expected process (i.e. the b.com |
| // process that was created for the navigation in the new window earlier). |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root(); |
| RenderFrameHostImpl* speculative_render_frame_host = |
| root->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and potentially ignore, if navigation queueing is disabled) the next |
| // `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| // Now begin a new navigation to c.com while the previous b.com navigation |
| // above is paused in the pending commit state. |
| std::optional<ResumeCommitClosureSetWaiter> |
| url_c_resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // If navigation queueing is enabled, the test should verify that a resume |
| // commit closure is actually set. Install a watcher now, before beginning |
| // the `url_c` navigation, since the resume commit closure may be |
| // synchronously set while handling the `BeginNavigation()` IPC in the |
| // browser. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| url_c_resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| |
| const GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html"); |
| TestNavigationManager url_c_nav(web_contents, url_c); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_c)); |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| ASSERT_TRUE(url_c_nav.WaitForRequestStart()); |
| } else { |
| url_c_nav.WaitForSpeculativeRenderFrameHostCreation(); |
| } |
| EXPECT_EQ(url_c, root->navigation_request()->GetURL()); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // The navigation to c.com should be queued. |
| url_c_nav.ResumeNavigation(); |
| url_c_resume_commit_closure_set_waiter->Wait(); |
| } |
| |
| // Now begin another navigation to d.com, which will cancel the navigation to |
| // c.com. |
| std::optional<ResumeCommitClosureSetWaiter> |
| url_d_resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // Install a commit closure watched for the `url_d` navigation too. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| url_d_resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| const GURL url_d = embedded_test_server()->GetURL("d.com", "/title1.html"); |
| TestNavigationManager url_d_nav(web_contents, url_d); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_d)); |
| ASSERT_TRUE(url_d_nav.WaitForRequestStart()); |
| EXPECT_EQ(url_d, root->navigation_request()->GetURL()); |
| |
| // The navigation to c.com didn't commit as it was replaced by the d.com |
| // navigation. |
| EXPECT_TRUE(url_c_nav.WaitForNavigationFinished()); |
| EXPECT_FALSE(url_c_nav.was_committed()); |
| |
| // Continue the d.com navigation. |
| url_d_nav.ResumeNavigation(); |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // Wait for the `url_d` navigation to be queued, and finish the pending |
| // commit b.com navigation. |
| url_d_resume_commit_closure_set_waiter->Wait(); |
| commit_pauser.ResumePausedCommit(); |
| } |
| |
| // After all the navigations finished, we will end up in d.com. |
| EXPECT_TRUE(url_d_nav.WaitForNavigationFinished()); |
| EXPECT_EQ(url_d, web_contents->GetLastCommittedURL()); |
| |
| // Check the order of navigations finishing. |
| auto results = logger.results(); |
| ASSERT_EQ(3u, results.size()); |
| |
| EXPECT_FALSE(results[0].committed); |
| EXPECT_EQ(std::nullopt, results[0].origin); |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // When navigation queueing is enabled, the pending commit navigation to |
| // b.com won't get canceled when the c.com navigation starts. Then when the |
| // d.com navigation starts, the c.com navigation will get canceled and |
| // finishes first without commmitting (while the b.com navigation stays as |
| // it is pending commit). |
| EXPECT_EQ(url_c, results[0].url); |
| EXPECT_TRUE(results[1].committed); |
| // After continuing b.com's commit, it finishes and commits succesfully. |
| EXPECT_EQ(url_b, results[1].url); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[1].origin); |
| } else { |
| // When navigation queueing is disabled, the pending commit navigation to |
| // b.com gets canceled when the c.com navigation starts. Then when the |
| // d.com navigation starts, the c.com navigation will get canceled too. |
| EXPECT_EQ(url_b, results[0].url); |
| EXPECT_FALSE(results[1].committed); |
| EXPECT_EQ(url_c, results[1].url); |
| EXPECT_EQ(std::nullopt, results[1].origin); |
| } |
| // Finally, the d.com navigation finishes and commits last. |
| EXPECT_TRUE(results[2].committed); |
| EXPECT_EQ(url_d, results[2].url); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("d.com"), results[2].origin); |
| } |
| |
| // Verify that a speculative RFH in the pending commit state is still cleaned up |
| // if the renderer crashes. |
| IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, |
| CrashedInPendingCommit) { |
| GURL url_a = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| SpeculativeRenderFrameHostObserver rfh_observer(web_contents, url_b); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), url_b)); |
| rfh_observer.Wait(); |
| |
| base::WeakPtr<RenderFrameHostImpl> speculative_render_frame_host = |
| web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->speculative_frame_host() |
| ->GetWeakPtr(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| |
| // Wait for the next `DidCommitProvisionalLoad()` and ignore it. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host.get()); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| ASSERT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kPendingCommit, |
| speculative_render_frame_host->lifecycle_state()); |
| |
| // Terminate the renderer process while `speculative_render_frame_host` is in |
| // `kPendingCommit`. |
| RenderProcessHostWatcher watcher( |
| speculative_render_frame_host->GetProcess(), |
| RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| speculative_render_frame_host->GetProcess()->ShutdownForBadMessage( |
| RenderProcessHost::CrashReportMode::NO_CRASH_DUMP); |
| watcher.Wait(); |
| |
| // The speculative RFH should be gone: |
| ASSERT_FALSE(speculative_render_frame_host); |
| EXPECT_FALSE(web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->speculative_frame_host()); |
| |
| // And a new navigation should not hit any DCHECKs in |
| // `GetFrameHostForNavigation()`. |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| shell(), embedded_test_server()->GetURL("b.com", "/title1.html"))); |
| } |
| |
| // Tests when a back navigation is pending commit, then another back navigation |
| // starts. |
| IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, |
| MultipleBackNavigation) { |
| // This test expects the document is freshly loaded on the back navigation. |
| DisableBackForwardCacheForTesting(web_contents(), |
| BackForwardCache::TEST_REQUIRES_NO_CACHING); |
| |
| // Navigate to a.com, then b.com. |
| const GURL url_a = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| const GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // Prior to implementing the UndoCommitNavigation() workaround, the race |
| // condition being tested would result in a crash in the b.com renderer. Open |
| // another b.com window in the same browsing instance to verify that the b.com |
| // renderer does not unexpectedly crash even if the b.com speculative |
| // RenderFrameHost is discarded. |
| ASSERT_TRUE(ExecJs(shell(), JsReplace("window.open($1)", url_b))); |
| ASSERT_EQ(2u, Shell::windows().size()); |
| WebContentsImpl* new_web_contents = |
| static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); |
| EXPECT_TRUE(WaitForLoadStop(new_web_contents)); |
| RenderProcessHost* const b_com_render_process_host = |
| new_web_contents->GetPrimaryMainFrame()->GetProcess(); |
| |
| // Navigate to c.com. |
| const GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), url_c)); |
| |
| NavigationLogger logger(shell()->web_contents()); |
| |
| // Start a back navigation that will create a speculative RFH in the existing |
| // render process for b.com. |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root(); |
| TestNavigationManager first_back_nav(web_contents, url_b); |
| ASSERT_TRUE(ExecJs(shell(), "history.back()")); |
| ASSERT_TRUE(first_back_nav.WaitForResponse()); |
| EXPECT_EQ(url_b, root->navigation_request()->GetURL()); |
| |
| // Ensure the speculative RFH is in the expected process (i.e. the b.com |
| // process that was created for the navigation in the new window earlier). |
| RenderFrameHostImpl* speculative_render_frame_host = |
| root->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_render_frame_host); |
| EXPECT_EQ(b_com_render_process_host, |
| speculative_render_frame_host->GetProcess()); |
| |
| // Pause (and potentially ignore, if navigation queueing is disabled) the next |
| // `DidCommitProvisionalLoad()` for b.com. |
| CommitNavigationPauser commit_pauser(speculative_render_frame_host); |
| first_back_nav.ResumeNavigation(); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| // Now begin a new back navigation while the previous back navigation above is |
| // paused in the pending commit state. |
| std::optional<ResumeCommitClosureSetWaiter> |
| second_back_nav_resume_commit_closure_set_waiter; |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // If navigation queueing is enabled, the test should verify that a resume |
| // commit closure is actually set. Install a watcher now, before beginning |
| // the second back navigation, since the resume commit closure may be |
| // synchronously set while handling the `BeginNavigation()` IPC in the |
| // browser. |
| OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { |
| second_back_nav_resume_commit_closure_set_waiter.emplace(handle); |
| }); |
| } |
| |
| TestNavigationManager second_back_nav(web_contents, url_a); |
| NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>( |
| shell()->web_contents()->GetController()); |
| controller.GoBack(); |
| ASSERT_TRUE(second_back_nav.WaitForRequestStart()); |
| EXPECT_EQ(url_a, root->navigation_request()->GetURL()); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // The second back navigation should be queued. |
| second_back_nav.ResumeNavigation(); |
| second_back_nav_resume_commit_closure_set_waiter->Wait(); |
| // Continue the first navigation's commit. |
| commit_pauser.ResumePausedCommit(); |
| } |
| |
| // After all the navigations finished, we will end up in a.com. |
| EXPECT_TRUE(second_back_nav.WaitForNavigationFinished()); |
| EXPECT_EQ(url_a, web_contents->GetLastCommittedURL()); |
| |
| // Check the order of navigations finishing. |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| |
| if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { |
| // When navigation queueing is enabled, the pending commit back navigation |
| // to b.com won't get canceled when the second back navigation starts. After |
| // continuing the second back navigation, it finishes and commits |
| // successfully to a.com. |
| EXPECT_EQ(url_b, results[0].url); |
| EXPECT_TRUE(results[0].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); |
| } else { |
| // When navigation queueing is disabled, the pending commit back navigation |
| // to b.com gets canceled when the second back navigation starts. Then the |
| // second back navigation will successfully commit to a.com. |
| EXPECT_EQ(url_b, results[0].url); |
| EXPECT_FALSE(results[0].committed); |
| } |
| |
| EXPECT_EQ(url_a, results[1].url); |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(, |
| CommitNavigationRaceBrowserTest, |
| ::testing::Bool(), |
| &CommitNavigationRaceBrowserTest::DescribeParams); |
| |
| // Validate browser-side state when a pending commit RFH sends a bad |
| // CommitNavigation() IPC. Immediately after the bad message is reported, the |
| // speculative RFH should remain in the kPendingCommit state, but with no |
| // pending commit for a cross-document navigation. This somewhat odd state comes |
| // about because processing the commit navigation ack consumes the |
| // NavigationRequest early on, before the bad message is reported. Reporting the |
| // bad message cancels any further processing of the commit navigation ack, but |
| // does not directly clear any other navigation-related state. |
| // |
| // Instead, the pending commit speculative RFH will be asynchronously torn down |
| // later, when the browser process observes the renderer process going away, |
| // which then implicitly ends the navigation. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| CommitBadNavigationInPendingCommitRFHCleanup) { |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP(); |
| } |
| |
| // Populate the main window with something so a subsequent navigation will |
| // create a speculative RFH. |
| EXPECT_TRUE(NavigateToURL( |
| web_contents(), embedded_test_server()->GetURL("b.com", "/title1.html"))); |
| |
| class CommitBadOriginInterceptor : public DidCommitNavigationInterceptor { |
| public: |
| using DidCommitNavigationInterceptor::DidCommitNavigationInterceptor; |
| |
| WebContentsImpl* web_contents() { |
| return static_cast<WebContentsImpl*>( |
| DidCommitNavigationInterceptor::web_contents()); |
| } |
| |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) |
| override { |
| // Mismatch from the expected origin for the renderer process, which |
| // should trigger a bad message kill. |
| (*params)->origin = url::Origin::Create(GURL("https://example.com/")); |
| |
| frame_watcher_.emplace(render_frame_host); |
| process_watcher_.emplace( |
| render_frame_host->GetProcess(), |
| RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindLambdaForTesting([this]() { |
| // This task should run after the browser has reported a bad |
| // message, but before the browser process has observed render |
| // process termination, so the pending commit speculative RFH should |
| // still be present... |
| auto* speculative_rfh = web_contents() |
| ->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->speculative_frame_host(); |
| ASSERT_TRUE(speculative_rfh); |
| EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kPendingCommit, |
| speculative_rfh->lifecycle_state()); |
| |
| // But it should not have any pending cross-document navigation |
| // commits, since the NavigationRequest is consumed from |
| // `RenderFrameHostImpl::navigation_requests_` as one of the first |
| // parts of handling the commit navigation ack from the renderer. |
| EXPECT_FALSE( |
| speculative_rfh->HasPendingCommitForCrossDocumentNavigation()); |
| |
| validated_speculative_rfh_state_ = true; |
| })); |
| |
| return true; |
| } |
| |
| void CheckPendingCommitRenderFrameHostIsGone() const { |
| // Make sure the validations in the posted callback actually ran. |
| EXPECT_TRUE(validated_speculative_rfh_state_); |
| EXPECT_TRUE(frame_watcher_->IsDestroyed()); |
| } |
| void WaitForRenderProcessExit() { process_watcher_->Wait(); } |
| |
| private: |
| bool validated_speculative_rfh_state_ = false; |
| std::optional<RenderFrameHostWrapper> frame_watcher_; |
| std::optional<RenderProcessHostWatcher> process_watcher_; |
| }; |
| |
| CommitBadOriginInterceptor interceptor(web_contents()); |
| |
| content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes; |
| // The infinitely loading page is load-bearing here: `NavigateToURL()` waits |
| // for `DidStopLoading()`, which can be triggered by: |
| // 1. The renderer completing the load and sending `DidStopLoading()` (this is |
| // typical). |
| // 2. The renderer process going away (e.g. crashing) and the browser manually |
| // triggering `DidStopLoading()` (this is unusual). |
| // |
| // However, this test is specifically testing case #2. To avoid the potential |
| // of #1 and #2 racing (and causing the test to flakily fail if #1 wins the |
| // race), set up the test so that the renderer will never call |
| // `DidStopLoading()`. |
| EXPECT_FALSE(NavigateToURL(web_contents(), |
| embedded_test_server()->GetURL( |
| "a.com", "/infinitely_loading_image.html"))); |
| |
| // NavigateToURL() should fail; at this point, make sure the speculative RFH |
| // was in the expected state after the browser reported a bad message. |
| // |
| // Furthermore, after the navigation completes, the speculative RFH should |
| // also be destroyed (implicitly, by the renderer process being terminated for |
| // a bad message). |
| interceptor.CheckPendingCommitRenderFrameHostIsGone(); |
| |
| // This should actually be signalled inside `NavigateToURL()`, since it waits |
| // for the navigation to finish (whether successful or not) before returning. |
| // Make sure the render process host actually exited: if it didn't, then the |
| // test will timeout here. |
| interceptor.WaitForRenderProcessExit(); |
| } |
| |
| // The following test checks what happens if a WebContentsDelegate navigates |
| // away in response to the NavigationStateChanged event. Previously |
| // (https://crbug.com/1210234), this was triggering a crash when creating the |
| // new NavigationRequest, because it was trying to access the current |
| // RenderFrameHost's PolicyContainerHost, which had not been set up yet by |
| // RenderFrameHostImpl::DidNavigate. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, Bug1210234) { |
| class NavigationWebContentsDelegate : public WebContentsDelegate { |
| public: |
| NavigationWebContentsDelegate(const GURL& url_to_intercept, |
| const GURL& url_to_navigate_to) |
| : url_to_intercept_(url_to_intercept), |
| url_to_navigate_to_(url_to_navigate_to) {} |
| void NavigationStateChanged(WebContents* source, |
| InvalidateTypes changed_flags) override { |
| if (!navigated_ && source->GetLastCommittedURL() == url_to_intercept_) { |
| navigated_ = true; |
| source->GetController().LoadURL(url_to_navigate_to_, Referrer(), |
| ui::PAGE_TRANSITION_AUTO_TOPLEVEL, |
| std::string()); |
| } |
| } |
| |
| private: |
| bool navigated_ = false; |
| GURL url_to_intercept_; |
| GURL url_to_navigate_to_; |
| }; |
| |
| GURL warmup_url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| GURL initial_url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| GURL redirection_url = |
| embedded_test_server()->GetURL("c.com", "/title1.html"); |
| |
| NavigationWebContentsDelegate delegate(initial_url, redirection_url); |
| web_contents()->SetDelegate(&delegate); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), warmup_url)); |
| |
| // Note that since we committed a navigation, the next cross-origin navigation |
| // will create a speculative RenderFrameHost (when site isolation is enabled). |
| |
| // Start the navigation to `initial_url` and wait until the web contents |
| // navigates to `redirection_url`. We cannot use helper functions like |
| // `NavigateToURLBlockUntilNavigationsComplete` because they wait for |
| // DidStopLoading and check the LastCommittedURL when they receive it. |
| // However, without SiteIsolation, an earlier DidStopLoading might be received |
| // when the WebContents has not yet committed the `redirection_url`. |
| |
| // Prepare for the navigation. |
| WaitForLoadStop(web_contents()); |
| TestNavigationObserver navigation_observer(redirection_url); |
| navigation_observer.WatchExistingWebContents(); |
| |
| shell()->LoadURL(initial_url); |
| |
| navigation_observer.Wait(); |
| |
| EXPECT_TRUE(IsLastCommittedEntryOfPageType(web_contents(), PAGE_TYPE_NORMAL)); |
| EXPECT_EQ(redirection_url, web_contents()->GetLastCommittedURL()); |
| } |
| |
| class NavigationBrowserTestCredentiallessIframe : public NavigationBrowserTest { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| NavigationBrowserTest::SetUpCommandLine(command_line); |
| |
| command_line->AppendSwitch(switches::kEnableBlinkTestFeatures); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTestCredentiallessIframe, |
| CheckCookiesForCredentiallessIframeNavigation) { |
| GURL main_url = |
| embedded_test_server()->GetURL("/page_with_credentialless_iframe.html"); |
| GURL iframe_url_1 = embedded_test_server()->GetURL("/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Set a cookie on the main frame |
| EXPECT_TRUE(ExecJs(main_frame(), "document.cookie = 'name=main;';")); |
| EXPECT_EQ("name=main", EvalJs(main_frame(), "document.cookie;")); |
| |
| // The main page has a child iframe with url `iframe_url_1`. |
| EXPECT_EQ(1U, main_frame()->child_count()); |
| FrameTreeNode* child = main_frame()->child_at(0); |
| EXPECT_EQ(iframe_url_1, child->current_url()); |
| EXPECT_TRUE(child->Credentialless()); |
| EXPECT_TRUE(child->current_frame_host()->IsCredentialless()); |
| EXPECT_TRUE(ExecJs(child->current_frame_host(), "window.credentialless")); |
| |
| // Set up devtools client so we can check which cookies were sent and blocked |
| // with frame navigation |
| TestDevToolsProtocolClient fenced_frame_devtools_client; |
| fenced_frame_devtools_client.AttachToFrameTreeHost( |
| child->current_frame_host()); |
| fenced_frame_devtools_client.SendCommandAsync("Network.enable"); |
| fenced_frame_devtools_client.ClearNotifications(); |
| |
| // Set a cookie on the frame and reload so we can see which cookies are |
| // included in the network request |
| EXPECT_TRUE(ExecJs(child->current_frame_host(), |
| "document.cookie = 'name=credentialless;';")); |
| |
| EXPECT_EQ("name=credentialless", |
| EvalJs(child->current_frame_host(), "document.cookie;")); |
| |
| EXPECT_TRUE(ExecJs(child->current_frame_host(), "location.reload();")); |
| |
| // Check associated cookies according to devtools |
| base::Value::Dict params = fenced_frame_devtools_client.WaitForNotification( |
| "Network.requestWillBeSentExtraInfo", /*allow_existing=*/true); |
| |
| const base::Value::List* associated_cookies = |
| params.FindList("associatedCookies"); |
| |
| EXPECT_THAT( |
| associated_cookies, |
| testing::Pointee(testing::UnorderedElementsAre( |
| base::test::IsSupersetOfValue(base::test::ParseJsonDict(R"({ |
| "blockedReasons": [ "AnonymousContext" ], |
| "cookie" : { |
| "name": "name", |
| "value": "main" |
| } |
| })")), |
| testing::AllOf( |
| base::test::IsSupersetOfValue(base::test::ParseJsonDict(R"({ |
| "blockedReasons": [ ], |
| "cookie" : { |
| "name": "name", |
| "value": "credentialless" |
| } |
| })")), |
| testing::ResultOf( |
| [](const base::Value& dict) { |
| return dict.GetDict().FindList("blockedReasons"); |
| }, |
| testing::Pointee(testing::IsEmpty())))))); |
| |
| fenced_frame_devtools_client.DetachProtocolClient(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTestCredentiallessIframe, |
| CredentiallessAttributeIsHonoredByNavigation) { |
| GURL main_url = embedded_test_server()->GetURL("/page_with_iframe.html"); |
| GURL iframe_url_1 = embedded_test_server()->GetURL("/title1.html"); |
| GURL iframe_url_2 = embedded_test_server()->GetURL("/title2.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // The main page has a child iframe with url `iframe_url_1`. |
| EXPECT_EQ(1U, main_frame()->child_count()); |
| FrameTreeNode* child = main_frame()->child_at(0); |
| EXPECT_EQ(iframe_url_1, child->current_url()); |
| EXPECT_FALSE(child->Credentialless()); |
| EXPECT_FALSE(child->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(false, |
| EvalJs(child->current_frame_host(), "window.credentialless")); |
| |
| // Changes to the iframe 'credentialless' attribute are propagated to the |
| // FrameTreeNode. The RenderFrameHost, however, is updated only on navigation. |
| EXPECT_TRUE( |
| ExecJs(main_frame(), |
| "document.getElementById('test_iframe').credentialless = true;")); |
| EXPECT_TRUE(child->Credentialless()); |
| EXPECT_FALSE(child->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(false, |
| EvalJs(child->current_frame_host(), "window.credentialless")); |
| |
| // Create a grandchild iframe. |
| EXPECT_TRUE(ExecJs( |
| child, JsReplace("let grandchild = document.createElement('iframe');" |
| "grandchild.src = $1;" |
| "document.body.appendChild(grandchild);", |
| iframe_url_2))); |
| WaitForLoadStop(web_contents()); |
| EXPECT_EQ(1U, child->child_count()); |
| FrameTreeNode* grandchild = child->child_at(0); |
| |
| // The grandchild FrameTreeNode does not set the 'credentialless' |
| // attribute. The grandchild RenderFrameHost is not credentialless, since its |
| // parent RenderFrameHost is not credentialless. |
| EXPECT_FALSE(grandchild->Credentialless()); |
| EXPECT_FALSE(grandchild->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(false, |
| EvalJs(grandchild->current_frame_host(), "window.credentialless")); |
| |
| // Navigate the child iframe same-document. This does not change anything. |
| EXPECT_TRUE(ExecJs(main_frame(), |
| JsReplace("document.getElementById('test_iframe')" |
| " .contentWindow.location.href = $1;", |
| iframe_url_1.Resolve("#here").spec()))); |
| WaitForLoadStop(web_contents()); |
| EXPECT_TRUE(child->Credentialless()); |
| EXPECT_FALSE(child->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(false, |
| EvalJs(child->current_frame_host(), "window.credentialless")); |
| |
| // Now navigate the child iframe cross-document. |
| EXPECT_TRUE(ExecJs( |
| main_frame(), JsReplace("document.getElementById('test_iframe').src = $1", |
| iframe_url_2))); |
| WaitForLoadStop(web_contents()); |
| EXPECT_TRUE(child->Credentialless()); |
| EXPECT_TRUE(child->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(true, EvalJs(child->current_frame_host(), "window.credentialless")); |
| // A credentialless document has a storage key with a nonce. |
| EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value()); |
| base::UnguessableToken credentialless_nonce = |
| current_frame_host()->GetPage().credentialless_iframes_nonce(); |
| EXPECT_EQ(credentialless_nonce, |
| child->current_frame_host()->GetStorageKey().nonce().value()); |
| |
| // Create a grandchild iframe. |
| EXPECT_TRUE(ExecJs( |
| child, JsReplace("let grandchild = document.createElement('iframe');" |
| "grandchild.id = 'grandchild_iframe';" |
| "document.body.appendChild(grandchild);", |
| iframe_url_1))); |
| EXPECT_EQ(1U, child->child_count()); |
| grandchild = child->child_at(0); |
| |
| // The grandchild does not set the 'credentialless' attribute, but the |
| // grandchild document is credentialless. |
| EXPECT_FALSE(grandchild->Credentialless()); |
| EXPECT_TRUE(grandchild->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(true, |
| EvalJs(grandchild->current_frame_host(), "window.credentialless")); |
| |
| // The storage key's nonce is the same for all credentialless documents in the |
| // same page. |
| EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value()); |
| EXPECT_EQ(credentialless_nonce, |
| child->current_frame_host()->GetStorageKey().nonce().value()); |
| |
| // Now navigate the grandchild iframe. |
| EXPECT_TRUE(ExecJs( |
| child, JsReplace("document.getElementById('grandchild_iframe').src = $1", |
| iframe_url_2))); |
| WaitForLoadStop(web_contents()); |
| EXPECT_TRUE(grandchild->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(true, |
| EvalJs(grandchild->current_frame_host(), "window.credentialless")); |
| |
| // The storage key's nonce is still the same. |
| EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value()); |
| EXPECT_EQ(credentialless_nonce, |
| child->current_frame_host()->GetStorageKey().nonce().value()); |
| |
| // Remove the 'credentialless' attribute from the iframe. This propagates to |
| // the FrameTreeNode. The RenderFrameHost, however, is updated only on |
| // navigation. |
| EXPECT_TRUE( |
| ExecJs(main_frame(), |
| "document.getElementById('test_iframe').credentialless = false;")); |
| EXPECT_FALSE(child->Credentialless()); |
| EXPECT_TRUE(child->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(true, EvalJs(child->current_frame_host(), "window.credentialless")); |
| EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value()); |
| EXPECT_EQ(credentialless_nonce, |
| child->current_frame_host()->GetStorageKey().nonce().value()); |
| |
| // Create another grandchild iframe. Even if the parent iframe element does |
| // not have the 'credentialless' attribute anymore, the grandchild document is |
| // still loaded inside of a credentialless RenderFrameHost, so it will be |
| // credentialless. |
| EXPECT_TRUE(ExecJs( |
| child, JsReplace("let grandchild2 = document.createElement('iframe');" |
| "document.body.appendChild(grandchild2);", |
| iframe_url_1))); |
| EXPECT_EQ(2U, child->child_count()); |
| FrameTreeNode* grandchild2 = child->child_at(1); |
| EXPECT_FALSE(grandchild2->Credentialless()); |
| EXPECT_TRUE(grandchild2->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(true, |
| EvalJs(grandchild2->current_frame_host(), "window.credentialless")); |
| EXPECT_TRUE( |
| grandchild2->current_frame_host()->GetStorageKey().nonce().has_value()); |
| EXPECT_EQ(credentialless_nonce, |
| grandchild2->current_frame_host()->GetStorageKey().nonce().value()); |
| |
| // Navigate the child iframe. Since the iframe element does not set the |
| // 'credentialless' attribute, the resulting RenderFrameHost will not be |
| // credentialless. |
| EXPECT_TRUE( |
| ExecJs(main_frame(), |
| JsReplace("document.getElementById('test_iframe').src = $1;", |
| iframe_url_2))); |
| WaitForLoadStop(web_contents()); |
| EXPECT_FALSE(child->Credentialless()); |
| EXPECT_FALSE(child->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(false, |
| EvalJs(child->current_frame_host(), "window.credentialless")); |
| EXPECT_FALSE( |
| child->current_frame_host()->GetStorageKey().nonce().has_value()); |
| |
| // Now navigate the whole page away. |
| GURL main_url_b = embedded_test_server()->GetURL( |
| "b.com", "/page_with_credentialless_iframe.html"); |
| GURL iframe_url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url_b)); |
| |
| // The main page has a credentialless child iframe with url `iframe_url_b`. |
| EXPECT_EQ(1U, main_frame()->child_count()); |
| FrameTreeNode* child_b = main_frame()->child_at(0); |
| EXPECT_EQ(iframe_url_b, child_b->current_url()); |
| EXPECT_TRUE(child_b->Credentialless()); |
| EXPECT_TRUE(child_b->current_frame_host()->IsCredentialless()); |
| EXPECT_EQ(true, |
| EvalJs(child_b->current_frame_host(), "window.credentialless")); |
| |
| EXPECT_TRUE( |
| child_b->current_frame_host()->GetStorageKey().nonce().has_value()); |
| base::UnguessableToken credentialless_nonce_b = |
| current_frame_host()->GetPage().credentialless_iframes_nonce(); |
| EXPECT_NE(credentialless_nonce, credentialless_nonce_b); |
| EXPECT_EQ(credentialless_nonce_b, |
| child_b->current_frame_host()->GetStorageKey().nonce().value()); |
| } |
| |
| // Ensures that OpenURLParams::FromNavigationHandle translates navigation params |
| // correctly when used to initiate a navigation in another WebContents. |
| IN_PROC_BROWSER_TEST_F( |
| NavigationBrowserTest, |
| FromNavigationHandleTranslatesNavigationParamsCorrectly) { |
| // Test that the params are translated correctly for a redirected navigation. |
| const GURL kRedirectedURL( |
| embedded_test_server()->GetURL("/server-redirect?/simple_page.html")); |
| NavigationController::LoadURLParams load_params(kRedirectedURL); |
| TestNavigationManager first_tab_manager(web_contents(), kRedirectedURL); |
| web_contents()->GetController().LoadURLWithParams(load_params); |
| |
| // Wait for response to allow the navigation to resolve the redirect. |
| EXPECT_TRUE(first_tab_manager.WaitForResponse()); |
| |
| // Create LoadURLParams from the navigation after redirection. |
| NavigationController::LoadURLParams load_url_params( |
| OpenURLParams::FromNavigationHandle( |
| first_tab_manager.GetNavigationHandle())); |
| |
| // Check that the FrameTreeNode id is set, but then clear it since we'll be |
| // navigating another tab with the rest of the params. |
| EXPECT_EQ(load_url_params.frame_tree_node_id, |
| main_frame()->frame_tree_node_id()); |
| load_url_params.frame_tree_node_id = content::FrameTreeNodeId(); |
| |
| Shell* second_tab = CreateBrowser(); |
| TestNavigationManager second_tab_manager(second_tab->web_contents(), |
| load_url_params.url); |
| second_tab->web_contents()->GetController().LoadURLWithParams( |
| load_url_params); |
| |
| EXPECT_TRUE(second_tab_manager.WaitForResponse()); |
| |
| // Ensure params from the navigation in the first tab are translated to the |
| // navigation in the second tab as expected. |
| auto* first_tab_handle = first_tab_manager.GetNavigationHandle(); |
| auto* second_tab_handle = second_tab_manager.GetNavigationHandle(); |
| EXPECT_EQ(embedded_test_server()->GetURL("/simple_page.html"), |
| second_tab_handle->GetURL()); |
| EXPECT_EQ(first_tab_handle->GetReferrer(), second_tab_handle->GetReferrer()); |
| EXPECT_TRUE( |
| ui::PageTransitionCoreTypeIs(first_tab_handle->GetPageTransition(), |
| second_tab_handle->GetPageTransition())); |
| EXPECT_EQ(first_tab_handle->IsRendererInitiated(), |
| second_tab_handle->IsRendererInitiated()); |
| EXPECT_EQ(first_tab_handle->GetInitiatorOrigin(), |
| second_tab_handle->GetInitiatorOrigin()); |
| EXPECT_EQ(first_tab_handle->GetSourceSiteInstance(), |
| second_tab_handle->GetSourceSiteInstance()); |
| EXPECT_EQ(first_tab_handle->HasUserGesture(), |
| second_tab_handle->HasUserGesture()); |
| EXPECT_EQ(first_tab_handle->WasStartedFromContextMenu(), |
| second_tab_handle->WasStartedFromContextMenu()); |
| EXPECT_EQ(first_tab_handle->GetHrefTranslate(), |
| second_tab_handle->GetHrefTranslate()); |
| EXPECT_EQ(first_tab_handle->GetReloadType(), |
| second_tab_handle->GetReloadType()); |
| EXPECT_EQ(first_tab_handle->GetRedirectChain(), |
| second_tab_handle->GetRedirectChain()); |
| } |
| |
| // Regression test for https://crbug.com/1392653. Ensure that loading a URL |
| // that doesn't go through the network stack but does assign a site for its |
| // SiteInstance in an unassigned SiteInstance does not fail. An example of |
| // such a URL is about:srcdoc. This ensures that the SiteInstance's site is set |
| // even on the WillCommitWithoutUrlLoader() path in NavigationRequest. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| AboutSrcdocInjectedOnAboutBlankPage) { |
| // Start on an about:blank page, which should stay in an unassigned |
| // SiteInstance. |
| EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL))); |
| SiteInstanceImpl* site_instance = current_frame_host()->GetSiteInstance(); |
| EXPECT_FALSE(site_instance->HasSite()); |
| |
| // The process should be considered unused at this point. |
| EXPECT_TRUE(site_instance->GetProcess()->IsUnused()); |
| |
| // Inject a srcdoc iframe into the blank document. This shouldn't really be |
| // possible on the open web, since an about:blank page with an unassigned |
| // SiteInstance shouldn't be scriptable by other pages, but it could still |
| // happen in automation scenarios or through DevTools. |
| TestNavigationObserver navigation_observer(web_contents()); |
| EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"( |
| let frame = document.createElement('iframe'); |
| frame.srcdoc = 'test'; |
| document.body.appendChild(frame); |
| )"))); |
| navigation_observer.Wait(); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_EQ("about:srcdoc", navigation_observer.last_navigation_url()); |
| |
| // The srcdoc child should stay in its about:blank parent SiteInstance. |
| EXPECT_EQ(1U, main_frame()->child_count()); |
| FrameTreeNode* child = main_frame()->child_at(0); |
| EXPECT_EQ(child->current_frame_host()->GetSiteInstance(), site_instance); |
| |
| // Committing an about:srcdoc navigation currently forces the SiteInstance's |
| // site to be set. Prior to fixing https://crbug.com/1392653, this happened |
| // after the actual commit was processed at DidNavigate() time, which is a |
| // path that is no longer supported, and hence this triggered a NOTREACHED(). |
| // Now, the site should be set before we send the CommitNavigation IPC. |
| EXPECT_TRUE(site_instance->HasSite()); |
| |
| if (AreStrictSiteInstancesEnabled()) { |
| // When we get into this situation with strict site isolation, the site URL |
| // currently used is "about:". This may be changed in the future (e.g., to |
| // an opaque ID). |
| EXPECT_EQ("about:", site_instance->GetSiteInfo().site_url()); |
| } else { |
| EXPECT_TRUE(site_instance->IsDefaultSiteInstance()); |
| EXPECT_EQ(SiteInstanceImpl::GetDefaultSiteURL(), |
| site_instance->GetSiteInfo().site_url()); |
| } |
| |
| // Ensure that the process was marked as used as part of setting the site. |
| EXPECT_FALSE(site_instance->GetProcess()->IsUnused()); |
| } |
| |
| class NavigationBrowserTestWarnSandboxIneffective |
| : public NavigationBrowserTest { |
| public: |
| static constexpr char kSandboxEscapeWarningMessage[] = |
| "An iframe which has both allow-scripts and allow-same-origin for its " |
| "sandbox attribute can escape its sandboxing."; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective, |
| WarnEscapableSandboxSameOrigin) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/empty.html"))); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern(kSandboxEscapeWarningMessage); |
| |
| // Create same-origin iframe. |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| const iframe = document.createElement("iframe"); |
| iframe.src = location.href; // Same-origin iframe. |
| iframe.sandbox = "allow-same-origin allow-scripts"; |
| document.body.appendChild(iframe); |
| )")); |
| ASSERT_TRUE(console_observer.Wait()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective, |
| WarnEscapableSandboxCrossOrigin) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/empty.html"))); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern(kSandboxEscapeWarningMessage); |
| |
| // Create cross-origin iframe. |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| const iframe = document.createElement("iframe"); |
| // Cross-origin iframe: |
| iframe.src = location.href.replace("a.com", "b.com"); |
| iframe.sandbox = "allow-same-origin allow-scripts"; |
| document.body.appendChild(iframe); |
| )")); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_EQ(console_observer.messages().size(), 0u); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective, |
| WarnEscapableSandboxSameOriginGrandChild) { |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/empty.html"))); |
| |
| WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern(kSandboxEscapeWarningMessage); |
| |
| // Create a same-origin doubly nested sandboxed iframe. |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| const child = document.createElement("iframe"); |
| document.body.appendChild(child); |
| |
| const grand_child = child.contentDocument.createElement("iframe"); |
| grand_child.src = location.href; |
| grand_child.sandbox = "allow-same-origin allow-scripts"; |
| child.contentDocument.body.appendChild(grand_child); |
| )")); |
| |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_EQ(console_observer.messages().size(), 0u); |
| } |
| |
| // We may have an unload handler in the main frame or a subframe or nowhere. |
| enum class UnloadFrameType { |
| kMainFrame, |
| kSubFrame, |
| kNone, |
| }; |
| |
| static std::string ToString(UnloadFrameType v) { |
| switch (v) { |
| case UnloadFrameType::kMainFrame: |
| return "MainFrame"; |
| case UnloadFrameType::kSubFrame: |
| return "SubFrame"; |
| case UnloadFrameType::kNone: |
| return "None"; |
| } |
| } |
| |
| // We may navigate the main frame, the subframe that may have an unload handler |
| // or another subframe that will never have an unload handler. |
| enum class NavigateFrameType { |
| kMainFrame, |
| kSubFrame, |
| kOther, |
| }; |
| |
| static std::string ToString(NavigateFrameType v) { |
| switch (v) { |
| case NavigateFrameType::kMainFrame: |
| return "MainFrame"; |
| case NavigateFrameType::kSubFrame: |
| return "SubFrame"; |
| case NavigateFrameType::kOther: |
| return "Other"; |
| } |
| } |
| |
| void AddUnloadHandler(RenderFrameHostImpl* rfh) { |
| ASSERT_TRUE(ExecJs(rfh, "addEventListener('unload', () => {})")); |
| ASSERT_TRUE(rfh->GetSuddenTerminationDisablerState( |
| blink::mojom::SuddenTerminationDisablerType::kUnloadHandler)); |
| } |
| |
| class NavigationSuddenTerminationDisablerTypeBrowserTest |
| : public NavigationBrowserTest { |
| public: |
| NavigationSuddenTerminationDisablerTypeBrowserTest() { |
| feature_list_.InitWithFeaturesAndParameters( |
| /*enabled_features=*/{}, |
| /*disabled_features=*/{network::features::kDeprecateUnload}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| class NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest |
| : public NavigationSuddenTerminationDisablerTypeBrowserTest, |
| public ::testing::WithParamInterface< |
| std::tuple<UnloadFrameType, NavigateFrameType>> { |
| public: |
| static std::string DescribeParams( |
| const ::testing::TestParamInfo<ParamType>& info) { |
| return "Unload" + ToString(std::get<0>(info.param)) + "Navigate" + |
| ToString(std::get<1>(info.param)); |
| } |
| |
| protected: |
| UnloadFrameType GetUnloadFrameType() { return std::get<0>(GetParam()); } |
| NavigateFrameType GetNavigateFrameType() { return std::get<1>(GetParam()); } |
| |
| void MaybeAddUnloadHandler() { |
| RenderFrameHostImpl* rfh = nullptr; |
| switch (GetUnloadFrameType()) { |
| case UnloadFrameType::kMainFrame: |
| rfh = current_frame_host(); |
| break; |
| case UnloadFrameType::kSubFrame: |
| rfh = DescendantRenderFrameHostImplAt(current_frame_host(), {0}); |
| break; |
| case UnloadFrameType::kNone: |
| break; |
| } |
| if (rfh) { |
| AddUnloadHandler(rfh); |
| } |
| } |
| |
| RenderFrameHostImpl* GetFrameToNavigate() { |
| switch (GetNavigateFrameType()) { |
| case NavigateFrameType::kMainFrame: |
| return current_frame_host(); |
| case NavigateFrameType::kSubFrame: |
| return DescendantRenderFrameHostImplAt(current_frame_host(), {0}); |
| case NavigateFrameType::kOther: |
| return DescendantRenderFrameHostImplAt(current_frame_host(), {1}); |
| } |
| } |
| |
| bool NavigatedFrameHasUnload() { |
| switch (GetNavigateFrameType()) { |
| case NavigateFrameType::kOther: |
| // It never has unload. |
| return false; |
| case NavigateFrameType::kMainFrame: |
| // Navigating the main will have unload if there is any unload. |
| return GetUnloadFrameType() != UnloadFrameType::kNone; |
| case NavigateFrameType::kSubFrame: |
| // Navigating the subframe will have unload if the subframe has unload. |
| return GetUnloadFrameType() == UnloadFrameType::kSubFrame; |
| } |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest, |
| ::testing::Combine(::testing::Values(UnloadFrameType::kMainFrame, |
| UnloadFrameType::kSubFrame, |
| UnloadFrameType::kNone), |
| ::testing::Values(NavigateFrameType::kMainFrame, |
| NavigateFrameType::kSubFrame, |
| NavigateFrameType::kOther)), |
| &NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest:: |
| DescribeParams); |
| |
| // Set up a page with 2 subframes. The main frame or one of the subframes may |
| // have an unload handler. Then navigate one of the frames and verify that we |
| // correctly record which type of frame navigates combined with whether it |
| // involved an unload handler. |
| IN_PROC_BROWSER_TEST_P( |
| NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest, |
| RecordUma) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a,a)"))); |
| |
| current_frame_host()->DisableUnloadTimerForTesting(); |
| // Set up the unload handler if needed. |
| MaybeAddUnloadHandler(); |
| |
| // Navigate the relevant frame and capture histograms. |
| base::HistogramTester histograms; |
| ASSERT_TRUE(NavigateFrameToURL(GetFrameToNavigate()->frame_tree_node(), |
| GURL("about:blank"))); |
| |
| // Check that we got the expected histogram value. |
| uint32_t expected_histogram_value = 0; |
| if (GetNavigateFrameType() == NavigateFrameType::kMainFrame) { |
| expected_histogram_value |= RenderFrameHostImpl:: |
| NavigationSuddenTerminationDisablerType::kMainFrame; |
| } |
| if (NavigatedFrameHasUnload()) { |
| expected_histogram_value |= |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload; |
| } |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.AllOrigins", |
| expected_histogram_value, 1); |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.SameOrigin", |
| expected_histogram_value, 1); |
| } |
| |
| // Test that "SameOrigin" only considers frames that have an unbroken path of |
| // same-origin frames from the frame that navigates. |
| IN_PROC_BROWSER_TEST_F( |
| NavigationSuddenTerminationDisablerTypeBrowserTest, |
| NavigationSuddenTerminationDisablerTypeRecordUmaSameOrigin) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(a))"))); |
| |
| // Set up the unload handler in the a.com subframe. |
| AddUnloadHandler( |
| DescendantRenderFrameHostImplAt(current_frame_host(), {0, 0})); |
| // Navigate the main frame and capture histograms. |
| base::HistogramTester histograms; |
| ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank"))); |
| |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.AllOrigins", |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame | |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload, |
| 1); |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.SameOrigin", |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame, |
| 1); |
| } |
| |
| // Test that we record when the navigation involves restoring from BFCache. |
| // This is tested because the code path for a navigation involving activation |
| // is different from one involving a pageload. |
| IN_PROC_BROWSER_TEST_F( |
| NavigationSuddenTerminationDisablerTypeBrowserTest, |
| NavigationSuddenTerminationDisablerTypeRecordUmaActivation) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("b.com", "/title1.html"))); |
| |
| // Set up the unload handler in the b.com page. |
| AddUnloadHandler(current_frame_host()); |
| // Navigate the main frame and capture histograms. |
| base::HistogramTester histograms; |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.AllOrigins", |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame | |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload, |
| 1); |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.SameOrigin", |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame | |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload, |
| 1); |
| } |
| |
| // Ensure that the first navigation of a subframe away from the initial empty |
| // document is recorded correctly. This does not test all possibilities of |
| // histogram value, just that the scenario is counted under the correct |
| // histogram. |
| IN_PROC_BROWSER_TEST_F( |
| NavigationSuddenTerminationDisablerTypeBrowserTest, |
| NavigationSuddenTerminationDisablerTypeRecordUmaInitialEmptyDocument) { |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Create a subframe with an unload handler. |
| ASSERT_TRUE(ExecJs(web_contents(), R"( |
| var i = document.createElement("iframe"); |
| document.body.appendChild(i); |
| )")); |
| |
| AddUnloadHandler(DescendantRenderFrameHostImplAt(current_frame_host(), {0})); |
| |
| // Navigate the subframe and capture histograms. |
| base::HistogramTester histograms; |
| uint32_t expected_histogram_value = |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload | |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType:: |
| kInitialEmptyDocument | |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kNotHttp; |
| ASSERT_TRUE(NavigateToURLFromRenderer( |
| DescendantRenderFrameHostImplAt(current_frame_host(), {0}), url)); |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.AllOrigins", |
| expected_histogram_value, 1); |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.SameOrigin", |
| expected_histogram_value, 1); |
| } |
| |
| // Ensure that navigations from non-HTTP(S) pages are recorded correctly. |
| IN_PROC_BROWSER_TEST_F( |
| NavigationSuddenTerminationDisablerTypeBrowserTest, |
| NavigationSuddenTerminationDisablerTypeRecordUmaNotHttp) { |
| GURL blank_url("about:blank"); |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), blank_url)); |
| |
| AddUnloadHandler(current_frame_host()); |
| |
| // Navigate the subframe and capture histograms. |
| base::HistogramTester histograms; |
| uint32_t expected_histogram_value = |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame | |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload | |
| RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kNotHttp; |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.AllOrigins", |
| expected_histogram_value, 1); |
| histograms.ExpectUniqueSample( |
| "Navigation.SuddenTerminationDisabler.SameOrigin", |
| expected_histogram_value, 1); |
| } |
| |
| // This is a regression test against https://crbug.com/1145717 - navigating to |
| // invalid/weird URLs (e.g. `about:mumble` or `about://mumble`) shouldn't crash. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, AboutMumble) { |
| // First navigate to an arbitrary http site to lock the renderer process. |
| GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| // Verify that browser-initiated navigation to `about:mumble` doesn't crash. |
| // |
| // The renderer will try to commit the original "about:mumble" URL, which will |
| // still show up via `window.location.href`, but the URL passed back in the |
| // DidCommit IPC will be rewritten to `about:blank#blocked` due to |
| // `RenderFrameImpl`'s `IsValidCommitUrl` and therefore this is what we expect |
| // in `GetLastCommittedURL`. |
| ASSERT_FALSE(NavigateToURL(shell(), GURL("about:mumble"))); |
| EXPECT_EQ(EvalJs(shell(), "window.location.href"), "about:mumble"); |
| EXPECT_EQ( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL(), |
| GURL("about:blank#blocked")); |
| |
| // Verify that browser-initiated navigation to `about://mumble` doesn't crash. |
| ASSERT_FALSE(NavigateToURL(shell(), GURL("about://mumble"))); |
| EXPECT_EQ(EvalJs(shell(), "window.location.href"), "about://mumble"); |
| EXPECT_EQ( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL(), |
| GURL("about:blank#blocked")); |
| } |
| |
| // Ensure that the browser process doesn't see a javascript: URL when opening a |
| // new window to a javascript: URL. These URLs are typically handled on the |
| // renderer side, and the renderer should not send the javascript: URL to the |
| // browser in a navigation request. Previously, this was not correctly handled |
| // for initial navigations to javascript: URLs. See https://crbug.com/1357515. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FilterURL_JavascriptURLs) { |
| GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| { |
| SCOPED_TRACE(testing::Message() << "Testing opener case."); |
| base::HistogramTester histograms; |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE( |
| ExecJs(shell(), "window.open('javascript:window.foo=\"bar\"');")); |
| WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); |
| EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href")); |
| // No commit message is sent to the browser in this case, so the last |
| // committed URL is still empty. |
| EXPECT_EQ(GURL(), popup_contents->GetLastCommittedURL()); |
| histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL", |
| 0); |
| |
| // The javascript: URL should have run. |
| EXPECT_EQ("bar", EvalJs(popup_contents, "window.foo")); |
| } |
| |
| { |
| SCOPED_TRACE(testing::Message() << "Testing noopener case."); |
| base::HistogramTester histograms; |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE(ExecJs( |
| shell(), |
| "window.open('javascript:window.foo=\"bar\"', '', 'noopener');")); |
| WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); |
| EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href")); |
| EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL()); |
| histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL", |
| 0); |
| |
| // The Javascript URL should not have run in the noopener case, because the |
| // origin should not be inherited according to spec. See: |
| // https://html.spec.whatwg.org/multipage/document-sequences.html#navigable-target-names%3Acreating-a-new-top-level-traversable |
| // https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-browsing-context |
| // TODO(crbug.com/40236679): Also prevent the origin from being |
| // inherited. |
| EXPECT_EQ(nullptr, EvalJs(popup_contents, "window.foo")); |
| } |
| } |
| |
| // Ensure that opening popups to empty URLs does not fail FilterURL. The |
| // renderer process treats empty URLs as about:blank, but the browser process |
| // does not consider them valid and may treat them as attempts to go to the NTP |
| // in some cases. As a result, the renderer should map empty URLs to about:blank |
| // before making navigation requests. See https://crbug.com/1357515. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FilterURL_EmptyURL) { |
| GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| ASSERT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| { |
| SCOPED_TRACE(testing::Message() << "Testing opener case."); |
| base::HistogramTester histograms; |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE(ExecJs(shell(), "window.open('');")); |
| WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); |
| EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href")); |
| EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL()); |
| histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL", |
| 0); |
| } |
| |
| { |
| SCOPED_TRACE(testing::Message() << "Testing noopener case."); |
| base::HistogramTester histograms; |
| ShellAddedObserver new_shell_observer; |
| EXPECT_TRUE(ExecJs(shell(), "window.open('', '', 'noopener');")); |
| WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); |
| EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href")); |
| EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL()); |
| histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL", |
| 0); |
| } |
| } |
| |
| // Check that an about:blank popup opened from a WebUI page is not allowed to |
| // execute Javascript URLs. chrome:// pages don't allow executing javascript: |
| // URLs, so an about:blank popup opened by one should not be allowed to either, |
| // despite its URL not having the chrome: scheme. See |
| // https://crbug.com/1471305. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| JavascriptURLBlockedInAboutBlankWebUiPopup) { |
| GURL webui_url = GetWebUIURL(kChromeUIGpuHost); |
| ASSERT_TRUE(NavigateToURL(shell(), webui_url)); |
| |
| // Open an about:blank popup which should inherit the WebUI origin, and set |
| // some state in window.foo. |
| Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "popup"); |
| EXPECT_TRUE(new_shell); |
| EXPECT_TRUE(ExecJs(new_shell, "window.foo = 123;")); |
| EXPECT_EQ(123, EvalJs(new_shell, "window.foo")); |
| |
| // Try to execute a Javascript URL that modifies window.foo in the popup via |
| // a browser-initiated navigation. This should be blocked, and the value of |
| // window.foo should stay unchanged. |
| new_shell->LoadURL(GURL("javascript:window.foo=456")); |
| EXPECT_EQ(123, EvalJs(new_shell, "window.foo")); |
| } |
| |
| // Same test as above, but with a sandboxed about:blank WebUI popup, which |
| // should still not be allowed to execute Javascript URLs. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| JavascriptURLBlockedInSandboxedWebUiPopup) { |
| GURL webui_url = GetWebUIURL(kChromeUIGpuHost); |
| ASSERT_TRUE(NavigateToURL(shell(), webui_url)); |
| |
| // Add a sandboxed about:blank iframe. |
| { |
| std::string script = |
| "var frame = document.createElement('iframe');\n" |
| "frame.sandbox = 'allow-scripts allow-popups';\n" |
| "document.body.appendChild(frame);\n"; |
| EXPECT_TRUE(ExecJs(shell(), script)); |
| } |
| |
| // Open an about:blank popup from that sandboxed iframe, which should have an |
| // opaque origin with the WebUI origin as the precursor. Set some state in |
| // window.foo in that popup. |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| Shell* new_shell = |
| OpenPopup(root->child_at(0), GURL(url::kAboutBlankURL), "popup"); |
| EXPECT_TRUE(new_shell); |
| EXPECT_TRUE(ExecJs(new_shell, "window.foo = 123;")); |
| EXPECT_EQ(123, EvalJs(new_shell, "window.foo")); |
| |
| // Try to execute a Javascript URL that modifies window.foo in the popup via |
| // a browser-initiated navigation. This should be blocked, and the value of |
| // window.foo should stay unchanged. |
| new_shell->LoadURL(GURL("javascript:window.foo=456")); |
| EXPECT_EQ(123, EvalJs(new_shell, "window.foo")); |
| } |
| |
| // Test navigation with site instances whose storage partitions are fixed. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FixedStoragePartition) { |
| auto* browser_context = shell()->web_contents()->GetBrowserContext(); |
| auto storage_partition_config = StoragePartitionConfig::Create( |
| browser_context, "NavigationBrowserTest", "FixedStoragePartition", true); |
| auto url = embedded_test_server()->GetURL("/"); |
| auto* shell = Shell::CreateNewWindow( |
| browser_context, url, |
| SiteInstanceImpl::CreateForFixedStoragePartition( |
| browser_context, url, storage_partition_config), |
| gfx::Size()); |
| |
| auto GetSiteInstance = [](Shell* shell) { |
| return static_cast<SiteInstanceImpl*>( |
| shell->web_contents()->GetSiteInstance()); |
| }; |
| |
| EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(), |
| storage_partition_config); |
| EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition()); |
| |
| // Check navigation. |
| ASSERT_TRUE( |
| NavigateToURL(shell, embedded_test_server()->GetURL("/title1.html"))); |
| EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(), |
| storage_partition_config); |
| EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition()); |
| |
| // Check opening a window. The new window should stay in the same |
| // BrowsingInstance and StoragePartition. |
| { |
| ShellAddedObserver observer; |
| auto destination = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| EXPECT_TRUE(ExecJs(shell, JsReplace("window.open($1)", destination), |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| auto* popup = observer.GetShell(); |
| EXPECT_TRUE(WaitForLoadStop(popup->web_contents())); |
| EXPECT_EQ(popup->web_contents()->GetLastCommittedURL(), destination); |
| EXPECT_EQ(GetSiteInstance(popup)->GetBrowsingInstanceId(), |
| GetSiteInstance(shell)->GetBrowsingInstanceId()); |
| EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(), |
| storage_partition_config); |
| EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition()); |
| } |
| |
| // Check opening a window with about:blank at the beginning, and then navigate |
| // it. It should stay in the same BrowsingInstance and StoragePartition. |
| { |
| ShellAddedObserver observer; |
| EXPECT_TRUE(ExecJs(shell, "newWindow = window.open()", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| auto* popup = observer.GetShell(); |
| EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(), |
| storage_partition_config); |
| EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition()); |
| |
| auto destination = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| EXPECT_TRUE(ExecJs(shell, |
| JsReplace("newWindow.location.href = $1", destination), |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| EXPECT_TRUE(WaitForLoadStop(popup->web_contents())); |
| EXPECT_EQ(popup->web_contents()->GetLastCommittedURL(), destination); |
| EXPECT_EQ(GetSiteInstance(popup)->GetBrowsingInstanceId(), |
| GetSiteInstance(shell)->GetBrowsingInstanceId()); |
| EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(), |
| storage_partition_config); |
| EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition()); |
| } |
| |
| // Check navigation again. |
| ASSERT_TRUE( |
| NavigateToURL(shell, embedded_test_server()->GetURL("/title2.html"))); |
| EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(), |
| storage_partition_config); |
| EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition()); |
| |
| // Check navigation that triggers a BrowsingInstance swap, and the storage |
| // partition config should also be preserved. |
| auto browsing_instance_id = GetSiteInstance(shell)->GetBrowsingInstanceId(); |
| ASSERT_TRUE(NavigateToURL( |
| shell, embedded_test_server()->GetURL("c.com", "/title2.html"))); |
| EXPECT_NE(GetSiteInstance(shell)->GetBrowsingInstanceId(), |
| browsing_instance_id); |
| EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(), |
| storage_partition_config); |
| EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition()); |
| } |
| |
| // Exercises the restored session history traversal code path which uses |
| // RESTORE navigation types, rather than HISTORY_{SAME|DIFFERENT}_DOCUMENT, |
| // which code might erroneously expect. See https://crbug.com/40068335. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| TraversingToRestoredEntryUsesRestoreType) { |
| ASSERT_TRUE( |
| web_contents()->GetController().GetActiveEntry()->IsInitialEntry()); |
| |
| const GURL url1(embedded_test_server()->GetURL("/title1.html")); |
| const GURL url2(embedded_test_server()->GetURL("/title2.html")); |
| const GURL url3(embedded_test_server()->GetURL("/title2.html#samedoc")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(NavigateToURL(shell(), url2)); |
| EXPECT_TRUE(NavigateToURL(shell(), url3)); |
| |
| // Clone the tab and load the page. |
| std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone(); |
| WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get()); |
| NavigationController& new_controller = new_tab_impl->GetController(); |
| |
| { |
| TestNavigationObserver clone_observer(new_tab.get()); |
| new_controller.LoadIfNecessary(); |
| clone_observer.Wait(); |
| } |
| |
| // Back to url2 which is a same document navigation but uses RESTORE. |
| { |
| NavigationHandleCommitObserver observer(new_tab.get(), url2); |
| ASSERT_TRUE(HistoryGoBack(new_tab_impl)); |
| EXPECT_EQ(observer.navigation_type(), |
| blink::mojom::NavigationType::RESTORE); |
| } |
| |
| // Back to url1 which is a cross document navigation but uses RESTORE. |
| { |
| NavigationHandleCommitObserver observer(new_tab.get(), url1); |
| ASSERT_TRUE(HistoryGoBack(new_tab_impl)); |
| EXPECT_EQ(observer.navigation_type(), |
| blink::mojom::NavigationType::RESTORE); |
| } |
| } |
| |
| class NavigationBrowserTestDeprecateUnloadOptOut |
| : public NavigationBrowserTest, |
| public ::testing::WithParamInterface<bool> { |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| NavigationBrowserTest::SetUpCommandLine(command_line); |
| if (IsOptOutEnabled()) { |
| scoped_feature_list_.InitWithFeatures( |
| {network::features::kDeprecateUnload, |
| blink::features::kDeprecateUnloadOptOut}, |
| {}); |
| } else { |
| scoped_feature_list_.InitWithFeatures( |
| {network::features::kDeprecateUnload}, |
| {blink::features::kDeprecateUnloadOptOut}); |
| } |
| } |
| |
| protected: |
| bool IsOptOutEnabled() const { return GetParam(); } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| NavigationBrowserTestDeprecateUnloadOptOut, |
| ::testing::Bool()); |
| |
| // Test that enabled/disabled kDeprecateUnloadOptOut has the desired effect. |
| IN_PROC_BROWSER_TEST_P(NavigationBrowserTestDeprecateUnloadOptOut, |
| DeprecateUnloadOptOutFlagRespected) { |
| GURL url_1(embedded_test_server()->GetURL("/title1.html")); |
| GURL url_2(embedded_test_server()->GetURL("/title2.html")); |
| |
| // Unload will not run on Android if the page is cacheable. |
| web_contents()->GetController().GetBackForwardCache().DisableForTesting( |
| BackForwardCacheImpl::TEST_USES_UNLOAD_EVENT); |
| // Navigate to a page and install an unload handler with a side-effect. |
| ASSERT_TRUE(NavigateToURL(web_contents(), url_1)); |
| ASSERT_TRUE(ExecJs(web_contents(), R"( |
| localStorage.setItem("unload", "not_dispatched"); |
| addEventListener("unload", () => { |
| localStorage.setItem("unload", "dispatched"); |
| }) |
| )")); |
| |
| // Navigate to a same-site page (to ensure that the unload handler's |
| // side-effect is reliably visible). |
| ASSERT_TRUE(NavigateToURL(web_contents(), url_2)); |
| |
| // Check for the side-effect. |
| ASSERT_EQ(EvalJs(web_contents(), "localStorage.getItem('unload')"), |
| IsOptOutEnabled() ? "dispatched" : "not_dispatched"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FCPMetrics) { |
| GURL url_1(embedded_test_server()->GetURL("/title1.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), url_1)); |
| |
| GURL url_2(embedded_test_server()->GetURL("/title2.html")); |
| NavigationController::LoadURLParams params(url_2); |
| params.transition_type = ui::PageTransitionFromInt( |
| ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); |
| web_contents()->GetController().LoadURLWithParams(params); |
| WaitForHistogramRecordedInChildProcess( |
| "Navigation.FCPFrameSubmittedBeforeSurfaceEmbed"); |
| } |
| |
| // Tests that if the main frame has focus before a same-site navigation, it's |
| // kept after navigation. |
| // Regression test for crbug.com/360705823. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| FocusPreservedOnNavigation_MainFrame) { |
| GURL url_1(embedded_test_server()->GetURL("/page_with_iframe.html")); |
| GURL url_2(embedded_test_server()->GetURL("/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), url_1)); |
| |
| // Set focus on a button in the main frame. |
| ASSERT_TRUE(ExecJs(current_frame_host(), R"( |
| let button = document.createElement('button'); |
| document.body.appendChild(button); |
| button.focus(); |
| )")); |
| // The main document should have focus. |
| ASSERT_EQ(true, EvalJs(current_frame_host(), "document.hasFocus();")); |
| |
| // After navigation, the main document should still have focus. |
| ASSERT_TRUE(NavigateToURL(shell(), url_2)); |
| ASSERT_EQ(true, EvalJs(current_frame_host(), "document.hasFocus();")); |
| } |
| |
| // Same as the above test, but the focus is on the iframe. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| FocusPreservedOnNavigation_Subframe) { |
| GURL url_1(embedded_test_server()->GetURL("/page_with_iframe.html")); |
| GURL url_2(embedded_test_server()->GetURL("/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), url_1)); |
| |
| // Set focus on a button in the iframe. |
| FrameTreeNode* child_ftn = current_frame_host()->child_at(0); |
| ASSERT_TRUE(ExecJs(child_ftn, R"( |
| let button = document.createElement('button'); |
| document.body.appendChild(button); |
| button.focus(); |
| )")); |
| // The iframe document should have focus. |
| ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();")); |
| |
| // After navigation, the child document should still have focus. |
| ASSERT_TRUE(NavigateFrameToURL(child_ftn, url_2)); |
| ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();")); |
| } |
| |
| // When the navigation is cross-site, focus is not preserved. |
| IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, |
| FocusNotPreservedOnNavigation_SubframeCrossSite) { |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP() << "Test needs local -> remote swap"; |
| } |
| GURL url_1(embedded_test_server()->GetURL("a.com", "/page_with_iframe.html")); |
| GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), url_1)); |
| |
| // Set focus on a button in the iframe. |
| FrameTreeNode* child_ftn = current_frame_host()->child_at(0); |
| ASSERT_TRUE(ExecJs(child_ftn, R"( |
| let button = document.createElement('button'); |
| document.body.appendChild(button); |
| button.focus(); |
| )")); |
| // The iframe document should have focus. |
| ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();")); |
| |
| // After navigation, the child document should no longer have focus. |
| ASSERT_TRUE(NavigateFrameToURL(child_ftn, url_2)); |
| ASSERT_EQ(false, EvalJs(child_ftn, "document.hasFocus();")); |
| } |
| |
| class NavigationWithPageSwapBrowserTest : public NavigationBrowserTest { |
| public: |
| NavigationWithPageSwapBrowserTest() { |
| feature_list_.InitAndEnableFeature(blink::features::kPageSwapEvent); |
| } |
| |
| bool NavigateBack(WebContentsImpl* contents) { |
| auto result = EvalJs(contents, JsReplace( |
| R"( |
| (async () => { |
| let pageswapfired = new Promise((resolve) => { |
| onpageswap = (e) => { |
| activation = e.activation; |
| resolve(activation); |
| }; |
| }); |
| history.back(); |
| let result = await pageswapfired; |
| return result != null; |
| })(); |
| )")); |
| return result.ExtractBool(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(NavigationWithPageSwapBrowserTest, |
| PageSwapForInitialEntry) { |
| ASSERT_TRUE( |
| web_contents()->GetController().GetActiveEntry()->IsInitialEntry()); |
| |
| // TODO(khushalsagar): Assert that pageswap is fired without activation. The |
| // test script is hitting an issue for the initial Document. |
| ASSERT_TRUE(NavigateToURL(web_contents(), |
| embedded_test_server()->GetURL("/title1.html"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NavigationWithPageSwapBrowserTest, |
| PageSwapWhenTraversingToRestoredEntry) { |
| ASSERT_TRUE( |
| web_contents()->GetController().GetActiveEntry()->IsInitialEntry()); |
| |
| const GURL url1(embedded_test_server()->GetURL("/title1.html")); |
| const GURL url2(embedded_test_server()->GetURL("/title2.html")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url1)); |
| EXPECT_TRUE(NavigateToURL(shell(), url2)); |
| |
| // Clone the tab and load the page. |
| std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone(); |
| WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get()); |
| NavigationController& new_controller = new_tab_impl->GetController(); |
| |
| { |
| TestNavigationObserver clone_observer(new_tab.get()); |
| new_controller.LoadIfNecessary(); |
| clone_observer.Wait(); |
| } |
| |
| ASSERT_TRUE(NavigateBack(new_tab_impl)); |
| } |
| |
| class NavigationBrowserTestPaintHoldingSubframe |
| : public NavigationBrowserTest, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| NavigationBrowserTestPaintHoldingSubframe() { |
| // Paint holding for in-process iframes is only enabled when there is a |
| // ViewTransition. |
| paint_holding_feature_.InitWithFeatures( |
| {blink::features::kPaintHoldingForIframes, |
| blink::features::kViewTransitionOnNavigationForIframes}, |
| {}); |
| |
| const bool enable_render_document = GetParam(); |
| if (enable_render_document) { |
| InitAndEnableRenderDocumentFeature( |
| &render_document_feature_, |
| GetRenderDocumentLevelName(RenderDocumentLevel::kSubframe)); |
| } else { |
| InitAndEnableRenderDocumentFeature( |
| &render_document_feature_, |
| GetRenderDocumentLevelName(RenderDocumentLevel::kCrashedFrame)); |
| } |
| |
| auto* command_line = base::CommandLine::ForCurrentProcess(); |
| |
| // This test requires cross-process iframes. |
| command_line->AppendSwitch(switches::kSitePerProcess); |
| } |
| |
| void SetUp() override { |
| EnablePixelOutput(); |
| NavigationBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| embedded_test_server()->RegisterRequestHandler(base::BindRepeating( |
| &NavigationBrowserTestPaintHoldingSubframe::HandleSlowStyleSheet, |
| base::Unretained(this))); |
| NavigationBrowserTest::SetUpOnMainThread(); |
| } |
| |
| protected: |
| void WaitForStylesheetRequest() { |
| if (start_response_) { |
| return; |
| } |
| |
| base::RunLoop run_loop; |
| run_loop_ = &run_loop; |
| run_loop.Run(); |
| run_loop_ = nullptr; |
| } |
| |
| void FinishStylesheetRequest(RenderFrameHost* rfh) { |
| std::move(start_response_).Run(); |
| std::move(finish_response_).Run(); |
| |
| // Ensure that the stylesheet response unblocks rendering for this Document. |
| ASSERT_TRUE(ExecJs(rfh, JsReplace( |
| R"( |
| (async () => { |
| let rafFired = new Promise((resolve) => { |
| requestAnimationFrame(resolve); |
| }); |
| await rafFired; |
| })(); |
| )"))); |
| } |
| |
| SkBitmap CopyView(RenderWidgetHostView* view) { |
| base::RunLoop run_loop; |
| run_loop_ = &run_loop; |
| |
| constexpr gfx::Size kOutputSize(10, 10); |
| view->CopyFromSurface( |
| gfx::Rect(), kOutputSize, |
| base::BindOnce(&NavigationBrowserTestPaintHoldingSubframe::OnCopyDone, |
| base::Unretained(this))); |
| |
| run_loop.Run(); |
| run_loop_ = nullptr; |
| return bitmap_; |
| } |
| |
| private: |
| class SlowHttpResponseNoCaching : public SlowHttpResponse { |
| public: |
| explicit SlowHttpResponseNoCaching(GotRequestCallback got_request) |
| : SlowHttpResponse(std::move(got_request)) {} |
| |
| base::StringPairs ResponseHeaders() override { |
| auto response = SlowHttpResponse::ResponseHeaders(); |
| // Disable response caching. |
| response.emplace_back("Cache-Control", "max-age=0"); |
| return response; |
| } |
| }; |
| |
| std::unique_ptr<net::test_server::HttpResponse> HandleSlowStyleSheet( |
| const net::test_server::HttpRequest& request) { |
| if (request.relative_url != "/slow-response") { |
| return nullptr; |
| } |
| return std::make_unique<SlowHttpResponseNoCaching>(base::BindOnce( |
| &NavigationBrowserTestPaintHoldingSubframe::OnStylesheetRequest, |
| base::Unretained(this))); |
| } |
| |
| void OnStylesheetRequest(base::OnceClosure start_response, |
| base::OnceClosure finish_response) { |
| start_response_ = std::move(start_response); |
| finish_response_ = std::move(finish_response); |
| |
| if (run_loop_) { |
| run_loop_->Quit(); |
| } |
| } |
| |
| void OnCopyDone(const SkBitmap& bitmap) { |
| bitmap_ = bitmap; |
| run_loop_->Quit(); |
| } |
| |
| SkBitmap bitmap_; |
| raw_ptr<base::RunLoop> run_loop_; |
| base::OnceClosure start_response_; |
| base::OnceClosure finish_response_; |
| base::test::ScopedFeatureList paint_holding_feature_; |
| base::test::ScopedFeatureList render_document_feature_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, Basic) { |
| auto* web_contents = shell()->web_contents(); |
| |
| GURL main_url( |
| embedded_test_server()->GetURL("/render-blocking-mainframe.html")); |
| ASSERT_TRUE(NavigateToURL(web_contents, main_url)); |
| |
| const std::string iframe_id = "iframe_id"; |
| RenderFrameHostImpl* subframe_rfh = nullptr; |
| |
| { |
| const std::string kCreateIFrameWithID = R"( |
| const iframe = document.createElement("iframe"); |
| iframe.id = $1; |
| document.body.appendChild(iframe); |
| )"; |
| ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(), |
| JsReplace(kCreateIFrameWithID, "iframe_id"))); |
| |
| GURL subframe_url(embedded_test_server()->GetURL( |
| "a.com", "/render-blocking-subframe.html")); |
| TestNavigationObserver load_observer(web_contents); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); |
| load_observer.WaitForNavigationFinished(); |
| WaitForStylesheetRequest(); |
| |
| // We should have a cross-process iframe which is a local root. |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| ASSERT_TRUE(subframe_rfh->is_local_root()); |
| |
| FinishStylesheetRequest(subframe_rfh); |
| WaitForCopyableViewInWebContents(web_contents); |
| } |
| |
| // The frame is displaying blue. |
| WaitForCopyableViewInFrame(subframe_rfh); |
| auto bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap); |
| |
| { |
| GURL subframe_url(embedded_test_server()->GetURL( |
| "a.com", "/render-blocking-subframe.html?red")); |
| |
| TestNavigationObserver load_observer(web_contents); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); |
| load_observer.WaitForNavigationFinished(); |
| WaitForStylesheetRequest(); |
| |
| // The subframe RFH could have changed. |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| ASSERT_TRUE(subframe_rfh->is_local_root()); |
| } |
| |
| // The frame should continue to display blue from paint holding. |
| WaitForCopyableViewInWebContents(web_contents); |
| bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE); |
| |
| // Respond to the stylesheet request which will resume rendering in the |
| // subframe. |
| FinishStylesheetRequest(subframe_rfh); |
| |
| // Now the frame is displaying red. |
| WaitForCopyableViewInFrame(subframe_rfh); |
| bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, |
| BasicInProcessIframe) { |
| auto* web_contents = shell()->web_contents(); |
| |
| GURL main_url( |
| embedded_test_server()->GetURL("/render-blocking-mainframe.html")); |
| ASSERT_TRUE(NavigateToURL(web_contents, main_url)); |
| |
| const std::string iframe_id = "iframe_id"; |
| RenderFrameHostImpl* subframe_rfh = nullptr; |
| |
| { |
| const std::string kCreateIFrameWithID = R"( |
| const iframe = document.createElement("iframe"); |
| iframe.id = $1; |
| document.body.appendChild(iframe); |
| )"; |
| ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(), |
| JsReplace(kCreateIFrameWithID, "iframe_id"))); |
| |
| GURL subframe_url( |
| embedded_test_server()->GetURL("/render-blocking-subframe.html")); |
| TestNavigationObserver load_observer(web_contents); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); |
| load_observer.WaitForNavigationFinished(); |
| WaitForStylesheetRequest(); |
| |
| // We should have a same-process iframe which is not a local root. |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| ASSERT_FALSE(subframe_rfh->is_local_root()); |
| ASSERT_EQ(subframe_rfh->GetProcess(), |
| web_contents->GetPrimaryMainFrame()->GetProcess()); |
| |
| FinishStylesheetRequest(subframe_rfh); |
| WaitForCopyableViewInWebContents(web_contents); |
| } |
| |
| { |
| const std::string kInjectVTOptIn = R"( |
| const style = document.createElement("style"); |
| style.innerHTML = "@view-transition { navigation: auto; }" |
| document.head.appendChild(style); |
| )"; |
| ASSERT_TRUE(ExecJs(subframe_rfh, kInjectVTOptIn)); |
| } |
| |
| // The frame is displaying blue. |
| WaitForCopyableViewInFrame(subframe_rfh); |
| auto bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap); |
| |
| { |
| GURL subframe_url( |
| embedded_test_server()->GetURL("/render-blocking-subframe.html?red")); |
| |
| TestNavigationObserver load_observer(web_contents); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); |
| load_observer.WaitForNavigationFinished(); |
| WaitForStylesheetRequest(); |
| |
| // The subframe RFH could have changed. |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| ASSERT_FALSE(subframe_rfh->is_local_root()); |
| ASSERT_EQ(subframe_rfh->GetProcess(), |
| web_contents->GetPrimaryMainFrame()->GetProcess()); |
| } |
| |
| // The frame should continue to display blue from paint holding. |
| WaitForCopyableViewInWebContents(web_contents); |
| bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE); |
| |
| // Respond to the stylesheet request which will resume rendering in the |
| // subframe. |
| FinishStylesheetRequest(subframe_rfh); |
| |
| // Now the frame is displaying red. |
| WaitForCopyableViewInFrame(subframe_rfh); |
| bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, CrossOrigin) { |
| auto* web_contents = shell()->web_contents(); |
| |
| GURL main_url( |
| embedded_test_server()->GetURL("/render-blocking-mainframe.html")); |
| ASSERT_TRUE(NavigateToURL(web_contents, main_url)); |
| |
| const std::string iframe_id = "iframe_id"; |
| RenderFrameHostImpl* subframe_rfh = nullptr; |
| |
| { |
| const std::string kCreateIFrameWithID = R"( |
| const iframe = document.createElement("iframe"); |
| iframe.id = $1; |
| document.body.appendChild(iframe); |
| )"; |
| ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(), |
| JsReplace(kCreateIFrameWithID, "iframe_id"))); |
| |
| GURL subframe_url(embedded_test_server()->GetURL( |
| "a.com", "/render-blocking-subframe.html")); |
| TestNavigationObserver load_observer(web_contents); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); |
| load_observer.WaitForNavigationFinished(); |
| WaitForStylesheetRequest(); |
| |
| // We should have a cross-process iframe which is a local root. |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| ASSERT_TRUE(subframe_rfh->is_local_root()); |
| |
| FinishStylesheetRequest(subframe_rfh); |
| WaitForCopyableViewInWebContents(web_contents); |
| } |
| |
| // The frame is displaying blue. |
| WaitForCopyableViewInFrame(subframe_rfh); |
| auto bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap); |
| |
| { |
| GURL subframe_url(embedded_test_server()->GetURL( |
| "b.com", "/render-blocking-subframe.html?red")); |
| |
| TestNavigationObserver load_observer(web_contents); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); |
| load_observer.WaitForNavigationFinished(); |
| WaitForStylesheetRequest(); |
| |
| // The subframe RFH could have changed. |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| ASSERT_TRUE(subframe_rfh->is_local_root()); |
| } |
| |
| // The frame is displaying white (from the main frame) because paint holding |
| // is disabled. |
| WaitForCopyableViewInWebContents(web_contents); |
| bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorWHITE); |
| |
| // Respond to the stylesheet request which will resume rendering in the |
| // subframe. |
| FinishStylesheetRequest(subframe_rfh); |
| |
| // Now the frame is displaying red. |
| WaitForCopyableViewInFrame(subframe_rfh); |
| bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, |
| CrashSubframe) { |
| auto* web_contents = shell()->web_contents(); |
| |
| GURL main_url( |
| embedded_test_server()->GetURL("/render-blocking-mainframe.html")); |
| ASSERT_TRUE(NavigateToURL(web_contents, main_url)); |
| |
| const std::string iframe_id = "iframe_id"; |
| RenderFrameHostImpl* subframe_rfh = nullptr; |
| |
| { |
| const std::string kCreateIFrameWithID = R"( |
| const iframe = document.createElement("iframe"); |
| iframe.id = $1; |
| document.body.appendChild(iframe); |
| )"; |
| ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(), |
| JsReplace(kCreateIFrameWithID, "iframe_id"))); |
| |
| GURL subframe_url(embedded_test_server()->GetURL( |
| "a.com", "/render-blocking-subframe.html")); |
| TestNavigationObserver load_observer(web_contents); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); |
| load_observer.WaitForNavigationFinished(); |
| WaitForStylesheetRequest(); |
| |
| // We should have a cross-process iframe which is a local root. |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| ASSERT_TRUE(subframe_rfh->is_local_root()); |
| |
| FinishStylesheetRequest(subframe_rfh); |
| WaitForCopyableViewInWebContents(web_contents); |
| } |
| |
| // The frame is displaying blue. |
| WaitForCopyableViewInFrame(subframe_rfh); |
| auto bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap); |
| |
| // Crash the subframe. |
| { |
| auto* process = subframe_rfh->GetProcess(); |
| content::ScopedAllowRendererCrashes allow_renderer_crashes(process); |
| |
| RenderProcessHostWatcher watcher( |
| process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| process->Shutdown(content::RESULT_CODE_KILLED); |
| watcher.Wait(); |
| } |
| |
| { |
| GURL subframe_url(embedded_test_server()->GetURL( |
| "a.com", "/render-blocking-subframe.html?red")); |
| |
| TestNavigationObserver load_observer(web_contents); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); |
| load_observer.WaitForNavigationFinished(); |
| WaitForStylesheetRequest(); |
| |
| // The subframe RFH could have changed. |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| ASSERT_TRUE(subframe_rfh->is_local_root()); |
| } |
| |
| // The frame is displaying white (from the main frame) because paint holding |
| // is disabled. |
| WaitForCopyableViewInWebContents(web_contents); |
| bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorWHITE) << cc::GetPNGDataUrl(bitmap); |
| |
| // Respond to the stylesheet request which will resume rendering in the |
| // subframe. |
| FinishStylesheetRequest(subframe_rfh); |
| |
| // Now the frame is displaying red. |
| WaitForCopyableViewInFrame(subframe_rfh); |
| bitmap = CopyView(web_contents->GetRenderWidgetHostView()); |
| EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| NavigationBrowserTestPaintHoldingSubframe, |
| ::testing::Bool()); |
| |
| RenderFrameHostImpl* GetMainFrameSpeculativeRFH(WebContentsImpl* web_contents) { |
| return web_contents->GetPrimaryFrameTree() |
| .root() |
| ->render_manager() |
| ->speculative_frame_host(); |
| } |
| |
| void VerifyDeferSpeculativeRFHActionUMA(const base::HistogramTester& tester, |
| DeferSpeculativeRFHAction action) { |
| tester.ExpectUniqueSample("Navigation.DeferSpeculativeRFHAction", |
| static_cast<int>(action), 1); |
| } |
| |
| class DeferSpeculativeRFHCreationTest : public NavigationBrowserTest { |
| public: |
| DeferSpeculativeRFHCreationTest() { |
| feature_list_.InitAndEnableFeature(features::kDeferSpeculativeRFHCreation); |
| // Enable render document for all frames to ensure a speculative RFH |
| // will be created during navigation. |
| InitAndEnableRenderDocumentFeature( |
| &render_document_feature_, |
| GetRenderDocumentLevelName(RenderDocumentLevel::kAllFrames)); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| base::test::ScopedFeatureList render_document_feature_; |
| }; |
| |
| class DeferSpeculativeRFHCreationRenderProcessTest |
| : public NavigationBrowserTest, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| DeferSpeculativeRFHCreationRenderProcessTest() |
| : warmup_spare_render_process_(GetParam()) { |
| std::map<std::string, std::string> parameters = { |
| {"warmup_spare_process", base::ToString(GetParam())}, |
| }; |
| defer_rfh_feature_list_.InitAndEnableFeatureWithParameters( |
| features::kDeferSpeculativeRFHCreation, parameters); |
| android_spare_rederer_feature_.InitAndEnableFeatureWithParameters( |
| features::kAndroidWarmUpSpareRendererWithTimeout, |
| base::FieldTrialParams{{"spare_renderer_memory_threshold", "0"}}); |
| InitAndEnableRenderDocumentFeature( |
| &render_document_feature_, |
| GetRenderDocumentLevelName(RenderDocumentLevel::kAllFrames)); |
| } |
| |
| // A new renderer process will only be created for a cross-RFH navigation if |
| // it involves a SiteInstanceGroup change, which will happen if site isolation |
| // or BFCache is turned on |
| bool WillWarmupSpareRenderProcess() { return warmup_spare_render_process_; } |
| |
| bool WillAllocateNewProcess() { |
| return AreAllSitesIsolatedForTesting() || IsBackForwardCacheEnabled(); |
| } |
| |
| private: |
| bool warmup_spare_render_process_; |
| base::test::ScopedFeatureList defer_rfh_feature_list_; |
| base::test::ScopedFeatureList android_spare_rederer_feature_; |
| base::test::ScopedFeatureList render_document_feature_; |
| }; |
| |
| // Verify the common flow for with DeferSpeculativeRFHCreation feature. |
| // The creation of the speculative RFH will be deferred until the network |
| // request is sent. |
| IN_PROC_BROWSER_TEST_P(DeferSpeculativeRFHCreationRenderProcessTest, |
| SpeculativeRFHCreationDeferred) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| RenderProcessHost* first_navigation_process = |
| main_frame()->render_manager()->current_frame_host()->GetProcess(); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| SpareRenderProcessHostManagerImpl::Get().CleanupSparesForTesting(); |
| SpareRenderProcessHostStartedObserver spare_started_observer; |
| |
| GURL url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| TestNavigationManager nav_manager(web_contents, url); |
| base::HistogramTester histogram_tester; |
| |
| // The speculative RFH shall not be created when the navigation request is |
| // created. |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url)); |
| DeferSpeculativeRFHAction expected_action = |
| WillWarmupSpareRenderProcess() |
| ? DeferSpeculativeRFHAction::kDeferredWithRenderProcessWarmUp |
| : DeferSpeculativeRFHAction::kDeferredWithoutRenderProcessWarmUp; |
| VerifyDeferSpeculativeRFHActionUMA(histogram_tester, expected_action); |
| NavigationRequest* navigation_request = |
| NavigationRequest::From(nav_manager.GetNavigationHandle()); |
| RenderProcessHost* created_process = nullptr; |
| if (WillWarmupSpareRenderProcess()) { |
| created_process = spare_started_observer.WaitForSpareRenderProcessStarted(); |
| } |
| ASSERT_EQ(!!created_process, WillWarmupSpareRenderProcess()); |
| ASSERT_TRUE(navigation_request); |
| // The navigation manager pauses the navigation in the WillStartRequest |
| // throttle. The speculative RFH will be created after the throttle completes |
| // and the navigation request is sent. |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_START_REQUEST); |
| // The loader will not be created until the WillStartRequest throttle check |
| // completed. |
| ASSERT_FALSE(navigation_request->HasLoader()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::NONE); |
| |
| nav_manager.WaitForSpeculativeRenderFrameHostCreation(); |
| // The speculative RFH shall be created after sending the request. |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_START_REQUEST); |
| ASSERT_TRUE(navigation_request->HasLoader()); |
| RenderFrameHostImplWrapper speculative_rfh( |
| GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_TRUE(speculative_rfh); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); |
| if (!WillAllocateNewProcess()) { |
| ASSERT_EQ(speculative_rfh->GetSiteInstance()->GetProcess(), |
| first_navigation_process); |
| } else if (WillWarmupSpareRenderProcess()) { |
| ASSERT_EQ(speculative_rfh->GetSiteInstance()->GetProcess(), |
| created_process); |
| } |
| // The speculative RFH shall become the primary RFH when the navigation is |
| // committed. |
| ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_EQ(main_frame()->render_manager()->current_frame_host(), |
| speculative_rfh.get()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| DeferSpeculativeRFHCreationRenderProcessTest, |
| ::testing::Bool()); |
| |
| // Verify that navigating from a crashed page will create a speculative |
| // RFH at once. |
| IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, |
| NavigationFromCrashedFrameNotDeferred) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| // Crash the frame. |
| { |
| auto* process = main_frame() |
| ->GetRenderFrameHostManager() |
| .current_frame_host() |
| ->GetProcess(); |
| content::ScopedAllowRendererCrashes allow_renderer_crashes(process); |
| |
| RenderProcessHostWatcher watcher( |
| process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| process->Shutdown(content::RESULT_CODE_KILLED); |
| watcher.Wait(); |
| } |
| |
| GURL url = embedded_test_server()->GetURL("a.com", "/title2.html"); |
| TestNavigationManager nav_manager(web_contents, url); |
| // Navigation from a crashed frame shall immediately create a speculative RFH. |
| base::HistogramTester histogram_tester; |
| shell()->LoadURL(url); |
| VerifyDeferSpeculativeRFHActionUMA(histogram_tester, |
| DeferSpeculativeRFHAction::kNotDeferred); |
| NavigationRequest* navigation_request = |
| NavigationRequest::From(nav_manager.GetNavigationHandle()); |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_START_REQUEST); |
| ASSERT_FALSE(navigation_request->HasLoader()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::CURRENT); |
| ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| } |
| |
| // Verify that the creation of the speculative RFH is not deferred for the |
| // web pages. |
| IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, |
| CreationNotDeferredForWebUI) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL url = GetWebUIURL(kChromeUIGpuHost); |
| TestNavigationManager nav_manager(web_contents, url); |
| // The speculative RFH shall be created when the navigation starts. |
| base::HistogramTester histogram_tester; |
| shell()->LoadURL(url); |
| VerifyDeferSpeculativeRFHActionUMA(histogram_tester, |
| DeferSpeculativeRFHAction::kNotDeferred); |
| ASSERT_TRUE(nav_manager.WaitForRequestStart()); |
| NavigationRequest* navigation_request = main_frame()->navigation_request(); |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_START_REQUEST); |
| ASSERT_FALSE(navigation_request->HasLoader()); |
| ASSERT_TRUE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); |
| ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| } |
| |
| // Verify that the creation of the speculative RFH is not deferred for the |
| // pages without a URL loader. |
| IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, |
| CreationNotDeferredWithoutURLLoader) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL url("about:blank"); |
| // The speculative RFH shall be created when the navigation starts. |
| base::HistogramTester histogram_tester; |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url)); |
| VerifyDeferSpeculativeRFHActionUMA(histogram_tester, |
| DeferSpeculativeRFHAction::kNotDeferred); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| } |
| |
| // Verify that the created speculative RFH after the network request will |
| // be correctly replaced if the redirection points to a different site. |
| IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, |
| SpeculativeRFHWithRedirect) { |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL redirect_url = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| GURL url = embedded_test_server()->GetURL( |
| "c.com", "/server-redirect?" + redirect_url.spec()); |
| TestNavigationManager nav_manager(web_contents, url); |
| |
| // The speculative RFH shall not be created when the navigation request is |
| // created. |
| base::HistogramTester histogram_tester; |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url)); |
| VerifyDeferSpeculativeRFHActionUMA( |
| histogram_tester, |
| DeferSpeculativeRFHAction::kDeferredWithoutRenderProcessWarmUp); |
| NavigationRequest* navigation_request = |
| NavigationRequest::From(nav_manager.GetNavigationHandle()); |
| ASSERT_TRUE(navigation_request); |
| // The navigation manager pauses the navigation in the WillStartRequest |
| // throttle. The speculative RFH will be created after the throttle completes |
| // and the navigation request is sent. |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_START_REQUEST); |
| // The loader will not be created until the WillStartRequest throttle check |
| // completed. |
| ASSERT_FALSE(navigation_request->HasLoader()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::NONE); |
| |
| nav_manager.WaitForSpeculativeRenderFrameHostCreation(); |
| // The speculative RFH shall be created after sending the request. |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_START_REQUEST); |
| ASSERT_TRUE(navigation_request->HasLoader()); |
| ASSERT_TRUE(GetMainFrameSpeculativeRFH(web_contents)); |
| RenderFrameHostImplWrapper speculative_rfh( |
| GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_TRUE(speculative_rfh); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); |
| |
| // After receiving the redirect, a new speculative RFH shall be created for |
| // the new site if site isolation is enabled. |
| ASSERT_TRUE(nav_manager.WaitForResponse()); |
| if (AreAllSitesIsolatedForTesting()) { |
| ASSERT_TRUE(speculative_rfh.IsDestroyed()); |
| } |
| RenderFrameHostImplWrapper new_speculative_rfh( |
| GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_TRUE(new_speculative_rfh); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); |
| // The speculative RFH shall become the primary RFH when the navigation is |
| // committed. |
| ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_EQ(main_frame()->render_manager()->current_frame_host(), |
| new_speculative_rfh.get()); |
| } |
| |
| // Test that if there is a navigation pending for commit, the deferred |
| // speculative RFH will not be created event after the request is sent. The new |
| // navigation will be queued until the pending navigation commits. |
| IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, |
| NavigateWithPendingCommit) { |
| // TODO(crbug.com/349487596): Enable the test after fixing the unrepsonive |
| // renderer issue. |
| if (!AreAllSitesIsolatedForTesting() && !IsBackForwardCacheEnabled()) { |
| return; |
| } |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| NavigationLogger logger(web_contents); |
| |
| // Create first navigation and pause before commit. |
| GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); |
| TestNavigationManager nav_manager_b(web_contents, url_b); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_b)); |
| nav_manager_b.WaitForSpeculativeRenderFrameHostCreation(); |
| RenderFrameHostImplWrapper speculative_rfh_b( |
| GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_TRUE(speculative_rfh_b); |
| ASSERT_TRUE(nav_manager_b.WaitForResponse()); |
| nav_manager_b.ResumeNavigation(); |
| CommitNavigationPauser commit_pauser(speculative_rfh_b.get()); |
| commit_pauser.WaitForCommitAndPause(); |
| |
| // Navigate to a new site, a new speculative RFH will not be created because |
| // of the pending navigation. |
| GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html"); |
| TestNavigationManager nav_manager_c(web_contents, url_c); |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_c)); |
| NavigationRequest* navigation_request = |
| NavigationRequest::From(nav_manager_c.GetNavigationHandle()); |
| ASSERT_TRUE(nav_manager_c.WaitForRequestStart()); |
| // Normally, the new navigation will create a speculative RFH after the |
| // network request is sent, but since there is a pre-existing speculative RFH |
| // for a pending commit navigation, the new navigation won't create a |
| // speculative RFH at this point. |
| nav_manager_c.ResumeNavigation(); |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_START_REQUEST); |
| ASSERT_TRUE(navigation_request->HasLoader()); |
| // Verify that the speculative RFH is not replaced by the new navigation. |
| ASSERT_EQ(speculative_rfh_b.get(), GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_FALSE(speculative_rfh_b.IsDestroyed()); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::NONE); |
| |
| commit_pauser.ResumePausedCommit(); |
| ASSERT_TRUE(nav_manager_b.WaitForNavigationFinished()); |
| // Verify that a new speculative RFH will be created after the pending |
| // navigation is committed. |
| ASSERT_TRUE(nav_manager_c.WaitForResponse()); |
| RenderFrameHostImplWrapper new_speculative_rfh( |
| GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_TRUE(new_speculative_rfh); |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_PROCESS_RESPONSE); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); |
| ASSERT_TRUE(nav_manager_c.WaitForNavigationFinished()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_EQ(main_frame()->render_manager()->current_frame_host(), |
| new_speculative_rfh.get()); |
| |
| // Check that all the navigations has been committed. |
| auto results = logger.results(); |
| ASSERT_EQ(2u, results.size()); |
| EXPECT_TRUE(results[0].committed); |
| EXPECT_EQ(url_b, results[0].url); |
| EXPECT_TRUE(results[1].committed); |
| EXPECT_EQ(url_c, results[1].url); |
| } |
| |
| class DeferSpeculativeRFHCreationReuseRFHTest : public NavigationBrowserTest { |
| public: |
| DeferSpeculativeRFHCreationReuseRFHTest() { |
| feature_list_.InitAndEnableFeature(features::kDeferSpeculativeRFHCreation); |
| render_document_feature_.InitAndDisableFeature(features::kRenderDocument); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| base::test::ScopedFeatureList render_document_feature_; |
| }; |
| |
| // Verify that navigating with the same RFH will reuse the RFH at once. |
| IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationReuseRFHTest, |
| ReuseSameRFHNotDeferred) { |
| ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank"))); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| TestNavigationManager nav_manager(web_contents, url); |
| // Navigation from about:blank will reuse the render frame host. |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url)); |
| NavigationRequest* navigation_request = |
| NavigationRequest::From(nav_manager.GetNavigationHandle()); |
| ASSERT_EQ(navigation_request->state(), |
| NavigationRequest::NavigationState::WILL_START_REQUEST); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_FALSE(navigation_request->HasLoader()); |
| ASSERT_TRUE(nav_manager.WaitForResponse()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| ASSERT_EQ(navigation_request->GetAssociatedRFHType(), |
| NavigationRequest::AssociatedRenderFrameHostType::CURRENT); |
| ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); |
| ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); |
| } |
| |
| class VisualPropertiesSynchronization : public NavigationBrowserTest { |
| public: |
| VisualPropertiesSynchronization() { |
| // The deferral of the RFH prevents the potential race condition that this |
| // regression test is attempting to check. |
| feature_list_.InitAndDisableFeature(features::kDeferSpeculativeRFHCreation); |
| |
| auto* command_line = base::CommandLine::ForCurrentProcess(); |
| |
| // This test requires cross-process iframes. |
| command_line->AppendSwitch(switches::kSitePerProcess); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Regression test for https://crbug.com/352093463. |
| // Verify that when a cross-origin subframe initiates a top-level navigation to |
| // a same-origin (with respect to itself) URL, that the visual properties |
| // are invalidated correctly. |
| IN_PROC_BROWSER_TEST_F(VisualPropertiesSynchronization, |
| RemoteToLocalTransition) { |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b_top_level(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| GURL url_b_iframe(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| EXPECT_TRUE(ExecJs(shell(), |
| "let iframe = document.createElement('iframe');" |
| "iframe.id = 'iframe_id';" |
| "iframe.src = 'about:blank';" |
| "iframe.style = 'width: 0px; height: 0px;';" |
| "document.body.appendChild(iframe);")); |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| |
| // Start a navigation of the top-level document to b.com. Before we leave |
| // the original a.com, load an iframe to b.com which will be hosted in the |
| // same b.com process. |
| content::TestNavigationManager top_level_navigation(web_contents, |
| url_b_top_level); |
| EXPECT_TRUE(ExecJs( |
| shell(), JsReplace("window.location.replace($1);", url_b_top_level))); |
| |
| // Don't proceed with the top-level navigation (wait while we complete our |
| // iframe nav to the same origin to commit). |
| EXPECT_TRUE(top_level_navigation.WaitForLoaderStart()); |
| EXPECT_FALSE(top_level_navigation.was_committed()); |
| |
| // Navigate the iframe to a 'b.com' URL, making a remote frame within the |
| // a.com page. |
| FrameTreeNode* root = |
| FrameTreeNode::From(web_contents->GetPrimaryMainFrame()); |
| CHECK(root->child_count() > 0u); |
| FrameTreeNode* iframe = root->child_at(0); |
| TestFrameNavigationObserver iframe_load_observer_first( |
| iframe->current_frame_host()); |
| ASSERT_TRUE( |
| BeginNavigateIframeToURL(web_contents, "iframe_id", url_b_iframe)); |
| iframe_load_observer_first.WaitForCommit(); |
| EXPECT_EQ(url_b_iframe, iframe_load_observer_first.last_committed_url()); |
| EXPECT_TRUE(iframe_load_observer_first.last_navigation_succeeded()); |
| |
| // Confirm the cross-process iframe process is not (yet) the main frame's |
| // process. |
| RenderFrameHostImpl* subframe_rfh = nullptr; |
| subframe_rfh = static_cast<RenderFrameHostImpl*>( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(subframe_rfh); |
| RenderProcessHost* cross_origin_iframe_process = subframe_rfh->GetProcess(); |
| ASSERT_NE(cross_origin_iframe_process, |
| web_contents->GetPrimaryMainFrame()->GetProcess()); |
| |
| // Allow the top-level navigation to proceed. |
| EXPECT_FALSE(top_level_navigation.was_committed()); |
| EXPECT_TRUE(top_level_navigation.WaitForNavigationFinished()); |
| EXPECT_TRUE(top_level_navigation.was_committed()); |
| |
| // The main frame should now be using the same 'b.com' renderer. |
| ASSERT_EQ(cross_origin_iframe_process, |
| web_contents->GetPrimaryMainFrame()->GetProcess()); |
| |
| // Verify that the browser side's VisualProperties' visible viewport size is |
| // non-zero. |
| root = FrameTreeNode::From(web_contents->GetPrimaryMainFrame()); |
| auto* root_rwh = root->current_frame_host()->GetRenderWidgetHost(); |
| std::optional<blink::VisualProperties> visual_properties = |
| root_rwh->LastComputedVisualProperties(); |
| EXPECT_TRUE(visual_properties); |
| EXPECT_NE(gfx::Size(0, 0), |
| visual_properties->visible_viewport_size_device_px); |
| |
| // Ensure a frame has been produced. |
| ASSERT_TRUE( |
| EvalJsAfterLifecycleUpdate(web_contents->GetPrimaryMainFrame(), "", "") |
| .error.empty()); |
| |
| // Verify the renderer received the correct size for the viewport. |
| EXPECT_GT(EvalJs(web_contents->GetPrimaryMainFrame(), "window.innerWidth;") |
| .ExtractDouble(), |
| 0); |
| EXPECT_GT(EvalJs(web_contents->GetPrimaryMainFrame(), "window.innerHeight;") |
| .ExtractDouble(), |
| 0); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| class AndroidPrewarmSpareRendererTest |
| : public NavigationBrowserTest, |
| public ::testing::WithParamInterface<std::tuple<std::string, bool>> { |
| public: |
| AndroidPrewarmSpareRendererTest() { |
| std::map<std::string, std::string> parameters = { |
| {"spare_renderer_creation_timing", std::get<0>(GetParam())}, |
| {"spare_renderer_timeout_seconds", |
| std::get<1>(GetParam()) ? "10" : "-1"}, |
| }; |
| feature_list_.InitWithFeaturesAndParameters( |
| /*enabled_features=*/{{features::kAndroidWarmUpSpareRendererWithTimeout, |
| parameters}}, |
| /*disabled_features=*/{{features::kSpareRendererForSitePerProcess}}); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| // Enable site per process so that the navigation will take |
| // the spare process. |
| command_line->AppendSwitch(switches::kSitePerProcess); |
| } |
| |
| bool SpareRendererHasTimeout() { return std::get<1>(GetParam()); } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| AndroidPrewarmSpareRendererTest, |
| testing::Combine( |
| testing::Values( |
| features::kAndroidSpareRendererCreationAfterLoading, |
| features::kAndroidSpareRendererCreationAfterFirstPaint, |
| features::kAndroidSpareRendererCreationDelayedDuringLoading), |
| testing::Bool())); |
| |
| IN_PROC_BROWSER_TEST_P(AndroidPrewarmSpareRendererTest, ReuseSpareRenderer) { |
| auto& spare_manager = SpareRenderProcessHostManagerImpl::Get(); |
| spare_manager.CleanupSparesForTesting(); |
| SpareRenderProcessHostStartedObserver spare_started_observer; |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| RenderProcessHost* created_process = |
| spare_started_observer.WaitForSpareRenderProcessStarted(); |
| ASSERT_TRUE(!!created_process); |
| ASSERT_THAT(spare_manager.GetSpares(), testing::ElementsAre(created_process)); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("b.com", "/title1.html"))); |
| ASSERT_EQ(web_contents->GetSiteInstance()->GetProcess(), created_process); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(AndroidPrewarmSpareRendererTest, RendererTimeout) { |
| scoped_refptr<base::TestMockTimeTaskRunner> task_runner = |
| new base::TestMockTimeTaskRunner(); |
| auto& spare_manager = SpareRenderProcessHostManagerImpl::Get(); |
| spare_manager.SetDeferTimerTaskRunnerForTesting(task_runner); |
| const base::TimeDelta kTimeout = base::Seconds(10); |
| |
| spare_manager.CleanupSparesForTesting(); |
| SpareRenderProcessHostStartedObserver spare_started_observer; |
| ASSERT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| RenderProcessHost* created_process = |
| spare_started_observer.WaitForSpareRenderProcessStarted(); |
| ASSERT_TRUE(!!created_process); |
| ASSERT_THAT(spare_manager.GetSpares(), testing::ElementsAre(created_process)); |
| |
| if (!SpareRendererHasTimeout()) { |
| // Warming up a spare renderer with a timeout shall not override |
| // a spare renderer without a timeout. |
| spare_manager.WarmupSpare(shell()->web_contents()->GetBrowserContext(), |
| kTimeout); |
| } |
| task_runner->FastForwardBy(kTimeout); |
| base::RunLoop().RunUntilIdle(); |
| if (SpareRendererHasTimeout()) { |
| EXPECT_TRUE(spare_manager.GetSpares().empty()); |
| } else { |
| ASSERT_THAT(spare_manager.GetSpares(), |
| testing::ElementsAre(created_process)); |
| } |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| class HstsUpgradeBrowserTest : public NavigationBrowserTest { |
| public: |
| HstsUpgradeBrowserTest() { |
| feature_list_.InitAndEnableFeature( |
| net::features::kHstsTopLevelNavigationsOnly); |
| } |
| |
| void SetUpOnMainThread() override { |
| NavigationBrowserTest::SetUpOnMainThread(); |
| ASSERT_TRUE(embedded_https_test_server().Start()); |
| } |
| |
| content::test::FencedFrameTestHelper& fenced_frame_test_helper() { |
| return fenced_frame_test_helper_; |
| } |
| |
| private: |
| content::test::FencedFrameTestHelper fenced_frame_test_helper_; |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Tests that when HstsTopLevelNavigationsOnly is enabled only top-level |
| // navigations will be upgraded by HSTS. |
| IN_PROC_BROWSER_TEST_F(HstsUpgradeBrowserTest, UpgradeTopLevelOnly) { |
| // Url that loads a page with the HSTS url, http://b.com, as an iframe under |
| // an http://a.com main frame. |
| GURL hsts_url_in_iframe_http = embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)"); |
| // The expected url of the HSTS url, http://b.com, iframe. |
| GURL url_of_hsts_frame_http = embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b()"); |
| |
| { |
| // Add hostname to the TransportSecurityState. |
| base::Time expiry = base::Time::Now() + base::Days(100); |
| bool include_subdomains = false; |
| auto* network_context = web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetNetworkContext(); |
| base::RunLoop run_loop; |
| network_context->AddHSTS(url_of_hsts_frame_http.host(), expiry, |
| include_subdomains, run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| // Navigate the main frame to the HSTS url, http://b.com. |
| |
| // Note: Because the http and https embedded test servers run on different |
| // (non-default) ports the test will fail if we try to navigate to |
| // `url_of_hsts_frame_http` because HSTS will simply change the scheme to |
| // https, but the port will remain the http server's port. To work around this |
| // we can take an https url, `hsts_url_main_frame_https`, and change its |
| // scheme to http which will then be upgraded by HSTS back to https and will |
| // load correctly. |
| |
| // Url of an https://b.com page. |
| GURL hsts_url_main_frame_https = |
| embedded_https_test_server().GetURL("b.com", "/title1.html"); |
| |
| GURL::Replacements scheme_replacement; |
| scheme_replacement.SetSchemeStr("http"); |
| |
| // The navigation should get upgraded to https://b.com. |
| EXPECT_TRUE(NavigateToURL( |
| web_contents(), |
| /*url=*/hsts_url_main_frame_https.ReplaceComponents(scheme_replacement), |
| /*expected_commit_url=*/hsts_url_main_frame_https)); |
| |
| // Now navigate to an http://a.com page that embeds an http://b.com iframe. |
| EXPECT_TRUE(NavigateToURL(web_contents(), hsts_url_in_iframe_http)); |
| auto* sub_frame = main_frame()->child_at(0); |
| // The http://b.com iframe should not have been upgraded. |
| EXPECT_EQ(url_of_hsts_frame_http, |
| sub_frame->current_frame_host()->GetLastCommittedURL()); |
| |
| // Fenced Frames are treated as top-level frames in many cases, but not for |
| // HSTS upgrades. Requests for fenced frames should not be upgraded. |
| content::RenderFrameHost* fenced_frame = |
| fenced_frame_test_helper().CreateFencedFrame( |
| main_frame()->current_frame_host(), url_of_hsts_frame_http); |
| |
| ASSERT_TRUE(fenced_frame); |
| EXPECT_EQ(url_of_hsts_frame_http, fenced_frame->GetLastCommittedURL()); |
| } |
| |
| } // namespace content |