blob: 863784ae53965ba5313f59fc83bf16a38ddd34e0 [file] [log] [blame]
// 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(https://crbug.com/792410): 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(https://crbug.com/555773): 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(https://crbug.com/792410): 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/718652): 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:
FakeContentSettingsClient() = default;
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;
};
} // 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()->GetWebFrame()->SetContentSettingsClient(
&fake_content_settings_client);
// 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()->GetWebFrame()->SetContentSettingsClient(
&fake_content_settings_client);
// 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()->GetWebFrame()->SetContentSettingsClient(
&fake_content_settings_client);
// 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);
// 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