// Copyright (c) 2015 The Chromium Embedded Framework 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 "include/base/cef_callback.h" #include "include/cef_pack_resources.h" #include "include/cef_request_context_handler.h" #include "include/wrapper/cef_closure_task.h" #include "include/wrapper/cef_stream_resource_handler.h" #include "tests/ceftests/routing_test_handler.h" #include "tests/gtest/include/gtest/gtest.h" #include "tests/shared/browser/client_app_browser.h" #include "tests/shared/browser/resource_util.h" namespace { // Browser-side app delegate. class PluginBrowserTest : public client::ClientAppBrowser::Delegate { public: PluginBrowserTest() {} void OnBeforeCommandLineProcessing( CefRefPtr app, CefRefPtr command_line) override { // Allow all plugin loading by default. command_line->AppendSwitchWithValue("plugin-policy", "allow"); } private: IMPLEMENT_REFCOUNTING(PluginBrowserTest); }; const char kPdfTestOrigin[] = "http://tests/"; const char kPdfHtmlUrl[] = "http://tests/pdf.html"; const char kPdfDirectUrl[] = "http://tests/pdf.pdf"; // Delay waiting for iframe tests to load the PDF file. #if defined(OS_LINUX) const int64 kPdfLoadDelayMs = 7000; #else const int64 kPdfLoadDelayMs = 5000; #endif // Browser-side test handler. class PluginTestHandler : public RoutingTestHandler, public CefContextMenuHandler { public: enum Mode { // No specified context or handler (implicitly uses the global context). GLOBAL_DEFAULT, // Global context with no handler. GLOBAL_NO_HANDLER, // Global context with handler that allows plugin load. GLOBAL_ALLOW, // Global context with handler that blocks plugin load. Then, load the // plugin via the context menu. GLOBAL_BLOCK_LOAD, // Global context with handler that blocks plugin load. Then, hide the // plugin via the context menu. GLOBAL_BLOCK_HIDE, // Global context with handler that disables plugin load. Then, hide the // plugin via the context menu. GLOBAL_DISABLE_HIDE, // Global context with handler that removes the plugin from the // `navigator.plugins` list and consequently disables plugin load. GLOBAL_NO_LIST, // Custom context with no handler. CUSTOM_NO_HANDLER, // Custom context with handler that allows plugin load. CUSTOM_ALLOW, // Custom context with handler that blocks plugin load. Then, load the // plugin via the context menu. CUSTOM_BLOCK_LOAD, // Custom context with handler that blocks plugin load. Then, hide the // plugin via the context menu. CUSTOM_BLOCK_HIDE, // Custom context with handler that disables plugin load. Then, hide the // plugin via the context menu. CUSTOM_DISABLE_HIDE, // Custom context with handler that removes the plugin from the // `navigator.plugins` list and consequently disables plugin load. CUSTOM_NO_LIST, }; class RequestContextHandler : public CefRequestContextHandler { public: explicit RequestContextHandler(PluginTestHandler* handler) : handler_(handler) {} bool OnBeforePluginLoad(const CefString& mime_type, const CefString& plugin_url, bool is_main_frame, const CefString& top_origin_url, CefRefPtr plugin_info, PluginPolicy* plugin_policy) override { const std::string& mime_type_str = mime_type; EXPECT_STREQ("application/pdf", mime_type_str.c_str()); EXPECT_STREQ("chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/", plugin_info->GetPath().ToString().c_str()); if (top_origin_url.empty()) { if (!handler_->got_on_before_plugin_empty_origin_) { // Checking for PDF support in the plugin frame (navigator.plugins // listing, pdf load, etc). handler_->got_on_before_plugin_empty_origin_.yes(); } else if (handler_->HasNoList()) { // When listing is disabled there should be an additional check in the // main frame for the navigator.plugins listing. if (!handler_->got_on_before_plugin_empty_origin2_) { EXPECT_TRUE(is_main_frame); handler_->got_on_before_plugin_empty_origin2_.yes(); } } if (handler_->HasNoList()) { // Remove the PDF plugin from the `navigator.plugins` list. *plugin_policy = PLUGIN_POLICY_DISABLE; return true; } // Ignore requests for building the plugin list. return false; } // Should only get requests for the test origin. EXPECT_STREQ(kPdfTestOrigin, top_origin_url.ToString().c_str()); if (!handler_->got_on_before_plugin_load_pdf_) handler_->got_on_before_plugin_load_pdf_.yes(); else NOTREACHED(); if (handler_->HasAllow()) { *plugin_policy = PLUGIN_POLICY_ALLOW; return true; } else if (handler_->HasBlock()) { *plugin_policy = PLUGIN_POLICY_BLOCK; return true; } else if (handler_->HasDisable()) { *plugin_policy = PLUGIN_POLICY_DISABLE; return true; } return false; } void Detach() { handler_ = nullptr; } private: PluginTestHandler* handler_; IMPLEMENT_REFCOUNTING(RequestContextHandler); }; PluginTestHandler(Mode mode, const std::string& url) : mode_(mode), url_(url) {} // Loading the PDF directly in the main frame instead of a sub-frame. bool HasDirectPluginLoad() const { return url_ == kPdfDirectUrl; } // Has a specified RequestContext but not necessarily a custom handler. bool HasRequestContext() const { return mode_ != GLOBAL_DEFAULT; } // Has a specified RequestContext and custom handler. bool HasRequestContextHandler() const { return mode_ != GLOBAL_DEFAULT && mode_ != GLOBAL_NO_HANDLER && mode_ != CUSTOM_NO_HANDLER; } // Using the global request context, either directly or with a custom handler. bool HasGlobalRequestContext() const { return mode_ >= GLOBAL_DEFAULT && mode_ <= GLOBAL_NO_LIST; } // Should allow the plugin load via the custom handler. bool HasAllow() const { return mode_ == GLOBAL_ALLOW || mode_ == CUSTOM_ALLOW; } // Should block the plugin load via the custom handler. bool HasBlock() const { return mode_ == GLOBAL_BLOCK_LOAD || mode_ == GLOBAL_BLOCK_HIDE || mode_ == CUSTOM_BLOCK_LOAD || mode_ == CUSTOM_BLOCK_HIDE; } // Should disable the plugin load via the custom handler. bool HasDisable() const { return mode_ == GLOBAL_DISABLE_HIDE || mode_ == CUSTOM_DISABLE_HIDE; } // Should exclude the plugin from the `navigator.plugins` list. bool HasNoList() const { return mode_ == GLOBAL_NO_LIST || mode_ == CUSTOM_NO_LIST; } // Should load the plugin via the context menu. bool HasContextLoad() const { return mode_ == GLOBAL_BLOCK_LOAD || mode_ == CUSTOM_BLOCK_LOAD; } // Should hide the plugin via the context menu. bool HasContextHide() const { return mode_ == GLOBAL_BLOCK_HIDE || mode_ == GLOBAL_DISABLE_HIDE || mode_ == CUSTOM_BLOCK_HIDE || mode_ == CUSTOM_DISABLE_HIDE; } std::string GetPluginNode() const { return "document.getElementsByTagName('embed')[0]"; } void WaitForNavigatorPlugins(CefRefPtr frame) { // We can no longer test for navigator.plugins because it returns empty // as of https://www.chromestatus.com/feature/5741884322349056. if (HasNoList()) { // The plugin will not load. End the test. EndTest(); } else if (HasBlock() || HasDisable()) { // Wait for the plugin placeholder for the first PDF file to load. The // test will continue from OnQuery. WaitForPlaceholderLoad(frame); } else { // Wait for the first PDF file to load. WaitForPluginLoad(frame); } } void WaitForPlaceholderLoad(CefRefPtr frame) { // Keep track of the frame that contains the placeholder. placeholder_frame_ = frame; // Waits for the placeholder to load. This is indicated by the presence // of the plugin node. const std::string& code = "(function waitForNode(i) {" " setTimeout(function() {" " var plugin = " + GetPluginNode() + ";" " if (plugin) {" " window.testQuery({request:'placeholder_loaded'});" " return;" " }" " if (--i) {" " waitForNode(i);" " } else {" " console.log('Timeout waiting for plugin load');" " }" " }, 250)" "})(4);"; frame->ExecuteJavaScript(code, frame->GetURL(), 0); } void WaitForPlaceholderHide() { // Waits for the placeholder to be hidden (style set to display:none). // See PluginPlaceholderBase::HidePlugin. const std::string& code = "var plugin = " + GetPluginNode() + ";" "var observer = new MutationObserver(function(mutations) {" " mutations.forEach(function(mutationRecord) {" " window.testQuery({request:'placeholder_hidden'});" " observer.disconnect();" " });" "});" "observer.observe(plugin, " " { attributes : true, attributeFilter : ['style'] });"; placeholder_frame_->ExecuteJavaScript(code, placeholder_frame_->GetURL(), 0); placeholder_frame_ = nullptr; } void WaitForPluginLoad(CefRefPtr frame) { if (got_context_menu_dismissed_) { // After context menu display. Destroy the test. CefPostDelayedTask(TID_UI, base::BindOnce(&PluginTestHandler::DestroyTest, this), kPdfLoadDelayMs); } else { // Trigger the context menu. CefPostDelayedTask(TID_UI, base::BindOnce(&PluginTestHandler::TriggerContextMenu, this, frame->GetBrowser()), kPdfLoadDelayMs); } } void EndTest() { CefPostTask(TID_UI, base::BindOnce(&PluginTestHandler::DestroyTest, this)); } CefRefPtr GetContextMenuHandler() override { return this; } void RunTest() override { CefRefPtr request_context; if (HasRequestContext()) { if (HasRequestContextHandler()) context_handler_ = new RequestContextHandler(this); if (HasGlobalRequestContext()) { // Use the global request context. request_context = CefRequestContext::CreateContext( CefRequestContext::GetGlobalContext(), context_handler_.get()); } else { // Create the request context that will use an in-memory cache. CefRequestContextSettings settings; request_context = CefRequestContext::CreateContext(settings, context_handler_.get()); } } // Create the browser. CreateBrowser(url_, request_context); // Time out the test after a reasonable period of time. SetTestTimeout(5000 + kPdfLoadDelayMs); } CefRefPtr GetResourceHandler( CefRefPtr browser, CefRefPtr frame, CefRefPtr request) override { const std::string& url = request->GetURL(); if (url == kPdfHtmlUrl) { CefRefPtr stream = client::GetBinaryResourceReader("pdf.html"); return new CefStreamResourceHandler("text/html", stream); } else if (url == kPdfDirectUrl) { CefRefPtr stream = client::GetBinaryResourceReader("pdf.pdf"); return new CefStreamResourceHandler("application/pdf", stream); } return nullptr; } void OnLoadEnd(CefRefPtr browser, CefRefPtr frame, int httpStatusCode) override { bool is_pdf1 = false; const std::string& url = frame->GetURL(); if (url == kPdfHtmlUrl) { if (!got_on_load_end_html_) got_on_load_end_html_.yes(); else NOTREACHED(); } else if (url == kPdfDirectUrl) { if (!got_on_load_end_pdf1_) { got_on_load_end_pdf1_.yes(); is_pdf1 = true; } else if (!got_on_load_end_pdf2_) { got_on_load_end_pdf2_.yes(); } else { NOTREACHED(); } } else { NOTREACHED(); } if (HasNoList()) { // If the plugin is not listed then the PDF documents will never load. EXPECT_STREQ(kPdfHtmlUrl, url.c_str()); WaitForNavigatorPlugins(frame); } else if (is_pdf1) { // The first PDF document has loaded. WaitForNavigatorPlugins(frame); } } bool OnQuery(CefRefPtr browser, CefRefPtr frame, int64 query_id, const CefString& request, bool persistent, CefRefPtr callback) override { if (request == "placeholder_loaded") { EXPECT_FALSE(got_placeholder_loaded_); EXPECT_FALSE(got_placeholder_hidden_); got_placeholder_loaded_.yes(); // The plugin placeholder has loaded. Trigger the context menu. CefPostDelayedTask( TID_UI, base::BindOnce(&PluginTestHandler::TriggerContextMenu, this, browser), kPdfLoadDelayMs / 5); } else if (request == "placeholder_hidden") { EXPECT_TRUE(got_placeholder_loaded_); EXPECT_FALSE(got_placeholder_hidden_); got_placeholder_hidden_.yes(); if (HasContextLoad()) { // Wait for the PDF plugin to load. WaitForPluginLoad(frame); } else { // The plugin placeholder has been hidden. End the test. EndTest(); } } else { NOTREACHED(); } return true; } void TriggerContextMenu(CefRefPtr browser) { CefMouseEvent mouse_event; if (HasDirectPluginLoad()) { // Somewhere in the main PDF viewing area (avoid left preview bar). mouse_event.x = 400; mouse_event.y = 200; } else { // Somewhere in the first plugin. mouse_event.x = 100; mouse_event.y = 100; } // Send right-click mouse down and mouse up to tigger context menu. browser->GetHost()->SendMouseClickEvent(mouse_event, MBT_RIGHT, false, 1); browser->GetHost()->SendMouseClickEvent(mouse_event, MBT_RIGHT, true, 1); } bool RunContextMenu(CefRefPtr browser, CefRefPtr frame, CefRefPtr params, CefRefPtr model, CefRefPtr callback) override { EXPECT_FALSE(got_run_context_menu_); got_run_context_menu_.yes(); if (HasContextLoad() || HasContextHide()) { // Should have 4 elements -- plugin name, separator, run, hide. EXPECT_EQ(4, model->GetCount()); // Wait for the placeholder to be hidden. WaitForPlaceholderHide(); int command_id; if (HasContextLoad()) { // Execute the run command. command_id = model->GetCommandIdAt(2); } else { // Execute the hide command. command_id = model->GetCommandIdAt(3); } EXPECT_GE(command_id, MENU_ID_CUSTOM_FIRST); EXPECT_LE(command_id, MENU_ID_CUSTOM_LAST); callback->Continue(command_id, static_cast(0)); } else { // Do nothing with the context menu. callback->Cancel(); } return true; } void OnContextMenuDismissed(CefRefPtr browser, CefRefPtr frame) override { EXPECT_FALSE(got_context_menu_dismissed_); got_context_menu_dismissed_.yes(); if (HasContextHide()) { // Nothing to do here. The test will continue from OnQuery. // TODO: Once block-then-load is working this should also be the case for // HasContextLoad(). return; } EndTest(); } void DestroyTest() override { if (context_handler_.get()) { context_handler_->Detach(); context_handler_ = nullptr; } if (HasContextLoad()) { EXPECT_TRUE(got_placeholder_loaded_); // TODO: Once block-then-load is working this value should be true. EXPECT_FALSE(got_placeholder_hidden_); } else if (HasContextHide()) { EXPECT_TRUE(got_placeholder_loaded_); EXPECT_TRUE(got_placeholder_hidden_); } else { EXPECT_FALSE(got_placeholder_loaded_); EXPECT_FALSE(got_placeholder_hidden_); } EXPECT_FALSE(placeholder_frame_); if (HasRequestContextHandler()) EXPECT_TRUE(got_on_before_plugin_empty_origin_); else EXPECT_FALSE(got_on_before_plugin_empty_origin_); if (HasNoList()) { EXPECT_TRUE(got_on_before_plugin_empty_origin2_); EXPECT_FALSE(got_run_context_menu_); EXPECT_FALSE(got_context_menu_dismissed_); } else { EXPECT_FALSE(got_on_before_plugin_empty_origin2_); EXPECT_TRUE(got_run_context_menu_); EXPECT_TRUE(got_context_menu_dismissed_); } if (url_ == kPdfHtmlUrl) { // The HTML file will load the PDF twice in iframes. EXPECT_TRUE(got_on_load_end_html_); if (!HasNoList()) { EXPECT_TRUE(got_on_load_end_pdf1_); EXPECT_TRUE(got_on_load_end_pdf2_); if (HasRequestContextHandler()) { EXPECT_TRUE(got_on_before_plugin_load_pdf_); } } } else if (url_ == kPdfDirectUrl) { // Load the PDF file directly. EXPECT_FALSE(got_on_load_end_html_); EXPECT_TRUE(got_on_load_end_pdf1_); EXPECT_FALSE(got_on_load_end_pdf2_); if (HasRequestContextHandler()) { EXPECT_TRUE(got_on_before_plugin_load_pdf_); } } else { NOTREACHED(); } if (!HasRequestContextHandler() || HasNoList()) { EXPECT_FALSE(got_on_before_plugin_load_pdf_); } TestHandler::DestroyTest(); } const Mode mode_; const std::string url_; CefRefPtr placeholder_frame_; TrackCallback got_on_before_plugin_empty_origin_; TrackCallback got_on_before_plugin_empty_origin2_; TrackCallback got_on_before_plugin_load_pdf_; TrackCallback got_on_load_end_html_; TrackCallback got_on_load_end_pdf1_; TrackCallback got_on_load_end_pdf2_; TrackCallback got_placeholder_loaded_; TrackCallback got_placeholder_hidden_; TrackCallback got_run_context_menu_; TrackCallback got_context_menu_dismissed_; CefRefPtr context_handler_; IMPLEMENT_REFCOUNTING(PluginTestHandler); }; } // namespace #define RUN_TEST(name, type, url) \ TEST(PluginTest, name) { \ CefRefPtr handler = \ new PluginTestHandler(PluginTestHandler::type, url); \ handler->ExecuteTest(); \ ReleaseAndWaitForDestructor(handler); \ } RUN_TEST(GlobalDefaultPdfDirect, GLOBAL_DEFAULT, kPdfDirectUrl) RUN_TEST(GlobalDefaultPdfHtml, GLOBAL_DEFAULT, kPdfHtmlUrl) RUN_TEST(GlobalNoHandlerPdfDirect, GLOBAL_NO_HANDLER, kPdfDirectUrl) RUN_TEST(GlobalNoHandlerPdfHtml, GLOBAL_NO_HANDLER, kPdfHtmlUrl) RUN_TEST(GlobalAllowPdfDirect, GLOBAL_ALLOW, kPdfDirectUrl) RUN_TEST(GlobalAllowPdfHtml, GLOBAL_ALLOW, kPdfHtmlUrl) RUN_TEST(GlobalBlockThenLoadPdfDirect, GLOBAL_BLOCK_LOAD, kPdfDirectUrl) RUN_TEST(GlobalBlockThenLoadPdfHtml, GLOBAL_BLOCK_LOAD, kPdfHtmlUrl) RUN_TEST(GlobalBlockThenHidePdfDirect, GLOBAL_BLOCK_HIDE, kPdfDirectUrl) RUN_TEST(GlobalBlockThenHidePdfHtml, GLOBAL_BLOCK_HIDE, kPdfHtmlUrl) RUN_TEST(GlobalDisableThenHidePdfDirect, GLOBAL_DISABLE_HIDE, kPdfDirectUrl) RUN_TEST(GlobalDisableThenHidePdfHtml, GLOBAL_DISABLE_HIDE, kPdfHtmlUrl) RUN_TEST(GlobalNoListHtml, GLOBAL_NO_LIST, kPdfHtmlUrl) RUN_TEST(CustomNoHandlerPdfDirect, CUSTOM_NO_HANDLER, kPdfDirectUrl) RUN_TEST(CustomNoHandlerPdfHtml, CUSTOM_NO_HANDLER, kPdfHtmlUrl) RUN_TEST(CustomAllowPdfDirect, CUSTOM_ALLOW, kPdfDirectUrl) RUN_TEST(CustomAllowPdfHtml, CUSTOM_ALLOW, kPdfHtmlUrl) RUN_TEST(CustomBlockThenLoadPdfDirect, CUSTOM_BLOCK_LOAD, kPdfDirectUrl) RUN_TEST(CustomBlockThenLoadPdfHtml, CUSTOM_BLOCK_LOAD, kPdfHtmlUrl) RUN_TEST(CustomBlockThenHidePdfDirect, CUSTOM_BLOCK_HIDE, kPdfDirectUrl) RUN_TEST(CustomBlockThenHidePdfHtml, CUSTOM_BLOCK_HIDE, kPdfHtmlUrl) RUN_TEST(CustomDisableThenHidePdfDirect, CUSTOM_DISABLE_HIDE, kPdfDirectUrl) RUN_TEST(CustomDisableThenHidePdfHtml, CUSTOM_DISABLE_HIDE, kPdfHtmlUrl) RUN_TEST(CustomNoListHtml, CUSTOM_NO_LIST, kPdfHtmlUrl) // Entry point for creating plugin browser test objects. // Called from client_app_delegates.cc. void CreatePluginBrowserTests( client::ClientAppBrowser::DelegateSet& delegates) { delegates.insert(new PluginBrowserTest); }