| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/renderer/render_frame_impl.h" |
| |
| #include <stdint.h> |
| |
| #include <optional> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/check_deref.h" |
| #include "base/command_line.h" |
| #include "base/debug/leak_annotations.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/protected_memory_buildflags.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/unguessable_token.h" |
| #include "build/build_config.h" |
| #include "content/common/features.h" |
| #include "content/common/renderer.mojom.h" |
| #include "content/public/common/bindings_policy.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/extra_mojo_js_features.mojom.h" |
| #include "content/public/renderer/content_renderer_client.h" |
| #include "content/public/test/frame_load_waiter.h" |
| #include "content/public/test/local_frame_host_interceptor.h" |
| #include "content/public/test/policy_container_utils.h" |
| #include "content/public/test/render_view_test.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/renderer/agent_scheduling_group.h" |
| #include "content/renderer/document_state.h" |
| #include "content/renderer/mojo/blink_interface_registry_impl.h" |
| #include "content/renderer/navigation_state.h" |
| #include "content/test/frame_host_test_interface.mojom.h" |
| #include "content/test/test_render_frame.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/navigation/navigation_params.h" |
| #include "third_party/blink/public/common/navigation/navigation_params_mojom_traits.h" |
| #include "third_party/blink/public/common/tokens/tokens.h" |
| #include "third_party/blink/public/mojom/browser_interface_broker.mojom-forward.h" |
| #include "third_party/blink/public/mojom/frame/frame_owner_properties.mojom.h" |
| #include "third_party/blink/public/mojom/frame/frame_replication_state.mojom.h" |
| #include "third_party/blink/public/mojom/frame/tree_scope_type.mojom.h" |
| #include "third_party/blink/public/mojom/frame/viewport_intersection_state.mojom.h" |
| #include "third_party/blink/public/mojom/widget/platform_widget.mojom.h" |
| #include "third_party/blink/public/mojom/widget/record_content_to_visible_time_request.mojom.h" |
| #include "third_party/blink/public/platform/web_runtime_features.h" |
| #include "third_party/blink/public/platform/web_string.h" |
| #include "third_party/blink/public/platform/web_url.h" |
| #include "third_party/blink/public/platform/web_url_request.h" |
| #include "third_party/blink/public/platform/web_v8_value_converter.h" |
| #include "third_party/blink/public/test/test_web_frame_content_dumper.h" |
| #include "third_party/blink/public/web/web_frame_widget.h" |
| #include "third_party/blink/public/web/web_history_item.h" |
| #include "third_party/blink/public/web/web_local_frame.h" |
| #include "third_party/blink/public/web/web_v8_features.h" |
| #include "third_party/blink/public/web/web_view.h" |
| #include "ui/display/screen_info.h" |
| #include "ui/display/screen_infos.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/native_theme/native_theme_features.h" |
| |
| using blink::WebURLRequest; |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr int32_t kSubframeRouteId = 20; |
| constexpr int32_t kSubframeWidgetRouteId = 21; |
| |
| const char kParentFrameHTML[] = "Parent frame <iframe name='frame'></iframe>"; |
| const char kSimpleScriptHtml[] = "<script>var x = 1;</script>"; |
| |
| const char kAutoplayTestOrigin[] = "https://www.google.com"; |
| |
| } // namespace |
| |
| // RenderFrameImplTest creates a RenderFrameImpl that is a child of the |
| // main frame, and has its own RenderWidget. This behaves like an out |
| // of process frame even though it is in the same process as its parent. |
| class RenderFrameImplTest : public RenderViewTest { |
| public: |
| explicit RenderFrameImplTest( |
| RenderFrameImpl::CreateRenderFrameImplFunction hook_function = nullptr) |
| : RenderViewTest(/*hook_render_frame_creation=*/!hook_function) { |
| if (hook_function) { |
| RenderFrameImpl::InstallCreateHook(hook_function); |
| } |
| } |
| ~RenderFrameImplTest() override = default; |
| |
| void SetUp() override { |
| blink::WebRuntimeFeatures::EnableOverlayScrollbars( |
| ui::IsOverlayScrollbarEnabled()); |
| RenderViewTest::SetUp(); |
| EXPECT_TRUE(GetMainRenderFrame()->is_main_frame_); |
| |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| |
| LoadHTML(kParentFrameHTML); |
| LoadChildFrame(); |
| } |
| |
| void LoadChildFrame() { |
| child_frame_token_ = blink::LocalFrameToken(); |
| mojom::CreateFrameWidgetParamsPtr widget_params = |
| mojom::CreateFrameWidgetParams::New(); |
| widget_params->routing_id = kSubframeWidgetRouteId; |
| widget_params->visual_properties.new_size = gfx::Size(100, 100); |
| widget_params->visual_properties.screen_infos = |
| display::ScreenInfos(display::ScreenInfo()); |
| |
| widget_remote_.reset(); |
| mojo::PendingAssociatedReceiver<blink::mojom::Widget> |
| blink_widget_receiver = |
| widget_remote_.BindNewEndpointAndPassDedicatedReceiver(); |
| |
| mojo::AssociatedRemote<blink::mojom::WidgetHost> blink_widget_host; |
| mojo::PendingAssociatedReceiver<blink::mojom::WidgetHost> |
| blink_widget_host_receiver = |
| blink_widget_host.BindNewEndpointAndPassDedicatedReceiver(); |
| |
| widget_params->widget = std::move(blink_widget_receiver); |
| widget_params->widget_host = blink_widget_host.Unbind(); |
| |
| auto frame_replication_state = blink::mojom::FrameReplicationState::New(); |
| frame_replication_state->name = "frame"; |
| frame_replication_state->unique_name = "frame-uniqueName"; |
| |
| auto remote_frame_interfaces = |
| blink::mojom::RemoteFrameInterfacesFromBrowser::New(); |
| mojo::AssociatedRemote<blink::mojom::RemoteFrame> frame; |
| remote_frame_interfaces->frame_receiver = |
| frame.BindNewEndpointAndPassDedicatedReceiver(); |
| |
| mojo::AssociatedRemote<blink::mojom::RemoteFrameHost> frame_host; |
| std::ignore = frame_host.BindNewEndpointAndPassDedicatedReceiver(); |
| remote_frame_interfaces->frame_host = frame_host.Unbind(); |
| |
| auto remote_main_frame_interfaces = |
| blink::mojom::RemoteMainFrameInterfaces::New(); |
| mojo::AssociatedRemote<blink::mojom::RemoteMainFrame> main_frame; |
| remote_main_frame_interfaces->main_frame = |
| main_frame.BindNewEndpointAndPassDedicatedReceiver(); |
| |
| mojo::AssociatedRemote<blink::mojom::RemoteMainFrameHost> main_frame_host; |
| std::ignore = main_frame_host.BindNewEndpointAndPassDedicatedReceiver(); |
| remote_main_frame_interfaces->main_frame_host = main_frame_host.Unbind(); |
| |
| blink::RemoteFrameToken remote_child_token = blink::RemoteFrameToken(); |
| RenderFrameImpl::FromWebFrame( |
| GetMainRenderFrame()->GetWebFrame()->FirstChild()) |
| ->Unload(false, frame_replication_state->Clone(), remote_child_token, |
| std::move(remote_frame_interfaces), |
| std::move(remote_main_frame_interfaces)); |
| MockPolicyContainerHost mock_policy_container_host; |
| RenderFrameImpl::CreateFrame( |
| *agent_scheduling_group_, child_frame_token_, kSubframeRouteId, |
| TestRenderFrame::CreateStubFrameReceiver(), |
| TestRenderFrame::CreateStubBrowserInterfaceBrokerRemote(), |
| TestRenderFrame::CreateStubAssociatedInterfaceProviderRemote(), |
| /*web_view=*/nullptr, |
| /*previous_frame_token=*/std::nullopt, |
| /*opener_frame_token=*/std::nullopt, |
| /*parent_frame_token=*/remote_child_token, |
| /*previous_sibling_frame_token=*/std::nullopt, |
| base::UnguessableToken::Create(), |
| blink::mojom::TreeScopeType::kDocument, |
| std::move(frame_replication_state), std::move(widget_params), |
| blink::mojom::FrameOwnerProperties::New(), |
| /*has_committed_real_load=*/true, blink::DocumentToken(), |
| blink::mojom::PolicyContainer::New( |
| blink::mojom::PolicyContainerPolicies::New(), |
| mock_policy_container_host.BindNewEndpointAndPassDedicatedRemote()), |
| /*is_for_nested_main_frame=*/false); |
| |
| EXPECT_FALSE(child_frame().is_main_frame_); |
| } |
| |
| void TearDown() override { |
| #if defined(LEAK_SANITIZER) |
| // Do this before shutting down V8 in RenderViewTest::TearDown(). |
| // http://crbug.com/328552 |
| __lsan_do_leak_check(); |
| #endif |
| RenderViewTest::TearDown(); |
| } |
| |
| TestRenderFrame* GetMainRenderFrame() { |
| return static_cast<TestRenderFrame*>(RenderViewTest::GetMainRenderFrame()); |
| } |
| |
| TestRenderFrame& child_frame() const { |
| return CHECK_DEREF( |
| static_cast<TestRenderFrame*>(RenderFrameImpl::FromWebFrame( |
| blink::WebLocalFrame::FromFrameToken(child_frame_token_)))); |
| } |
| |
| blink::WebFrameWidget* frame_widget() const { |
| return child_frame().GetLocalRootWebFrameWidget(); |
| } |
| |
| mojo::AssociatedRemote<blink::mojom::Widget>& widget_remote() { |
| return widget_remote_; |
| } |
| |
| static url::Origin GetOriginForFrame(TestRenderFrame* frame) { |
| return url::Origin(frame->GetWebFrame()->GetSecurityOrigin()); |
| } |
| |
| static int32_t AutoplayFlagsForFrame(const TestRenderFrame& frame) { |
| return frame.GetWebView()->AutoplayFlagsForTest(); |
| } |
| |
| private: |
| mojo::AssociatedRemote<blink::mojom::Widget> widget_remote_; |
| blink::LocalFrameToken child_frame_token_; |
| }; |
| |
| class RenderFrameTestObserver : public RenderFrameObserver { |
| public: |
| explicit RenderFrameTestObserver(RenderFrame* render_frame) |
| : RenderFrameObserver(render_frame), |
| visible_(false), |
| last_intersection_rect_(-1, -1, -1, -1) {} |
| |
| ~RenderFrameTestObserver() override {} |
| |
| // RenderFrameObserver implementation. |
| void WasShown() override { visible_ = true; } |
| void WasHidden() override { visible_ = false; } |
| void OnDestruct() override { delete this; } |
| void OnMainFrameIntersectionChanged( |
| const gfx::Rect& intersection_rect) override { |
| last_intersection_rect_ = intersection_rect; |
| } |
| void OnMainFrameViewportRectangleChanged( |
| const gfx::Rect& viewport_rect) override { |
| last_viewport_rect_ = viewport_rect; |
| } |
| |
| bool visible() const { return visible_; } |
| gfx::Rect last_intersection_rect() const { return last_intersection_rect_; } |
| gfx::Rect last_viewport_rect() const { return last_viewport_rect_; } |
| |
| private: |
| bool visible_; |
| gfx::Rect last_intersection_rect_; |
| gfx::Rect last_viewport_rect_; |
| }; |
| |
| // Verify that a frame with a WebRemoteFrame as a parent has its own |
| // RenderWidget. |
| TEST_F(RenderFrameImplTest, SubframeWidget) { |
| EXPECT_TRUE(frame_widget()); |
| |
| RenderFrameImpl* main_frame = GetMainRenderFrame(); |
| blink::WebFrameWidget* main_frame_widget = |
| main_frame->GetLocalRootWebFrameWidget(); |
| EXPECT_NE(frame_widget(), main_frame_widget); |
| } |
| |
| // Verify a subframe RenderWidget properly processes its viewport being |
| // resized. |
| TEST_F(RenderFrameImplTest, FrameResize) { |
| // Make an update where the widget's size and the visible_viewport_size |
| // are not the same. |
| blink::VisualProperties visual_properties; |
| visual_properties.screen_infos = display::ScreenInfos(display::ScreenInfo()); |
| gfx::Size widget_size(400, 200); |
| gfx::Size visible_size(350, 170); |
| visual_properties.new_size = widget_size; |
| visual_properties.compositor_viewport_pixel_rect = gfx::Rect(widget_size); |
| visual_properties.visible_viewport_size = visible_size; |
| |
| blink::WebFrameWidget* main_frame_widget = |
| GetMainRenderFrame()->GetLocalRootWebFrameWidget(); |
| |
| // The main frame's widget will receive the resize message before the |
| // subframe's widget, and it will set the size for the WebView. |
| main_frame_widget->ApplyVisualProperties(visual_properties); |
| // The main frame widget's size is the "widget size", not the visible viewport |
| // size, which is given to blink separately. |
| EXPECT_EQ(gfx::Size(web_view_->MainFrameWidget()->Size()), widget_size); |
| EXPECT_EQ(gfx::SizeF(web_view_->VisualViewportSize()), |
| gfx::SizeF(visible_size)); |
| // The main frame doesn't change other local roots directly. |
| EXPECT_NE(gfx::Size(frame_widget()->Size()), visible_size); |
| |
| // A subframe in the same process does not modify the WebView. |
| frame_widget()->ApplyVisualProperties(visual_properties); |
| EXPECT_EQ(gfx::Size(frame_widget()->Size()), widget_size); |
| |
| // A subframe in another process would use the |visible_viewport_size| as its |
| // size. |
| } |
| |
| // Verify a subframe RenderWidget properly processes a WasShown message. |
| TEST_F(RenderFrameImplTest, FrameWasShown) { |
| RenderFrameTestObserver observer(&child_frame()); |
| |
| widget_remote()->WasShown( |
| /* was_evicted=*/false, |
| blink::mojom::RecordContentToVisibleTimeRequestPtr()); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(frame_widget()->IsHidden()); |
| EXPECT_TRUE(observer.visible()); |
| } |
| |
| namespace { |
| class DownloadURLMockLocalFrameHost : public LocalFrameHostInterceptor { |
| public: |
| explicit DownloadURLMockLocalFrameHost( |
| blink::AssociatedInterfaceProvider* provider) |
| : LocalFrameHostInterceptor(provider) {} |
| |
| MOCK_METHOD3(RunModalAlertDialog, |
| void(const std::u16string& alert_message, |
| bool disable_third_party_subframe_suppresion, |
| RunModalAlertDialogCallback callback)); |
| MOCK_METHOD1(DownloadURL, void(blink::mojom::DownloadURLParamsPtr params)); |
| }; |
| |
| class DownloadURLTestRenderFrame : public TestRenderFrame { |
| public: |
| static RenderFrameImpl* CreateTestRenderFrame( |
| RenderFrameImpl::CreateParams params) { |
| return new DownloadURLTestRenderFrame(std::move(params)); |
| } |
| |
| ~DownloadURLTestRenderFrame() override = default; |
| |
| blink::AssociatedInterfaceProvider* GetRemoteAssociatedInterfaces() override { |
| blink::AssociatedInterfaceProvider* associated_interface_provider = |
| RenderFrameImpl::GetRemoteAssociatedInterfaces(); |
| |
| // Attach our fake local frame host at the very first call to |
| // GetRemoteAssociatedInterfaces. |
| if (!local_frame_host_) { |
| local_frame_host_ = std::make_unique<DownloadURLMockLocalFrameHost>( |
| associated_interface_provider); |
| } |
| return associated_interface_provider; |
| } |
| |
| DownloadURLMockLocalFrameHost* download_url_mock_local_frame_host() { |
| return local_frame_host_.get(); |
| } |
| |
| private: |
| explicit DownloadURLTestRenderFrame(RenderFrameImpl::CreateParams params) |
| : TestRenderFrame(std::move(params)) {} |
| |
| std::unique_ptr<DownloadURLMockLocalFrameHost> local_frame_host_; |
| }; |
| } // namespace |
| |
| class RenderViewImplDownloadURLTest : public RenderFrameImplTest { |
| public: |
| RenderViewImplDownloadURLTest() |
| : RenderFrameImplTest( |
| &DownloadURLTestRenderFrame::CreateTestRenderFrame) {} |
| |
| DownloadURLMockLocalFrameHost* download_url_mock_local_frame_host() { |
| return static_cast<DownloadURLTestRenderFrame*>(&child_frame()) |
| ->download_url_mock_local_frame_host(); |
| } |
| }; |
| |
| // Tests that url download are throttled when reaching the limit. |
| TEST_F(RenderViewImplDownloadURLTest, DownloadUrlLimit) { |
| WebURLRequest request; |
| request.SetUrl(GURL("http://test/test.pdf")); |
| request.SetRequestorOrigin( |
| blink::WebSecurityOrigin::Create(GURL("http://test"))); |
| |
| EXPECT_CALL(*download_url_mock_local_frame_host(), DownloadURL(testing::_)) |
| .Times(10); |
| for (int i = 0; i < 10; ++i) { |
| child_frame().GetWebFrame()->DownloadURL( |
| request, network::mojom::RedirectMode::kManual, mojo::NullRemote()); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| EXPECT_CALL(*download_url_mock_local_frame_host(), DownloadURL(testing::_)) |
| .Times(0); |
| child_frame().GetWebFrame()->DownloadURL( |
| request, network::mojom::RedirectMode::kManual, mojo::NullRemote()); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Regression test for crbug.com/692557. It shouldn't crash if we inititate a |
| // text finding, and then delete the frame immediately before the text finding |
| // returns any text match. |
| TEST_F(RenderFrameImplTest, NoCrashWhenDeletingFrameDuringFind) { |
| child_frame().GetWebFrame()->FindForTesting( |
| 1, "foo", true /* match_case */, true /* forward */, |
| true /* new_session */, true /* force */, false /* wrap_within_frame */, |
| false /* async */); |
| |
| static_cast<mojom::Frame*>(&child_frame()) |
| ->Delete(mojom::FrameDeleteIntention::kNotMainFrame); |
| } |
| |
| TEST_F(RenderFrameImplTest, NoCrashOnReceiveTitleWhenNavigatingToJavascript) { |
| LoadHTML( |
| "<html>" |
| " <everything id='outer'>" |
| " <title><empty></title>" |
| " <body>" |
| " <iframe id='iframe'></iframe>" |
| " <script>" |
| " iframe.contentWindow.onunload = () => {" |
| " document.adoptNode(outer);" |
| " };" |
| " window.location = 'javascript:\"PASS.\"';" |
| " </script>" |
| " </body>" |
| " </everything>" |
| "</html> "); |
| } |
| |
| TEST_F(RenderFrameImplTest, AutoplayFlags) { |
| // Add autoplay flags to the page. |
| GetMainRenderFrame()->AddAutoplayFlags( |
| url::Origin::Create(GURL(kAutoplayTestOrigin)), |
| blink::mojom::kAutoplayFlagHighMediaEngagement); |
| |
| // Navigate the top frame. |
| LoadHTMLWithUrlOverride(kParentFrameHTML, kAutoplayTestOrigin); |
| |
| // Check the flags have been set correctly. |
| EXPECT_EQ(blink::mojom::kAutoplayFlagHighMediaEngagement, |
| AutoplayFlagsForFrame(*GetMainRenderFrame())); |
| |
| // Navigate the child frame. |
| LoadChildFrame(); |
| |
| // Check the flags are set on both frames. |
| EXPECT_EQ(blink::mojom::kAutoplayFlagHighMediaEngagement, |
| AutoplayFlagsForFrame(*GetMainRenderFrame())); |
| EXPECT_EQ(blink::mojom::kAutoplayFlagHighMediaEngagement, |
| AutoplayFlagsForFrame(child_frame())); |
| |
| // Navigate the top frame. |
| LoadHTMLWithUrlOverride(kParentFrameHTML, "https://www.example.com"); |
| LoadChildFrame(); |
| |
| // Check the flags have been cleared. |
| EXPECT_EQ(blink::mojom::kAutoplayFlagNone, |
| AutoplayFlagsForFrame(*GetMainRenderFrame())); |
| EXPECT_EQ(blink::mojom::kAutoplayFlagNone, |
| AutoplayFlagsForFrame(child_frame())); |
| } |
| |
| TEST_F(RenderFrameImplTest, AutoplayFlags_WrongOrigin) { |
| // Add autoplay flags to the page. |
| GetMainRenderFrame()->AddAutoplayFlags( |
| url::Origin(), blink::mojom::kAutoplayFlagHighMediaEngagement); |
| |
| // Navigate the top frame. |
| LoadHTMLWithUrlOverride(kParentFrameHTML, kAutoplayTestOrigin); |
| |
| // Check the flags have been not been set. |
| EXPECT_EQ(blink::mojom::kAutoplayFlagNone, |
| AutoplayFlagsForFrame(*GetMainRenderFrame())); |
| } |
| |
| TEST_F(RenderFrameImplTest, FileUrlPathAlias) { |
| const struct { |
| const char* original; |
| const char* transformed; |
| } kTestCases[] = { |
| {"file:///alias", "file:///replacement"}, |
| {"file:///alias/path/to/file", "file:///replacement/path/to/file"}, |
| {"file://alias/path/to/file", "file://alias/path/to/file"}, |
| {"file:///notalias/path/to/file", "file:///notalias/path/to/file"}, |
| {"file:///root/alias/path/to/file", "file:///root/alias/path/to/file"}, |
| {"file:///", "file:///"}, |
| }; |
| base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( |
| switches::kFileUrlPathAlias, "/alias=/replacement"); |
| |
| for (const auto& test_case : kTestCases) { |
| WebURLRequest request; |
| request.SetUrl(GURL(test_case.original)); |
| GetMainRenderFrame()->WillSendRequest( |
| request, blink::WebLocalFrameClient::ForRedirect(false)); |
| EXPECT_EQ(test_case.transformed, request.Url().GetString().Utf8()); |
| } |
| } |
| |
| TEST_F(RenderFrameImplTest, MainFrameIntersectionRecorded) { |
| RenderFrameTestObserver observer(&child_frame()); |
| gfx::Rect mainframe_intersection(0, 0, 200, 140); |
| child_frame().OnMainFrameIntersectionChanged(mainframe_intersection); |
| // Setting a new frame intersection in a local frame triggers the render frame |
| // observer call. |
| EXPECT_EQ(observer.last_intersection_rect(), mainframe_intersection); |
| } |
| |
| TEST_F(RenderFrameImplTest, MainFrameViewportRectRecorded) { |
| RenderFrameTestObserver observer(GetMainRenderFrame()); |
| gfx::Rect mainframe_viewport(0, 0, 200, 140); |
| GetMainRenderFrame()->OnMainFrameViewportRectangleChanged(mainframe_viewport); |
| EXPECT_EQ(observer.last_viewport_rect(), mainframe_viewport); |
| |
| // After a navigation, the notification of `mainframe_viewport` should be |
| // propagated to `RenderFrameTestObserver` again for the new document. |
| LoadHTML(kParentFrameHTML); |
| RenderFrameTestObserver observer2(GetMainRenderFrame()); |
| GetMainRenderFrame()->OnMainFrameViewportRectangleChanged(mainframe_viewport); |
| EXPECT_EQ(observer2.last_viewport_rect(), mainframe_viewport); |
| } |
| |
| // Used to annotate the source of an interface request. |
| struct SourceAnnotation { |
| // The URL of the active document in the frame, at the time the interface was |
| // requested by the RenderFrame. |
| GURL document_url; |
| |
| // The RenderFrameObserver event in response to which the interface is |
| // requested by the RenderFrame. |
| std::string render_frame_event; |
| |
| bool operator==(const SourceAnnotation& rhs) const { |
| return document_url == rhs.document_url && |
| render_frame_event == rhs.render_frame_event; |
| } |
| bool operator!=(const SourceAnnotation& rhs) const { return !(*this == rhs); } |
| }; |
| |
| std::ostream& operator<<(std::ostream& out, const SourceAnnotation& s) { |
| out << s.document_url.possibly_invalid_spec() << " : " |
| << s.render_frame_event; |
| return out; |
| } |
| |
| // RenderFrameRemoteInterfacesTest ------------------------------------ |
| |
| namespace { |
| |
| constexpr char kTestFirstURL[] = "http://foo.com/1"; |
| constexpr char kTestSecondURL[] = "http://foo.com/2"; |
| // constexpr char kTestCrossOriginURL[] = "http://bar.com/"; |
| constexpr char kAboutBlankURL[] = "about:blank"; |
| |
| constexpr char kFrameEventDidCreateNewFrame[] = "did-create-new-frame"; |
| constexpr char kFrameEventDidCreateNewDocument[] = "did-create-new-document"; |
| constexpr char kFrameEventDidCreateDocumentElement[] = |
| "did-create-document-element"; |
| constexpr char kFrameEventReadyToCommitNavigation[] = |
| "ready-to-commit-navigation"; |
| constexpr char kFrameEventDidCommitProvisionalLoad[] = |
| "did-commit-provisional-load"; |
| constexpr char kFrameEventDidCommitSameDocumentLoad[] = |
| "did-commit-same-document-load"; |
| constexpr char kFrameEventAfterCommit[] = "after-commit"; |
| |
| constexpr char kNoDocumentMarkerURL[] = "data:,No document."; |
| |
| class TestSimpleBrowserInterfaceBrokerImpl |
| : public blink::mojom::BrowserInterfaceBroker { |
| public: |
| using BinderCallback = |
| base::RepeatingCallback<void(mojo::ScopedMessagePipeHandle)>; |
| |
| // Incoming interface requests for |interface_name| will invoke |binder|. |
| // Everything else is ignored. |
| TestSimpleBrowserInterfaceBrokerImpl(const std::string& interface_name, |
| BinderCallback binder_callback) |
| : receiver_(this), |
| interface_name_(interface_name), |
| binder_callback_(binder_callback) {} |
| |
| TestSimpleBrowserInterfaceBrokerImpl( |
| const TestSimpleBrowserInterfaceBrokerImpl&) = delete; |
| TestSimpleBrowserInterfaceBrokerImpl& operator=( |
| const TestSimpleBrowserInterfaceBrokerImpl&) = delete; |
| |
| void BindAndFlush( |
| mojo::PendingReceiver<blink::mojom::BrowserInterfaceBroker> receiver) { |
| ASSERT_FALSE(receiver_.is_bound()); |
| receiver_.Bind(std::move(receiver)); |
| receiver_.FlushForTesting(); |
| } |
| |
| private: |
| // blink::mojom::BrowserInterfaceBroker: |
| void GetInterface(mojo::GenericPendingReceiver receiver) override { |
| if (receiver.interface_name().value() == interface_name_) { |
| binder_callback_.Run(receiver.PassPipe()); |
| } |
| } |
| |
| mojo::Receiver<blink::mojom::BrowserInterfaceBroker> receiver_; |
| |
| std::string interface_name_; |
| BinderCallback binder_callback_; |
| }; |
| |
| class FrameHostTestInterfaceImpl : public mojom::FrameHostTestInterface { |
| public: |
| FrameHostTestInterfaceImpl() = default; |
| |
| FrameHostTestInterfaceImpl(const FrameHostTestInterfaceImpl&) = delete; |
| FrameHostTestInterfaceImpl& operator=(const FrameHostTestInterfaceImpl&) = |
| delete; |
| |
| ~FrameHostTestInterfaceImpl() override {} |
| |
| void BindAndFlush( |
| mojo::PendingReceiver<mojom::FrameHostTestInterface> receiver) { |
| receiver_.Bind(std::move(receiver)); |
| receiver_.WaitForIncomingCall(); |
| } |
| |
| const std::optional<SourceAnnotation>& ping_source() const { |
| return ping_source_; |
| } |
| |
| protected: |
| void Ping(const GURL& url, const std::string& event) override { |
| ping_source_ = SourceAnnotation{url, event}; |
| } |
| |
| private: |
| mojo::Receiver<mojom::FrameHostTestInterface> receiver_{this}; |
| std::optional<SourceAnnotation> ping_source_; |
| }; |
| |
| // RenderFrameObserver that issues FrameHostTestInterface interface requests |
| // through the RenderFrame's |remote_interfaces_| in response to observing |
| // important milestones in a frame's lifecycle. |
| class FrameHostTestInterfaceRequestIssuer : public RenderFrameObserver { |
| public: |
| explicit FrameHostTestInterfaceRequestIssuer(RenderFrame* render_frame) |
| : RenderFrameObserver(render_frame) {} |
| |
| FrameHostTestInterfaceRequestIssuer( |
| const FrameHostTestInterfaceRequestIssuer&) = delete; |
| FrameHostTestInterfaceRequestIssuer& operator=( |
| const FrameHostTestInterfaceRequestIssuer&) = delete; |
| |
| void RequestTestInterfaceOnFrameEvent(const std::string& event) { |
| mojo::Remote<mojom::FrameHostTestInterface> remote; |
| blink::WebDocument document = render_frame()->GetWebFrame()->GetDocument(); |
| render_frame()->GetBrowserInterfaceBroker()->GetInterface( |
| remote.BindNewPipeAndPassReceiver()); |
| remote->Ping( |
| !document.IsNull() ? GURL(document.Url()) : GURL(kNoDocumentMarkerURL), |
| event); |
| } |
| |
| private: |
| // RenderFrameObserver: |
| void OnDestruct() override {} |
| |
| void DidCreateDocumentElement() override { |
| RequestTestInterfaceOnFrameEvent(kFrameEventDidCreateDocumentElement); |
| } |
| |
| void DidCreateNewDocument() override { |
| RequestTestInterfaceOnFrameEvent(kFrameEventDidCreateNewDocument); |
| } |
| |
| void ReadyToCommitNavigation(blink::WebDocumentLoader* loader) override { |
| RequestTestInterfaceOnFrameEvent(kFrameEventReadyToCommitNavigation); |
| } |
| |
| void DidCommitProvisionalLoad(ui::PageTransition transition) override { |
| RequestTestInterfaceOnFrameEvent(kFrameEventDidCommitProvisionalLoad); |
| } |
| |
| void DidFinishSameDocumentNavigation() override { |
| RequestTestInterfaceOnFrameEvent(kFrameEventDidCommitSameDocumentLoad); |
| } |
| }; |
| |
| // RenderFrameObserver that can be used to wait for the next commit in a frame. |
| class FrameCommitWaiter : public RenderFrameObserver { |
| public: |
| explicit FrameCommitWaiter(RenderFrame* render_frame) |
| : RenderFrameObserver(render_frame) {} |
| |
| FrameCommitWaiter(const FrameCommitWaiter&) = delete; |
| FrameCommitWaiter& operator=(const FrameCommitWaiter&) = delete; |
| |
| void Wait() { |
| if (did_commit_) { |
| return; |
| } |
| run_loop_.Run(); |
| } |
| |
| private: |
| // RenderFrameObserver: |
| void OnDestruct() override {} |
| |
| void DidCommitProvisionalLoad(ui::PageTransition transition) override { |
| did_commit_ = true; |
| run_loop_.Quit(); |
| } |
| |
| base::RunLoop run_loop_; |
| bool did_commit_ = false; |
| }; |
| |
| // Testing ContentRendererClient implementation that fires the |callback| |
| // whenever a new frame is created. |
| class FrameCreationObservingRendererClient : public ContentRendererClient { |
| public: |
| using FrameCreatedCallback = base::RepeatingCallback<void(TestRenderFrame*)>; |
| |
| FrameCreationObservingRendererClient() {} |
| |
| FrameCreationObservingRendererClient( |
| const FrameCreationObservingRendererClient&) = delete; |
| FrameCreationObservingRendererClient& operator=( |
| const FrameCreationObservingRendererClient&) = delete; |
| |
| ~FrameCreationObservingRendererClient() override {} |
| |
| void set_callback(FrameCreatedCallback callback) { |
| callback_ = std::move(callback); |
| } |
| |
| void reset_callback() { callback_.Reset(); } |
| |
| protected: |
| void RenderFrameCreated(RenderFrame* render_frame) override { |
| ContentRendererClient::RenderFrameCreated(render_frame); |
| if (callback_) { |
| callback_.Run(static_cast<TestRenderFrame*>(render_frame)); |
| } |
| } |
| |
| private: |
| FrameCreatedCallback callback_; |
| }; |
| |
| // Expects observing the creation of a new frame, and creates an instance of |
| // FrameHostTestInterfaceRequestIssuerRenderFrame for that new frame to exercise |
| // its RemoteInterfaceProvider interface. |
| class ScopedNewFrameInterfaceProviderExerciser { |
| public: |
| explicit ScopedNewFrameInterfaceProviderExerciser( |
| FrameCreationObservingRendererClient* frame_creation_observer, |
| const std::optional<std::string>& html_override_for_first_load = |
| std::nullopt) |
| : frame_creation_observer_(frame_creation_observer), |
| html_override_for_first_load_(html_override_for_first_load) { |
| frame_creation_observer_->set_callback(base::BindRepeating( |
| &ScopedNewFrameInterfaceProviderExerciser::OnFrameCreated, |
| base::Unretained(this))); |
| } |
| |
| ScopedNewFrameInterfaceProviderExerciser( |
| const ScopedNewFrameInterfaceProviderExerciser&) = delete; |
| ScopedNewFrameInterfaceProviderExerciser& operator=( |
| const ScopedNewFrameInterfaceProviderExerciser&) = delete; |
| |
| ~ScopedNewFrameInterfaceProviderExerciser() { |
| frame_creation_observer_->reset_callback(); |
| } |
| |
| void ExpectNewFrameAndWaitForLoad(const GURL& expected_loaded_url) { |
| ASSERT_NE(nullptr, frame_); |
| frame_commit_waiter_->Wait(); |
| |
| ASSERT_FALSE(frame_->GetWebFrame()->GetCurrentHistoryItem().IsNull()); |
| ASSERT_FALSE(frame_->GetWebFrame()->GetDocument().IsNull()); |
| EXPECT_EQ(expected_loaded_url, |
| GURL(frame_->GetWebFrame()->GetDocument().Url())); |
| |
| browser_interface_broker_receiver_for_first_document_ = |
| frame_->TakeLastBrowserInterfaceBrokerReceiver(); |
| } |
| |
| mojo::PendingReceiver<blink::mojom::BrowserInterfaceBroker> |
| browser_interface_broker_receiver_for_initial_empty_document() { |
| return std::move( |
| browser_interface_broker_receiver_for_initial_empty_document_); |
| } |
| |
| mojo::PendingReceiver<blink::mojom::BrowserInterfaceBroker> |
| browser_interface_broker_receiver_for_first_document() { |
| return std::move(browser_interface_broker_receiver_for_first_document_); |
| } |
| |
| private: |
| void OnFrameCreated(TestRenderFrame* frame) { |
| ASSERT_EQ(nullptr, frame_.get()); |
| frame_ = frame; |
| frame_commit_waiter_.emplace(frame); |
| |
| if (html_override_for_first_load_.has_value()) { |
| frame_->SetHTMLOverrideForNextNavigation( |
| std::move(html_override_for_first_load_).value()); |
| } |
| |
| // The FrameHostTestInterfaceRequestIssuer needs to stay alive even after |
| // this method returns, so that it continues to observe RenderFrame |
| // lifecycle events and request test interfaces in response. |
| test_request_issuer_.emplace(frame); |
| test_request_issuer_->RequestTestInterfaceOnFrameEvent( |
| kFrameEventDidCreateNewFrame); |
| |
| browser_interface_broker_receiver_for_initial_empty_document_ = |
| frame_->TakeLastBrowserInterfaceBrokerReceiver(); |
| EXPECT_TRUE(frame->GetWebFrame()->GetCurrentHistoryItem().IsNull()); |
| } |
| |
| raw_ptr<FrameCreationObservingRendererClient> frame_creation_observer_; |
| raw_ptr<TestRenderFrame> frame_ = nullptr; |
| std::optional<std::string> html_override_for_first_load_; |
| GURL first_committed_url_; |
| |
| std::optional<FrameCommitWaiter> frame_commit_waiter_; |
| std::optional<FrameHostTestInterfaceRequestIssuer> test_request_issuer_; |
| |
| mojo::PendingReceiver<blink::mojom::BrowserInterfaceBroker> |
| browser_interface_broker_receiver_for_initial_empty_document_; |
| mojo::PendingReceiver<blink::mojom::BrowserInterfaceBroker> |
| browser_interface_broker_receiver_for_first_document_; |
| }; |
| |
| // Extracts all interface receivers for FrameHostTestInterface pending on the |
| // specified |browser_interface_broker_receiver|, and returns a list of the |
| // source annotations that are provided in the pending Ping() call for each of |
| // these FrameHostTestInterface receivers. |
| void ExpectPendingInterfaceReceiversFromSources( |
| mojo::PendingReceiver<blink::mojom::BrowserInterfaceBroker> |
| browser_interface_broker_receiver, |
| std::vector<SourceAnnotation> expected_sources) { |
| std::vector<SourceAnnotation> sources; |
| ASSERT_TRUE(browser_interface_broker_receiver.is_valid()); |
| |
| std::vector<SourceAnnotation> browser_interface_broker_sources; |
| ASSERT_TRUE(browser_interface_broker_receiver.is_valid()); |
| TestSimpleBrowserInterfaceBrokerImpl browser_broker( |
| mojom::FrameHostTestInterface::Name_, |
| base::BindLambdaForTesting([&browser_interface_broker_sources]( |
| mojo::ScopedMessagePipeHandle handle) { |
| FrameHostTestInterfaceImpl impl; |
| impl.BindAndFlush(mojo::PendingReceiver<mojom::FrameHostTestInterface>( |
| std::move(handle))); |
| ASSERT_TRUE(impl.ping_source().has_value()); |
| browser_interface_broker_sources.push_back(impl.ping_source().value()); |
| })); |
| browser_broker.BindAndFlush(std::move(browser_interface_broker_receiver)); |
| EXPECT_THAT(browser_interface_broker_sources, |
| ::testing::ElementsAreArray(expected_sources)); |
| } |
| |
| } // namespace |
| |
| class RenderFrameRemoteInterfacesTest : public RenderViewTest { |
| public: |
| RenderFrameRemoteInterfacesTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| features::kAllowContentInitiatedDataUrlNavigations); |
| blink::WebRuntimeFeatures::EnableFeatureFromString( |
| "AllowContentInitiatedDataUrlNavigations", true); |
| } |
| |
| RenderFrameRemoteInterfacesTest(const RenderFrameRemoteInterfacesTest&) = |
| delete; |
| RenderFrameRemoteInterfacesTest& operator=( |
| const RenderFrameRemoteInterfacesTest&) = delete; |
| |
| ~RenderFrameRemoteInterfacesTest() override { |
| blink::WebRuntimeFeatures::EnableFeatureFromString( |
| "AllowContentInitiatedDataUrlNavigations", false); |
| } |
| |
| protected: |
| void SetUp() override { |
| RenderViewTest::SetUp(); |
| LoadHTML("Nothing to see here."); |
| } |
| |
| void TearDown() override { |
| #if defined(LEAK_SANITIZER) |
| // Do this before shutting down V8 in RenderViewTest::TearDown(). |
| // http://crbug.com/328552 |
| __lsan_do_leak_check(); |
| #endif |
| RenderViewTest::TearDown(); |
| } |
| |
| FrameCreationObservingRendererClient* frame_creation_observer() { |
| DCHECK(frame_creation_observer_); |
| return frame_creation_observer_; |
| } |
| |
| TestRenderFrame* GetMainRenderFrame() { |
| return static_cast<TestRenderFrame*>(RenderViewTest::GetMainRenderFrame()); |
| } |
| |
| ContentRendererClient* CreateContentRendererClient() override { |
| frame_creation_observer_ = new FrameCreationObservingRendererClient(); |
| return frame_creation_observer_; |
| } |
| |
| private: |
| // Owned by RenderViewTest. |
| raw_ptr<FrameCreationObservingRendererClient> frame_creation_observer_ = |
| nullptr; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Expect that |remote_interfaces_| is bound before the first committed load in |
| // a child frame, and then re-bound on the first commit. |
| TEST_F(RenderFrameRemoteInterfacesTest, ChildFrameAtFirstCommittedLoad) { |
| ScopedNewFrameInterfaceProviderExerciser child_frame_exerciser( |
| frame_creation_observer()); |
| const std::string html = base::StringPrintf("<iframe src=\"%s\"></iframe>", |
| "data:text/html,Child"); |
| LoadHTMLWithUrlOverride(html.c_str(), kTestSecondURL); |
| |
| const GURL child_frame_url("data:text/html,Child"); |
| ASSERT_NO_FATAL_FAILURE( |
| child_frame_exerciser.ExpectNewFrameAndWaitForLoad(child_frame_url)); |
| |
| // TODO(crbug.com/40553427): It is unfortunate how many internal |
| // details of frame/document creation this encodes. Need to decouple. |
| const GURL initial_empty_url(kAboutBlankURL); |
| ExpectPendingInterfaceReceiversFromSources( |
| child_frame_exerciser |
| .browser_interface_broker_receiver_for_initial_empty_document(), |
| {{initial_empty_url, kFrameEventDidCreateNewFrame}, |
| {child_frame_url, kFrameEventReadyToCommitNavigation}, |
| // TODO(crbug.com/40444754): It seems strange that the new |
| // document is created and DidCreateNewDocument is invoked *before* the |
| // provisional load would have even committed. |
| {child_frame_url, kFrameEventDidCreateNewDocument}}); |
| ExpectPendingInterfaceReceiversFromSources( |
| child_frame_exerciser |
| .browser_interface_broker_receiver_for_first_document(), |
| {{child_frame_url, kFrameEventDidCommitProvisionalLoad}, |
| {child_frame_url, kFrameEventDidCreateDocumentElement}}); |
| } |
| |
| // Expect that |remote_interfaces_| is bound before the first committed load in |
| // the main frame of an opened window, and then re-bound on the first commit. |
| TEST_F(RenderFrameRemoteInterfacesTest, |
| MainFrameOfOpenedWindowAtFirstCommittedLoad) { |
| const GURL new_window_url("data:text/html,NewWindow"); |
| ScopedNewFrameInterfaceProviderExerciser main_frame_exerciser( |
| frame_creation_observer(), std::string("foo")); |
| const std::string html = |
| base::StringPrintf("<script>window.open(\"%s\", \"_blank\")</script>", |
| "data:text/html,NewWindow"); |
| LoadHTMLWithUrlOverride(html.c_str(), kTestSecondURL); |
| ASSERT_NO_FATAL_FAILURE( |
| main_frame_exerciser.ExpectNewFrameAndWaitForLoad(new_window_url)); |
| |
| // The URL of the initial empty document is "" for opened windows, in |
| // contrast to child frames, where it is "about:blank". See |
| // Document::Document and Document::SetURL for more details. |
| // |
| // Furthermore, for main frames, InitializeCoreFrame is invoked first, and |
| // RenderFrameImpl::Initialize is invoked second, in contrast to child |
| // frames where it is vice versa. ContentRendererClient::RenderFrameCreated |
| // is invoked from RenderFrameImpl::Initialize, so we miss the events |
| // related to initial empty document that is created from |
| // InitializeCoreFrame, and there is already a document when |
| // RenderFrameCreated is invoked. |
| // |
| // TODO(crbug.com/40553427): It is unfortunate how many internal |
| // details of frame/document creation this encodes. Need to decouple. |
| const GURL initial_empty_url; |
| ExpectPendingInterfaceReceiversFromSources( |
| main_frame_exerciser |
| .browser_interface_broker_receiver_for_initial_empty_document(), |
| {{initial_empty_url, kFrameEventDidCreateNewFrame}, |
| {new_window_url, kFrameEventReadyToCommitNavigation}, |
| {new_window_url, kFrameEventDidCreateNewDocument}}); |
| ExpectPendingInterfaceReceiversFromSources( |
| main_frame_exerciser |
| .browser_interface_broker_receiver_for_first_document(), |
| {{new_window_url, kFrameEventDidCommitProvisionalLoad}, |
| {new_window_url, kFrameEventDidCreateDocumentElement}}); |
| } |
| |
| // Expect that |remote_interfaces_| is not bound to a new pipe if the first |
| // committed load in the child frame has the same security origin as that of the |
| // initial empty document. |
| // |
| // In this case, the LocalDOMWindow object associated with the initial empty |
| // document will be re-used for the newly committed document. Here, we must |
| // continue using the InterfaceProvider connection created for the initial empty |
| // document to support the following use-case: |
| // 1) Parent frame dynamically injects an <iframe>. |
| // 2) The parent frame calls `child.contentDocument.write(...)` to inject |
| // Javascript that may stash objects on the child frame's global object |
| // (LocalDOMWindow). Internally, these objects may be using Mojo services |
| // exposed by the RenderFrameHost. The InterfaceRequests for these services |
| // might still be en-route to the RemnderFrameHost's InterfaceProvider. |
| // 3) The `child` frame commits the first real load, and it is same-origin. |
| // 4) The global object in the child frame's browsing context is re-used. |
| // 5) Javascript objects stashed on the global object should continue to work. |
| // |
| // TODO(japhet?): javascript: urls are untestable here, because they don't |
| // go through the normal commit pipeline. If we were to give javascript: urls |
| // their own DocumentLoader in blink and model them as a real navigation, we |
| // should add a test case here. |
| // TODO(crbug.com/40519010): when all clients are converted to use |
| // BrowserInterfaceBroker, PendingReceiver<InterfaceProvider>-related code will |
| // be removed. |
| TEST_F(RenderFrameRemoteInterfacesTest, |
| ChildFrameReusingWindowOfInitialDocument) { |
| const GURL main_frame_url(kTestFirstURL); |
| const GURL initial_empty_url(kAboutBlankURL); |
| |
| constexpr const char* kTestCases[] = {kTestSecondURL, kAboutBlankURL}; |
| |
| for (const char* test_case : kTestCases) { |
| SCOPED_TRACE(::testing::Message() << "child_frame_url = " << test_case); |
| |
| // Override the URL for the first navigation in the newly created frame to |
| // |child_frame_url|. |
| const GURL child_frame_url(test_case); |
| ScopedNewFrameInterfaceProviderExerciser child_frame_exerciser( |
| frame_creation_observer(), std::string("foo")); |
| |
| std::string html = "<iframe src='" + child_frame_url.spec() + "'></iframe>"; |
| LoadHTMLWithUrlOverride(html.c_str(), main_frame_url.spec().c_str()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| child_frame_exerciser.ExpectNewFrameAndWaitForLoad(child_frame_url)); |
| |
| ExpectPendingInterfaceReceiversFromSources( |
| child_frame_exerciser |
| .browser_interface_broker_receiver_for_initial_empty_document(), |
| {{initial_empty_url, kFrameEventDidCreateNewFrame}, |
| {child_frame_url, kFrameEventReadyToCommitNavigation}, |
| {child_frame_url, kFrameEventDidCreateNewDocument}, |
| {child_frame_url, kFrameEventDidCommitProvisionalLoad}, |
| {child_frame_url, kFrameEventDidCreateDocumentElement}}); |
| |
| auto browser_interface_broker_receiver = |
| child_frame_exerciser |
| .browser_interface_broker_receiver_for_first_document(); |
| ASSERT_FALSE(browser_interface_broker_receiver.is_valid()); |
| } |
| } |
| |
| // Expect that |remote_interfaces_| is bound to a new pipe on cross-document |
| // navigations. |
| TEST_F(RenderFrameRemoteInterfacesTest, ReplacedOnNonSameDocumentNavigation) { |
| LoadHTMLWithUrlOverride("", kTestFirstURL); |
| |
| auto browser_interface_broker_receiver_for_first_document = |
| GetMainRenderFrame()->TakeLastBrowserInterfaceBrokerReceiver(); |
| |
| FrameHostTestInterfaceRequestIssuer requester(GetMainRenderFrame()); |
| requester.RequestTestInterfaceOnFrameEvent(kFrameEventAfterCommit); |
| |
| LoadHTMLWithUrlOverride("", kTestSecondURL); |
| |
| auto browser_interface_broker_receiver_for_second_document = |
| GetMainRenderFrame()->TakeLastBrowserInterfaceBrokerReceiver(); |
| |
| ASSERT_TRUE(browser_interface_broker_receiver_for_first_document.is_valid()); |
| |
| ExpectPendingInterfaceReceiversFromSources( |
| std::move(browser_interface_broker_receiver_for_first_document), |
| {{GURL(kTestFirstURL), kFrameEventAfterCommit}, |
| {GURL(kTestSecondURL), kFrameEventReadyToCommitNavigation}, |
| {GURL(kTestSecondURL), kFrameEventDidCreateNewDocument}}); |
| |
| ASSERT_TRUE(browser_interface_broker_receiver_for_second_document.is_valid()); |
| |
| ExpectPendingInterfaceReceiversFromSources( |
| std::move(browser_interface_broker_receiver_for_second_document), |
| {{GURL(kTestSecondURL), kFrameEventDidCommitProvisionalLoad}, |
| {GURL(kTestSecondURL), kFrameEventDidCreateDocumentElement}}); |
| } |
| |
| // Expect that |remote_interfaces_| is not bound to a new pipe on same-document |
| // navigations, i.e. the existing InterfaceProvider connection is continued to |
| // be used. |
| TEST_F(RenderFrameRemoteInterfacesTest, ReusedOnSameDocumentNavigation) { |
| LoadHTMLWithUrlOverride("", kTestFirstURL); |
| |
| auto browser_interface_broker_receiver = |
| GetMainRenderFrame()->TakeLastBrowserInterfaceBrokerReceiver(); |
| |
| FrameHostTestInterfaceRequestIssuer requester(GetMainRenderFrame()); |
| OnSameDocumentNavigation(GetMainFrame(), true /* is_new_navigation */); |
| |
| ASSERT_TRUE(browser_interface_broker_receiver.is_valid()); |
| |
| ExpectPendingInterfaceReceiversFromSources( |
| std::move(browser_interface_broker_receiver), |
| {{GURL(kTestFirstURL), kFrameEventDidCommitSameDocumentLoad}}); |
| } |
| |
| TEST_F(RenderFrameImplTest, LastCommittedUrlForUKM) { |
| // Test the case where we have a data url with a base_url. |
| GURL data_url = GURL("data:text/html,"); |
| auto common_params = blink::CreateCommonNavigationParams(); |
| common_params->url = data_url; |
| common_params->navigation_type = |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT; |
| common_params->transition = ui::PAGE_TRANSITION_TYPED; |
| common_params->base_url_for_data_url = GURL("about:blank"); |
| auto commit_params = blink::CreateCommitNavigationParams(); |
| auto waiter = std::make_unique<FrameLoadWaiter>(GetMainRenderFrame()); |
| |
| GetMainRenderFrame()->Navigate(std::move(common_params), |
| std::move(commit_params)); |
| waiter->Wait(); |
| EXPECT_EQ(GURL(GetMainRenderFrame()->LastCommittedUrlForUKM()), data_url); |
| |
| // Test the case where we have an unreachable URL. |
| GURL unreachable_url = GURL("http://www.example.com"); |
| waiter = std::make_unique<FrameLoadWaiter>(GetMainRenderFrame()); |
| GetMainRenderFrame()->LoadHTMLStringForTesting( |
| "test", data_url, "UTF-8", unreachable_url, |
| false /* replace_current_item */); |
| waiter->Wait(); |
| EXPECT_EQ(GURL(GetMainRenderFrame()->LastCommittedUrlForUKM()), |
| unreachable_url); |
| |
| // Test the base case, normal load. |
| GURL override_url = GURL("http://example.com"); |
| waiter = std::make_unique<FrameLoadWaiter>(GetMainRenderFrame()); |
| LoadHTMLWithUrlOverride("Test", "http://example.com"); |
| waiter->Wait(); |
| EXPECT_EQ(GURL(GetMainRenderFrame()->LastCommittedUrlForUKM()), override_url); |
| } |
| |
| // Verify that a frame with a pending update is cancelled when a forced update |
| // is sent. |
| TEST_F(RenderFrameImplTest, SendUpdateCancelsPending) { |
| RenderFrameImpl* main_frame = GetMainRenderFrame(); |
| main_frame->StartDelayedSyncTimer(); |
| EXPECT_TRUE(main_frame->delayed_state_sync_timer_.IsRunning()); |
| main_frame->SendUpdateState(); |
| EXPECT_FALSE(main_frame->delayed_state_sync_timer_.IsRunning()); |
| } |
| |
| namespace { |
| |
| // All content setting tests use the same data url, which contains html which |
| // has different behavior depending on whether script is enabled or disabled. |
| blink::mojom::CommonNavigationParamsPtr |
| GetCommonParamsForContentSettingsTest() { |
| const char kHtml[] = |
| "<html>" |
| "<noscript>JS_DISABLED</noscript>" |
| "<script>document.write('JS_ENABLED');</script>" |
| "</html>"; |
| std::string data_url_contents = "data:text/html,"; |
| data_url_contents += kHtml; |
| |
| auto common_params = blink::CreateCommonNavigationParams(); |
| common_params->url = GURL(data_url_contents); |
| return common_params; |
| } |
| |
| // Dump the layout tree and see whether it contains "text". |
| bool HasText(blink::WebLocalFrame* frame, const std::string& text) { |
| std::string layout_tree = |
| blink::TestWebFrameContentDumper::DumpLayoutTreeAsText( |
| frame, blink::TestWebFrameContentDumper::kLayoutAsTextNormal) |
| .Utf8(); |
| |
| return base::Contains(layout_tree, text); |
| } |
| |
| // Waits for the navigation to finish. |
| void NavigateAndWait(content::TestRenderFrame* frame, |
| blink::mojom::CommonNavigationParamsPtr common_params, |
| blink::mojom::CommitNavigationParamsPtr commit_params, |
| blink::WebView* web_view) { |
| FrameLoadWaiter waiter(frame); |
| frame->Navigate(std::move(common_params), std::move(commit_params)); |
| waiter.Wait(); |
| } |
| |
| class FakeContentSettingsClient : public blink::WebContentSettingsClient { |
| public: |
| explicit FakeContentSettingsClient(content::RenderFrame* render_frame) |
| : render_frame_(render_frame) { |
| render_frame_->GetWebFrame()->SetContentSettingsClient(this); |
| } |
| |
| ~FakeContentSettingsClient() override { |
| render_frame_->GetWebFrame()->SetContentSettingsClient(nullptr); |
| } |
| |
| // blink::WebContentSettingsClient implementation. |
| void DidNotAllowImage() override { ++did_not_allow_image_count_; } |
| void DidNotAllowScript() override { ++did_not_allow_script_count_; } |
| |
| int did_not_allow_image_count_ = 0; |
| int did_not_allow_script_count_ = 0; |
| raw_ptr<content::RenderFrame> render_frame_; |
| }; |
| |
| } // namespace |
| |
| // Checks that when images are blocked, the ContentSettingsAgent receives a |
| // callback. |
| TEST_F(RenderFrameImplTest, ContentSettingsCallbackImageBlocked) { |
| // Create a fake content settings client to track image blocked callbacks. |
| FakeContentSettingsClient fake_content_settings_client(GetMainRenderFrame()); |
| |
| // Navigate to a URL that consists of a red square. |
| std::string data_url_contents = |
| "data:image/" |
| "png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZAQMAAAD+JxcgAAAAA1BMVEX/" |
| "AAAZ4gk3AAAAC0lEQVR4AWMYSAAAAH0AAVFwgb4AAAAASUVORK5CYII="; |
| |
| auto common_params = blink::CreateCommonNavigationParams(); |
| common_params->url = GURL(data_url_contents); |
| common_params->navigation_type = |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT; |
| blink::mojom::CommitNavigationParamsPtr commit_params = |
| blink::CreateCommitNavigationParams(); |
| commit_params->content_settings->allow_image = false; |
| content::TestRenderFrame* frame = |
| static_cast<TestRenderFrame*>(GetMainRenderFrame()); |
| |
| NavigateAndWait(frame, common_params->Clone(), commit_params->Clone(), |
| web_view_); |
| |
| EXPECT_EQ(1, fake_content_settings_client.did_not_allow_image_count_); |
| } |
| |
| // Checks that when script is blocked, the ContentSettingsAgent receives a |
| // callback. |
| TEST_F(RenderFrameImplTest, ContentSettingsCallbackScriptBlocked) { |
| // Create a fake content settings client to track script blocked callbacks. |
| FakeContentSettingsClient fake_content_settings_client(GetMainRenderFrame()); |
| |
| // Navigate to a URL with script disabled. |
| auto common_params = GetCommonParamsForContentSettingsTest(); |
| common_params->navigation_type = |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT; |
| blink::mojom::CommitNavigationParamsPtr commit_params = |
| blink::CreateCommitNavigationParams(); |
| commit_params->content_settings->allow_script = false; |
| content::TestRenderFrame* frame = |
| static_cast<TestRenderFrame*>(GetMainRenderFrame()); |
| |
| NavigateAndWait(frame, common_params->Clone(), commit_params->Clone(), |
| web_view_); |
| EXPECT_TRUE(HasText(GetMainFrame(), "JS_DISABLED")); |
| EXPECT_FALSE(HasText(GetMainFrame(), "JS_ENABLED")); |
| |
| EXPECT_EQ(1, fake_content_settings_client.did_not_allow_script_count_); |
| } |
| |
| // Checks that when script is allowed, the ContentSettingsAgent does not receive |
| // a callback. |
| TEST_F(RenderFrameImplTest, ContentSettingsCallbackScriptAllowed) { |
| // Create a fake content settings client to track script blocked callbacks. |
| FakeContentSettingsClient fake_content_settings_client(GetMainRenderFrame()); |
| |
| // Navigate to a URL with script enabled. |
| auto common_params = GetCommonParamsForContentSettingsTest(); |
| common_params->navigation_type = |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT; |
| blink::mojom::CommitNavigationParamsPtr commit_params = |
| blink::CreateCommitNavigationParams(); |
| content::TestRenderFrame* frame = |
| static_cast<TestRenderFrame*>(GetMainRenderFrame()); |
| |
| NavigateAndWait(frame, common_params->Clone(), commit_params->Clone(), |
| web_view_); |
| // Verify that the script was not blocked. |
| EXPECT_FALSE(HasText(GetMainFrame(), "JS_DISABLED")); |
| EXPECT_TRUE(HasText(GetMainFrame(), "JS_ENABLED")); |
| |
| // Verify there was no script blocked callback. |
| EXPECT_EQ(0, fake_content_settings_client.did_not_allow_script_count_); |
| } |
| |
| // Regression test for crbug.com/232410: Load a page with JS blocked. Then, |
| // allow JS and reload the page. In each case, only one of noscript or script |
| // tags should be enabled, but never both. |
| TEST_F(RenderFrameImplTest, ContentSettingsNoscriptTag) { |
| // Navigate to a URL with script disabled. |
| auto common_params = GetCommonParamsForContentSettingsTest(); |
| common_params->navigation_type = |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT; |
| blink::mojom::CommitNavigationParamsPtr commit_params = |
| blink::CreateCommitNavigationParams(); |
| commit_params->content_settings->allow_script = false; |
| content::TestRenderFrame* frame = |
| static_cast<TestRenderFrame*>(GetMainRenderFrame()); |
| |
| NavigateAndWait(frame, common_params->Clone(), commit_params->Clone(), |
| web_view_); |
| EXPECT_TRUE(HasText(GetMainFrame(), "JS_DISABLED")); |
| EXPECT_FALSE(HasText(GetMainFrame(), "JS_ENABLED")); |
| |
| // Reload the page but allow Javascript. |
| common_params->navigation_type = blink::mojom::NavigationType::RELOAD; |
| commit_params->content_settings->allow_script = true; |
| NavigateAndWait(frame, common_params->Clone(), commit_params->Clone(), |
| web_view_); |
| EXPECT_FALSE(HasText(GetMainFrame(), "JS_DISABLED")); |
| EXPECT_TRUE(HasText(GetMainFrame(), "JS_ENABLED")); |
| } |
| |
| // Checks that same document navigations don't update content settings for the |
| // page. |
| TEST_F(RenderFrameImplTest, ContentSettingsSameDocumentNavigation) { |
| // Load a page which contains a script. |
| auto common_params = GetCommonParamsForContentSettingsTest(); |
| common_params->navigation_type = |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT; |
| blink::mojom::CommitNavigationParamsPtr commit_params = |
| blink::CreateCommitNavigationParams(); |
| content::TestRenderFrame* frame = |
| static_cast<TestRenderFrame*>(GetMainRenderFrame()); |
| |
| NavigateAndWait(frame, common_params->Clone(), commit_params->Clone(), |
| web_view_); |
| |
| // Verify that the script was not blocked. |
| EXPECT_FALSE(HasText(GetMainFrame(), "JS_DISABLED")); |
| EXPECT_TRUE(HasText(GetMainFrame(), "JS_ENABLED")); |
| |
| RenderFrameImpl* main_frame = GetMainRenderFrame(); |
| |
| main_frame->DidFinishSameDocumentNavigation( |
| blink::kWebStandardCommit, |
| /*is_synchronously_committed=*/true, |
| blink::mojom::SameDocumentNavigationType::kFragment, |
| /*is_client_redirect=*/false, |
| /*screenshot_destination=*/std::nullopt); |
| |
| // Verify that the script was not blocked. |
| EXPECT_FALSE(HasText(GetMainFrame(), "JS_DISABLED")); |
| EXPECT_TRUE(HasText(GetMainFrame(), "JS_ENABLED")); |
| } |
| |
| class RenderFrameImplMojoJsTest : public RenderViewTest { |
| public: |
| RenderFrameImplMojoJsTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| blink::features::kEnableMojoJSProtectedMemory); |
| } |
| |
| void SetUp() override { |
| RenderViewTest::SetUp(); |
| EXPECT_TRUE(GetMainRenderFrame()->IsMainFrame()); |
| } |
| |
| void TearDown() override { |
| #if defined(LEAK_SANITIZER) |
| // Do this before shutting down V8 in RenderViewTest::TearDown(). |
| // http://crbug.com/328552 |
| __lsan_do_leak_check(); |
| #endif |
| RenderViewTest::TearDown(); |
| } |
| |
| TestRenderFrame* GetMainRenderFrame() { |
| return static_cast<TestRenderFrame*>(RenderViewTest::GetMainRenderFrame()); |
| } |
| |
| // Gets the main world script context for the test main frame and returns if |
| // MojoJS bindings are enabled. |
| bool IsMojoJsEnabledForScriptContext() { |
| v8::Isolate* isolate = Isolate(); |
| v8::HandleScope handle_scope(isolate); |
| v8::Local<v8::Context> local_v8_context = |
| GetMainFrame()->MainWorldScriptContext(); |
| |
| return blink::WebV8Features::IsMojoJSEnabledForTesting(local_v8_context); |
| } |
| |
| // Method used to validate the final stage protected memory check in |
| // ContextFeatureSettings::isMojoJSEnabled constructs a scenario where |
| // the |enable_mojo_js_| value of the ContextFeatureSettings is tampered with |
| // directly before being used. We expect this to crash. |
| void ContextFeatureSettingsEnableMojoJsTampered() { |
| v8::Isolate* isolate = Isolate(); |
| v8::HandleScope handle_scope(isolate); |
| v8::Local<v8::Context> local_v8_context = |
| GetMainFrame()->MainWorldScriptContext(); |
| |
| // Use |WebV8Features::EnableMojoJSForTesting| to enable the MojoJS bindings |
| // while bypassing the earlier protected memory checks. This mimics an |
| // attacker tampering with the |enable_mojo_js_| value of the |
| // ContextFeatureSettings. |
| blink::WebV8Features::EnableMojoJSWithoutSecurityChecksForTesting( |
| local_v8_context); |
| |
| // Use |WebV8Features::isMojoJSEnabledForTesting| to manually access |
| // |ContextFeatureSettings::isMojoEnabled()| This is used to mimic a |
| // scenario where an attacker manages to directly tamper with |
| // |enable_mojo_js_| before the |ContextFeatureSettings::isMojoJSEnabled()| |
| // is called to determine if the bindings is enabled. This validates the |
| // final protected memory check and should crash. |
| blink::WebV8Features::IsMojoJSEnabledForTesting(local_v8_context); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Verifies enabling MojoJS bindings via allowing the |
| // BINDINGS_POLICY_MOJO_WEB_UI binding. |
| TEST_F(RenderFrameImplMojoJsTest, AllowMojoWebUIBindings) { |
| GetMainRenderFrame()->AllowBindings(BINDINGS_POLICY_MOJO_WEB_UI); |
| LoadHTML(kSimpleScriptHtml); |
| |
| // Expect no crash and MojoJs bindings are enabled in the context. |
| EXPECT_TRUE(IsMojoJsEnabledForScriptContext()); |
| } |
| |
| // Verifies enabling MojoJS bindings via EnableMojoJsBindings method. |
| TEST_F(RenderFrameImplMojoJsTest, EnableMojoJSBindings) { |
| GetMainRenderFrame()->EnableMojoJsBindings( |
| content::mojom::ExtraMojoJsFeatures::New()); |
| LoadHTML(kSimpleScriptHtml); |
| |
| // Expect no crash and MojoJs bindings are enabled in the context. |
| EXPECT_TRUE(IsMojoJsEnabledForScriptContext()); |
| } |
| |
| // Verifies enabling MojoJS bindings via directly enabling mojo |
| TEST_F(RenderFrameImplMojoJsTest, EnableMojoJSBindingsWithBroker) { |
| GetMainRenderFrame()->EnableMojoJsBindingsWithBroker( |
| TestRenderFrame::CreateStubBrowserInterfaceBrokerRemote()); |
| LoadHTML(kSimpleScriptHtml); |
| |
| // Expect no crash and MojoJs bindings are enabled in the context. |
| EXPECT_TRUE(IsMojoJsEnabledForScriptContext()); |
| } |
| |
| #if BUILDFLAG(PROTECTED_MEMORY_ENABLED) |
| using RenderFrameImplMojoJsDeathTest = RenderFrameImplMojoJsTest; |
| // Verifies that tampering with enabled_bindings_ to enable MojoJS bindings |
| // crashes. |
| TEST_F(RenderFrameImplMojoJsDeathTest, EnabledBindingsTampered) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| |
| // Should CHECK fail due to the bindings value differing from the protected |
| // memory value. |
| BASE_EXPECT_DEATH( |
| { |
| GetMainRenderFrame()->enabled_bindings_ |= BINDINGS_POLICY_MOJO_WEB_UI; |
| |
| LoadHTML(kSimpleScriptHtml); |
| }, |
| "Check failed: \\*mojo_js_allowed_"); |
| } |
| |
| // Verifies that tampering with enable_mojo_js_bindings_ to enable MojoJS |
| // bindings crashes. |
| TEST_F(RenderFrameImplMojoJsDeathTest, EnableMojoJsBindingsTampered) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| |
| // Should CHECK fail due to the bindings value differing from the protected |
| // memory value. |
| BASE_EXPECT_DEATH( |
| { |
| GetMainRenderFrame()->enable_mojo_js_bindings_ = true; |
| |
| LoadHTML(kSimpleScriptHtml); |
| }, |
| "Check failed: \\*mojo_js_allowed_"); |
| } |
| |
| // Verifies that tampering with mojo_js_interface_broker_ to enable MojoJS |
| // bindings crashes. |
| TEST_F(RenderFrameImplMojoJsDeathTest, MojoJsInterfaceBrokerTampered) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| |
| // Should CHECK fail due to the bindings value differing from the protected |
| // memory value. |
| BASE_EXPECT_DEATH( |
| { |
| GetMainRenderFrame()->mojo_js_interface_broker_ = |
| TestRenderFrame::CreateStubBrowserInterfaceBrokerRemote(); |
| |
| LoadHTML(kSimpleScriptHtml); |
| }, |
| "Check failed: \\*mojo_js_allowed_"); |
| } |
| |
| // Verifies that tampering with mojo_js_interface_broker_ to enable MojoJS |
| // bindings crashes. |
| TEST_F(RenderFrameImplMojoJsDeathTest, |
| ContextFeatureSettingsEnableMojoJsTampered) { |
| GTEST_FLAG_SET(death_test_style, "threadsafe"); |
| |
| // Should CHECK fail due to the bindings value differing from the protected |
| // memory value. |
| BASE_EXPECT_DEATH(ContextFeatureSettingsEnableMojoJsTampered(), |
| "Check failed: \\*mojo_js_allowed_"); |
| } |
| #endif // BUILDFLAG(PROTECTED_MEMORY_ENABLED) |
| |
| } // namespace content |