// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/renderer/browser_plugin/browser_plugin_browsertest.h" #include "base/debug/leak_annotations.h" #include "base/files/file_path.h" #include "base/memory/singleton.h" #include "base/path_service.h" #include "base/pickle.h" #include "content/public/common/content_constants.h" #include "content/public/renderer/content_renderer_client.h" #include "content/renderer/browser_plugin/browser_plugin.h" #include "content/renderer/browser_plugin/browser_plugin_manager_factory.h" #include "content/renderer/browser_plugin/mock_browser_plugin.h" #include "content/renderer/browser_plugin/mock_browser_plugin_manager.h" #include "content/renderer/render_thread_impl.h" #include "content/renderer/renderer_webkitplatformsupport_impl.h" #include "skia/ext/platform_canvas.h" #include "third_party/WebKit/public/platform/WebCursorInfo.h" #include "third_party/WebKit/public/web/WebInputEvent.h" #include "third_party/WebKit/public/web/WebLocalFrame.h" #include "third_party/WebKit/public/web/WebScriptSource.h" namespace content { namespace { const char kHTMLForBrowserPluginObject[] = "" ""; const char kHTMLForBrowserPluginWithAllAttributes[] = ""; const char kHTMLForSourcelessPluginObject[] = ""; const char kHTMLForPartitionedPluginObject[] = ""; const char kHTMLForInvalidPartitionedPluginObject[] = ""; const char kHTMLForPartitionedPersistedPluginObject[] = ""; std::string GetHTMLForBrowserPluginObject() { return base::StringPrintf(kHTMLForBrowserPluginObject, kBrowserPluginMimeType); } } // namespace class TestContentRendererClient : public ContentRendererClient { public: TestContentRendererClient() : ContentRendererClient() { } virtual ~TestContentRendererClient() { } virtual bool AllowBrowserPlugin( blink::WebPluginContainer* container) OVERRIDE { // Allow BrowserPlugin for tests. return true; } }; // Test factory for creating test instances of BrowserPluginManager. class TestBrowserPluginManagerFactory : public BrowserPluginManagerFactory { public: virtual MockBrowserPluginManager* CreateBrowserPluginManager( RenderViewImpl* render_view) OVERRIDE { return new MockBrowserPluginManager(render_view); } // Singleton getter. static TestBrowserPluginManagerFactory* GetInstance() { return Singleton::get(); } protected: TestBrowserPluginManagerFactory() {} virtual ~TestBrowserPluginManagerFactory() {} private: // For Singleton. friend struct DefaultSingletonTraits; DISALLOW_COPY_AND_ASSIGN(TestBrowserPluginManagerFactory); }; BrowserPluginTest::BrowserPluginTest() {} BrowserPluginTest::~BrowserPluginTest() {} void BrowserPluginTest::SetUp() { BrowserPluginManager::set_factory_for_testing( TestBrowserPluginManagerFactory::GetInstance()); content::RenderViewTest::SetUp(); } void BrowserPluginTest::TearDown() { BrowserPluginManager::set_factory_for_testing( TestBrowserPluginManagerFactory::GetInstance()); #if defined(LEAK_SANITIZER) // Do this before shutting down V8 in RenderViewTest::TearDown(). // http://crbug.com/328552 __lsan_do_leak_check(); #endif RenderViewTest::TearDown(); } ContentRendererClient* BrowserPluginTest::CreateContentRendererClient() { return new TestContentRendererClient; } std::string BrowserPluginTest::ExecuteScriptAndReturnString( const std::string& script) { v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); v8::Handle value = GetMainFrame()->executeScriptAndReturnValue( blink::WebScriptSource(blink::WebString::fromUTF8(script.c_str()))); if (value.IsEmpty() || !value->IsString()) return std::string(); v8::Local v8_str = value->ToString(); int length = v8_str->Utf8Length() + 1; scoped_ptr str(new char[length]); v8_str->WriteUtf8(str.get(), length); return str.get(); } int BrowserPluginTest::ExecuteScriptAndReturnInt( const std::string& script) { v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); v8::Handle value = GetMainFrame()->executeScriptAndReturnValue( blink::WebScriptSource(blink::WebString::fromUTF8(script.c_str()))); if (value.IsEmpty() || !value->IsInt32()) return 0; return value->Int32Value(); } // A return value of false means that a value was not present. The return value // of the script is stored in |result| bool BrowserPluginTest::ExecuteScriptAndReturnBool( const std::string& script, bool* result) { v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); v8::Handle value = GetMainFrame()->executeScriptAndReturnValue( blink::WebScriptSource(blink::WebString::fromUTF8(script.c_str()))); if (value.IsEmpty() || !value->IsBoolean()) return false; *result = value->BooleanValue(); return true; } MockBrowserPlugin* BrowserPluginTest::GetCurrentPlugin() { BrowserPluginHostMsg_Attach_Params params; return GetCurrentPluginWithAttachParams(¶ms); } MockBrowserPlugin* BrowserPluginTest::GetCurrentPluginWithAttachParams( BrowserPluginHostMsg_Attach_Params* params) { int instance_id = 0; const IPC::Message* msg = browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_Attach::ID); if (!msg) return NULL; PickleIterator iter(*msg); if (!iter.ReadInt(&instance_id)) return NULL; if (!IPC::ParamTraits::Read( msg, &iter, params)) return NULL; MockBrowserPlugin* browser_plugin = static_cast( browser_plugin_manager()->GetBrowserPlugin(instance_id)); BrowserPluginMsg_Attach_ACK_Params attach_ack_params; browser_plugin->OnAttachACK(instance_id, attach_ack_params); return browser_plugin; } // This test verifies that an initial resize occurs when we instantiate the // browser plugin. TEST_F(BrowserPluginTest, InitialResize) { LoadHTML(GetHTMLForBrowserPluginObject().c_str()); // Verify that the information in Attach is correct. BrowserPluginHostMsg_Attach_Params params; MockBrowserPlugin* browser_plugin = GetCurrentPluginWithAttachParams(¶ms); EXPECT_EQ(640, params.resize_guest_params.view_rect.width()); EXPECT_EQ(480, params.resize_guest_params.view_rect.height()); ASSERT_TRUE(browser_plugin); } // This test verifies that all attributes (present at the time of writing) are // parsed on initialization. However, this test does minimal checking of // correct behavior. TEST_F(BrowserPluginTest, ParseAllAttributes) { std::string html = base::StringPrintf(kHTMLForBrowserPluginWithAllAttributes, kBrowserPluginMimeType); LoadHTML(html.c_str()); bool result; bool has_value = ExecuteScriptAndReturnBool( "document.getElementById('browserplugin').autosize", &result); EXPECT_TRUE(has_value); EXPECT_TRUE(result); int maxHeight = ExecuteScriptAndReturnInt( "document.getElementById('browserplugin').maxheight"); EXPECT_EQ(600, maxHeight); int maxWidth = ExecuteScriptAndReturnInt( "document.getElementById('browserplugin').maxwidth"); EXPECT_EQ(800, maxWidth); int minHeight = ExecuteScriptAndReturnInt( "document.getElementById('browserplugin').minheight"); EXPECT_EQ(240, minHeight); int minWidth = ExecuteScriptAndReturnInt( "document.getElementById('browserplugin').minwidth"); EXPECT_EQ(320, minWidth); std::string name = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').name"); EXPECT_STREQ("Jim", name.c_str()); std::string partition = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').partition"); EXPECT_STREQ("someid", partition.c_str()); std::string src = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').src"); EXPECT_STREQ("foo", src.c_str()); } // Verify that the src attribute on the browser plugin works as expected. TEST_F(BrowserPluginTest, SrcAttribute) { LoadHTML(GetHTMLForBrowserPluginObject().c_str()); // Verify that we're reporting the correct URL to navigate to based on the // src attribute. { BrowserPluginHostMsg_Attach_Params params; MockBrowserPlugin* browser_plugin = GetCurrentPluginWithAttachParams(¶ms); ASSERT_TRUE(browser_plugin); EXPECT_EQ("foo", params.src); } browser_plugin_manager()->sink().ClearMessages(); // Navigate to bar and observe the associated // BrowserPluginHostMsg_NavigateGuest message. // Verify that the src attribute is updated as well. ExecuteJavaScript("document.getElementById('browserplugin').src = 'bar'"); { // Verify that we do not get a Attach on subsequent navigations. const IPC::Message* create_msg = browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_Attach::ID); ASSERT_FALSE(create_msg); const IPC::Message* msg = browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_NavigateGuest::ID); ASSERT_TRUE(msg); BrowserPluginHostMsg_NavigateGuest::Param params; BrowserPluginHostMsg_NavigateGuest::Read(msg, ¶ms); std::string src = params.b; EXPECT_EQ("bar", src); std::string src_value = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').src"); EXPECT_EQ("bar", src_value); } } TEST_F(BrowserPluginTest, ResizeFlowControl) { LoadHTML(GetHTMLForBrowserPluginObject().c_str()); MockBrowserPlugin* browser_plugin = GetCurrentPlugin(); ASSERT_TRUE(browser_plugin); int instance_id = browser_plugin->guest_instance_id(); // Send an UpdateRect to the BrowserPlugin to make sure the browser sees a // resize related (SetAutoSize) message. { // We send a stale UpdateRect to the BrowserPlugin. BrowserPluginMsg_UpdateRect_Params update_rect_params; update_rect_params.view_size = gfx::Size(640, 480); update_rect_params.scale_factor = 1.0f; update_rect_params.is_resize_ack = true; BrowserPluginMsg_UpdateRect msg(instance_id, update_rect_params); browser_plugin->OnMessageReceived(msg); } browser_plugin_manager()->sink().ClearMessages(); // Resize the browser plugin three times. ExecuteJavaScript("document.getElementById('browserplugin').width = '641px'"); GetMainFrame()->view()->layout(); ProcessPendingMessages(); ExecuteJavaScript("document.getElementById('browserplugin').width = '642px'"); GetMainFrame()->view()->layout(); ProcessPendingMessages(); ExecuteJavaScript("document.getElementById('browserplugin').width = '643px'"); GetMainFrame()->view()->layout(); ProcessPendingMessages(); // Expect to see one resize messsage in the sink. BrowserPlugin will not issue // subsequent resize requests until the first request is satisfied by the // guest. The rest of the messages could be // BrowserPluginHostMsg_UpdateGeometry msgs. EXPECT_LE(1u, browser_plugin_manager()->sink().message_count()); for (size_t i = 0; i < browser_plugin_manager()->sink().message_count(); ++i) { const IPC::Message* msg = browser_plugin_manager()->sink().GetMessageAt(i); if (msg->type() != BrowserPluginHostMsg_ResizeGuest::ID) EXPECT_EQ(msg->type(), BrowserPluginHostMsg_UpdateGeometry::ID); } const IPC::Message* msg = browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_ResizeGuest::ID); ASSERT_TRUE(msg); BrowserPluginHostMsg_ResizeGuest::Param param; BrowserPluginHostMsg_ResizeGuest::Read(msg, ¶m); instance_id = param.a; BrowserPluginHostMsg_ResizeGuest_Params params = param.b; EXPECT_EQ(641, params.view_rect.width()); EXPECT_EQ(480, params.view_rect.height()); { // We send a stale UpdateRect to the BrowserPlugin. BrowserPluginMsg_UpdateRect_Params update_rect_params; update_rect_params.view_size = gfx::Size(641, 480); update_rect_params.scale_factor = 1.0f; update_rect_params.is_resize_ack = true; BrowserPluginMsg_UpdateRect msg(instance_id, update_rect_params); browser_plugin->OnMessageReceived(msg); } // Send the BrowserPlugin another UpdateRect, but this time with a size // that matches the size of the container. { BrowserPluginMsg_UpdateRect_Params update_rect_params; update_rect_params.view_size = gfx::Size(643, 480); update_rect_params.scale_factor = 1.0f; update_rect_params.is_resize_ack = true; BrowserPluginMsg_UpdateRect msg(instance_id, update_rect_params); browser_plugin->OnMessageReceived(msg); } } TEST_F(BrowserPluginTest, RemovePlugin) { LoadHTML(GetHTMLForBrowserPluginObject().c_str()); EXPECT_FALSE(browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_PluginDestroyed::ID)); ExecuteJavaScript("x = document.getElementById('browserplugin'); " "x.parentNode.removeChild(x);"); ProcessPendingMessages(); EXPECT_TRUE(browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_PluginDestroyed::ID)); } // This test verifies that PluginDestroyed messages do not get sent from a // BrowserPlugin that has never navigated. TEST_F(BrowserPluginTest, RemovePluginBeforeNavigation) { std::string html = base::StringPrintf(kHTMLForSourcelessPluginObject, kBrowserPluginMimeType); LoadHTML(html.c_str()); EXPECT_FALSE(browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_PluginDestroyed::ID)); ExecuteJavaScript("x = document.getElementById('browserplugin'); " "x.parentNode.removeChild(x);"); ProcessPendingMessages(); EXPECT_FALSE(browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_PluginDestroyed::ID)); } // Verify that the 'partition' attribute on the browser plugin is parsed // correctly. TEST_F(BrowserPluginTest, PartitionAttribute) { std::string html = base::StringPrintf(kHTMLForPartitionedPluginObject, kBrowserPluginMimeType); LoadHTML(html.c_str()); std::string partition_value = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').partition"); EXPECT_STREQ("someid", partition_value.c_str()); html = base::StringPrintf(kHTMLForPartitionedPersistedPluginObject, kBrowserPluginMimeType); LoadHTML(html.c_str()); partition_value = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').partition"); EXPECT_STREQ("persist:someid", partition_value.c_str()); // Verify that once HTML has defined a source and partition, we cannot change // the partition anymore. ExecuteJavaScript( "try {" " document.getElementById('browserplugin').partition = 'foo';" " document.title = 'success';" "} catch (e) { document.title = e.message; }"); std::string title = ExecuteScriptAndReturnString("document.title"); EXPECT_STREQ( "The object has already navigated, so its partition cannot be changed.", title.c_str()); // Load a browser tag without 'src' defined. html = base::StringPrintf(kHTMLForSourcelessPluginObject, kBrowserPluginMimeType); LoadHTML(html.c_str()); // Ensure we don't parse just "persist:" string and return exception. ExecuteJavaScript( "try {" " document.getElementById('browserplugin').partition = 'persist:';" " document.title = 'success';" "} catch (e) { document.title = e.message; }"); title = ExecuteScriptAndReturnString("document.title"); EXPECT_STREQ("Invalid partition attribute.", title.c_str()); } // This test verifies that BrowserPlugin enters an error state when the // partition attribute is invalid. TEST_F(BrowserPluginTest, InvalidPartition) { std::string html = base::StringPrintf(kHTMLForInvalidPartitionedPluginObject, kBrowserPluginMimeType); LoadHTML(html.c_str()); // Attempt to navigate with an invalid partition. { ExecuteJavaScript( "try {" " document.getElementById('browserplugin').src = 'bar';" " document.title = 'success';" "} catch (e) { document.title = e.message; }"); std::string title = ExecuteScriptAndReturnString("document.title"); EXPECT_STREQ("Invalid partition attribute.", title.c_str()); // Verify that the 'src' attribute has not been updated. EXPECT_EQ("", ExecuteScriptAndReturnString( "document.getElementById('browserplugin').src")); } // Verify that the BrowserPlugin accepts changes to its src attribue after // setting the partition to a valid value. ExecuteJavaScript( "document.getElementById('browserplugin').partition = 'persist:foo'"); ExecuteJavaScript("document.getElementById('browserplugin').src = 'bar'"); EXPECT_EQ("bar", ExecuteScriptAndReturnString( "document.getElementById('browserplugin').src")); ProcessPendingMessages(); // Verify that the BrowserPlugin does not 'deadlock': it can recover from // the partition ID error state. { ExecuteJavaScript( "try {" " document.getElementById('browserplugin').partition = 'persist:1337';" " document.title = 'success';" "} catch (e) { document.title = e.message; }"); std::string title = ExecuteScriptAndReturnString("document.title"); EXPECT_STREQ( "The object has already navigated, so its partition cannot be changed.", title.c_str()); ExecuteJavaScript("document.getElementById('browserplugin').src = '42'"); EXPECT_EQ("42", ExecuteScriptAndReturnString( "document.getElementById('browserplugin').src")); } } // Test to verify that after the first navigation, the partition attribute // cannot be modified. TEST_F(BrowserPluginTest, ImmutableAttributesAfterNavigation) { std::string html = base::StringPrintf(kHTMLForSourcelessPluginObject, kBrowserPluginMimeType); LoadHTML(html.c_str()); ExecuteJavaScript( "document.getElementById('browserplugin').partition = 'storage'"); std::string partition_value = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').partition"); EXPECT_STREQ("storage", partition_value.c_str()); std::string src_value = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').src"); EXPECT_STREQ("", src_value.c_str()); ExecuteJavaScript("document.getElementById('browserplugin').src = 'bar'"); ProcessPendingMessages(); { BrowserPluginHostMsg_Attach_Params params; MockBrowserPlugin* browser_plugin = GetCurrentPluginWithAttachParams(¶ms); ASSERT_TRUE(browser_plugin); EXPECT_STREQ("storage", params.storage_partition_id.c_str()); EXPECT_FALSE(params.persist_storage); EXPECT_STREQ("bar", params.src.c_str()); } // Setting the partition should throw an exception and the value should not // change. ExecuteJavaScript( "try {" " document.getElementById('browserplugin').partition = 'someid';" " document.title = 'success';" "} catch (e) { document.title = e.message; }"); std::string title = ExecuteScriptAndReturnString("document.title"); EXPECT_STREQ( "The object has already navigated, so its partition cannot be changed.", title.c_str()); partition_value = ExecuteScriptAndReturnString( "document.getElementById('browserplugin').partition"); EXPECT_STREQ("storage", partition_value.c_str()); } TEST_F(BrowserPluginTest, AutoSizeAttributes) { std::string html = base::StringPrintf(kHTMLForSourcelessPluginObject, kBrowserPluginMimeType); LoadHTML(html.c_str()); const char* kSetAutoSizeParametersAndNavigate = "var browserplugin = document.getElementById('browserplugin');" "browserplugin.autosize = true;" "browserplugin.minwidth = 42;" "browserplugin.minheight = 43;" "browserplugin.maxwidth = 1337;" "browserplugin.maxheight = 1338;" "browserplugin.src = 'foobar';"; const char* kDisableAutoSize = "document.getElementById('browserplugin').removeAttribute('autosize');"; int instance_id = 0; // Set some autosize parameters before navigating then navigate. // Verify that the BrowserPluginHostMsg_Attach message contains // the correct autosize parameters. ExecuteJavaScript(kSetAutoSizeParametersAndNavigate); ProcessPendingMessages(); BrowserPluginHostMsg_Attach_Params params; MockBrowserPlugin* browser_plugin = GetCurrentPluginWithAttachParams(¶ms); ASSERT_TRUE(browser_plugin); EXPECT_TRUE(params.auto_size_params.enable); EXPECT_EQ(42, params.auto_size_params.min_size.width()); EXPECT_EQ(43, params.auto_size_params.min_size.height()); EXPECT_EQ(1337, params.auto_size_params.max_size.width()); EXPECT_EQ(1338, params.auto_size_params.max_size.height()); // Disable autosize. AutoSize state will not be sent to the guest until // the guest has responded to the last resize request. ExecuteJavaScript(kDisableAutoSize); ProcessPendingMessages(); const IPC::Message* auto_size_msg = browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_SetAutoSize::ID); EXPECT_FALSE(auto_size_msg); // Send the BrowserPlugin an UpdateRect equal to its |max_size|. BrowserPluginMsg_UpdateRect_Params update_rect_params; update_rect_params.view_size = gfx::Size(1337, 1338); update_rect_params.scale_factor = 1.0f; update_rect_params.is_resize_ack = true; BrowserPluginMsg_UpdateRect msg(instance_id, update_rect_params); browser_plugin->OnMessageReceived(msg); // Verify that the autosize state has been updated. { const IPC::Message* auto_size_msg = browser_plugin_manager()->sink().GetUniqueMessageMatching( BrowserPluginHostMsg_SetAutoSize::ID); ASSERT_TRUE(auto_size_msg); BrowserPluginHostMsg_SetAutoSize::Param param; BrowserPluginHostMsg_SetAutoSize::Read(auto_size_msg, ¶m); BrowserPluginHostMsg_AutoSize_Params auto_size_params = param.b; BrowserPluginHostMsg_ResizeGuest_Params resize_params = param.c; EXPECT_FALSE(auto_size_params.enable); // These value are not populated (as an optimization) if autosize is // disabled. EXPECT_EQ(0, auto_size_params.min_size.width()); EXPECT_EQ(0, auto_size_params.min_size.height()); EXPECT_EQ(0, auto_size_params.max_size.width()); EXPECT_EQ(0, auto_size_params.max_size.height()); } } } // namespace content