// 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 "base/bind.h" #include "base/callback.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/extensions/api/permissions/permissions_api.h" #include "chrome/browser/extensions/extension_apitest.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/test_extension_dir.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/common/chrome_switches.h" #include "chrome/test/base/ui_test_utils.h" #include "components/app_modal/javascript_dialog_extensions_client.h" #include "components/app_modal/javascript_dialog_manager.h" #include "content/public/browser/javascript_dialog_manager.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/test/browser_test_utils.h" #include "extensions/browser/notification_types.h" #include "extensions/common/extension.h" #include "extensions/test/extension_test_message_listener.h" #include "extensions/test/result_catcher.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "url/gurl.h" namespace extensions { namespace { // A fake webstore domain. const char kWebstoreDomain[] = "cws.com"; // Check whether or not style was injected, with |expected_injection| indicating // the expected result. Also ensure that no CSS was added to the // document.styleSheets array. testing::AssertionResult CheckStyleInjection(Browser* browser, const GURL& url, bool expected_injection) { ui_test_utils::NavigateToURL(browser, url); bool css_injected = false; if (!content::ExecuteScriptAndExtractBool( browser->tab_strip_model()->GetActiveWebContents(), "window.domAutomationController.send(" " document.defaultView.getComputedStyle(document.body, null)." " getPropertyValue('display') == 'none');", &css_injected)) { return testing::AssertionFailure() << "Failed to execute script and extract bool for injection status."; } if (css_injected != expected_injection) { std::string message; if (css_injected) message = "CSS injected when no injection was expected."; else message = "CSS not injected when injection was expected."; return testing::AssertionFailure() << message; } bool css_doesnt_add_to_list = false; if (!content::ExecuteScriptAndExtractBool( browser->tab_strip_model()->GetActiveWebContents(), "window.domAutomationController.send(" " document.styleSheets.length == 0);", &css_doesnt_add_to_list)) { return testing::AssertionFailure() << "Failed to execute script and extract bool for stylesheets length."; } if (!css_doesnt_add_to_list) { return testing::AssertionFailure() << "CSS injection added to number of stylesheets."; } return testing::AssertionSuccess(); } class DialogClient; // A helper class to hijack the dialog manager's ExtensionsClient, so that we // know when dialogs are being opened. // NOTE: The default implementation of the JavaScriptDialogExtensionsClient // doesn't do anything, so it's safe to override it. If, at some stage, this // has behavior (like if we move this into app shell), we'll need to update // this (by, e.g., making DialogClient a wrapper around the implementation). class DialogHelper { public: explicit DialogHelper(content::WebContents* web_contents); ~DialogHelper(); // Notifies the DialogHelper that a dialog was opened. Runs |quit_closure_|, // if it is non-null. void DialogOpened(); // Closes any active dialogs. void CloseDialogs(); void set_quit_closure(const base::Closure& quit_closure) { quit_closure_ = quit_closure; } size_t dialog_count() const { return dialog_count_; } private: // The number of dialogs to appear. size_t dialog_count_; // The WebContents this helper is associated with. content::WebContents* web_contents_; // The dialog manager for |web_contents_|. content::JavaScriptDialogManager* dialog_manager_; // The dialog client override. DialogClient* client_; // The quit closure to run when a dialog appears. base::Closure quit_closure_; DISALLOW_COPY_AND_ASSIGN(DialogHelper); }; // The client override for the DialogHelper. class DialogClient : public app_modal::JavaScriptDialogExtensionsClient { public: explicit DialogClient(DialogHelper* helper) : helper_(helper) {} ~DialogClient() override {} void set_helper(DialogHelper* helper) { helper_ = helper; } private: // app_modal::JavaScriptDialogExtensionsClient: void OnDialogOpened(content::WebContents* web_contents) override { if (helper_) helper_->DialogOpened(); } void OnDialogClosed(content::WebContents* web_contents) override {} bool GetExtensionName(content::WebContents* web_contents, const GURL& origin_url, std::string* name_out) override { return false; } // The dialog helper to notify of any open dialogs. DialogHelper* helper_; DISALLOW_COPY_AND_ASSIGN(DialogClient); }; DialogHelper::DialogHelper(content::WebContents* web_contents) : dialog_count_(0), web_contents_(web_contents), dialog_manager_(nullptr), client_(nullptr) { app_modal::JavaScriptDialogManager* dialog_manager_impl = app_modal::JavaScriptDialogManager::GetInstance(); dialog_manager_ = web_contents_->GetDelegate()->GetJavaScriptDialogManager(web_contents_); DCHECK_EQ(dialog_manager_impl, dialog_manager_); client_ = new DialogClient(this); dialog_manager_impl->SetExtensionsClient(make_scoped_ptr(client_)); } DialogHelper::~DialogHelper() { client_->set_helper(nullptr); } void DialogHelper::CloseDialogs() { dialog_manager_->CancelActiveAndPendingDialogs(web_contents_); } void DialogHelper::DialogOpened() { ++dialog_count_; if (!quit_closure_.is_null()) { quit_closure_.Run(); quit_closure_ = base::Closure(); } } // Runs all pending tasks in the renderer associated with |web_contents|, and // then all pending tasks in the browser process. // Returns true on success. bool RunAllPending(content::WebContents* web_contents) { // This is slight hack to achieve a RunPendingInRenderer() method. Since IPCs // are sent synchronously, anything started prior to this method will finish // before this method returns (as content::ExecuteScript() is synchronous). if (!content::ExecuteScript(web_contents, "1 == 1;")) return false; base::RunLoop().RunUntilIdle(); return true; } // A simple extension manifest with content scripts on all pages. const char kManifest[] = "{" " \"name\": \"%s\"," " \"version\": \"1.0\"," " \"manifest_version\": 2," " \"content_scripts\": [{" " \"matches\": [\"*://*/*\"]," " \"js\": [\"script.js\"]," " \"run_at\": \"%s\"" " }]" "}"; // A (blocking) content script that pops up an alert. const char kBlockingScript[] = "alert('ALERT');"; // A (non-blocking) content script that sends a message. const char kNonBlockingScript[] = "chrome.test.sendMessage('done');"; } // namespace IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptAllFrames) { ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest("content_scripts/all_frames")) << message_; } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptAboutBlankIframes) { ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE( RunExtensionTest("content_scripts/about_blank_iframes")) << message_; } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptAboutBlankAndSrcdoc) { // The optional "*://*/*" permission is requested after verifying that // content script insertion solely depends on content_scripts[*].matches. // The permission is needed for chrome.tabs.executeScript tests. PermissionsRequestFunction::SetAutoConfirmForTests(true); PermissionsRequestFunction::SetIgnoreUserGestureForTests(true); ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest("content_scripts/about_blank_srcdoc")) << message_; } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptExtensionIframe) { ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest("content_scripts/extension_iframe")) << message_; } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptExtensionProcess) { ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE( RunExtensionTest("content_scripts/extension_process")) << message_; } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptFragmentNavigation) { ASSERT_TRUE(StartEmbeddedTestServer()); const char extension_name[] = "content_scripts/fragment"; ASSERT_TRUE(RunExtensionTest(extension_name)) << message_; } // Times out on Linux: http://crbug.com/163097 #if defined(OS_LINUX) #define MAYBE_ContentScriptIsolatedWorlds DISABLED_ContentScriptIsolatedWorlds #else #define MAYBE_ContentScriptIsolatedWorlds ContentScriptIsolatedWorlds #endif IN_PROC_BROWSER_TEST_F(ExtensionApiTest, MAYBE_ContentScriptIsolatedWorlds) { // This extension runs various bits of script and tests that they all run in // the same isolated world. ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest("content_scripts/isolated_world1")) << message_; // Now load a different extension, inject into same page, verify worlds aren't // shared. ASSERT_TRUE(RunExtensionTest("content_scripts/isolated_world2")) << message_; } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptIgnoreHostPermissions) { host_resolver()->AddRule("a.com", "127.0.0.1"); host_resolver()->AddRule("b.com", "127.0.0.1"); ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest( "content_scripts/dont_match_host_permissions")) << message_; } // crbug.com/39249 -- content scripts js should not run on view source. IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptViewSource) { ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest("content_scripts/view_source")) << message_; } // crbug.com/126257 -- content scripts should not get injected into other // extensions. IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptOtherExtensions) { host_resolver()->AddRule("a.com", "127.0.0.1"); ASSERT_TRUE(StartEmbeddedTestServer()); // First, load extension that sets up content script. ASSERT_TRUE(RunExtensionTest("content_scripts/other_extensions/injector")) << message_; // Then load targeted extension to make sure its content isn't changed. ASSERT_TRUE(RunExtensionTest("content_scripts/other_extensions/victim")) << message_; } class ContentScriptCssInjectionTest : public ExtensionApiTest { protected: // TODO(rdevlin.cronin): Make a testing switch that looks like FeatureSwitch, // but takes in an optional value so that we don't have to do this. void SetUpCommandLine(base::CommandLine* command_line) override { ExtensionApiTest::SetUpCommandLine(command_line); // We change the Webstore URL to be http://cws.com. We need to do this so // we can check that css injection is not allowed on the webstore (which // could lead to spoofing). Unfortunately, host_resolver seems to have // problems with redirecting "chrome.google.com" to the test server, so we // can't use the real Webstore's URL. If this changes, we could clean this // up. command_line->AppendSwitchASCII( switches::kAppsGalleryURL, base::StringPrintf("http://%s", kWebstoreDomain)); } }; IN_PROC_BROWSER_TEST_F(ContentScriptCssInjectionTest, ContentScriptInjectsStyles) { ASSERT_TRUE(StartEmbeddedTestServer()); host_resolver()->AddRule(kWebstoreDomain, "127.0.0.1"); ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("content_scripts") .AppendASCII("css_injection"))); // CSS injection should be allowed on an aribitrary web page. GURL url = embedded_test_server()->GetURL("/extensions/test_file_with_body.html"); EXPECT_TRUE(CheckStyleInjection(browser(), url, true)); // The loaded extension has an exclude match for "extensions/test_file.html", // so no CSS should be injected. url = embedded_test_server()->GetURL("/extensions/test_file.html"); EXPECT_TRUE(CheckStyleInjection(browser(), url, false)); // We disallow all injection on the webstore. GURL::Replacements replacements; replacements.SetHostStr(kWebstoreDomain); url = embedded_test_server()->GetURL("/extensions/test_file_with_body.html") .ReplaceComponents(replacements); EXPECT_TRUE(CheckStyleInjection(browser(), url, false)); } // crbug.com/120762 IN_PROC_BROWSER_TEST_F( ExtensionApiTest, DISABLED_ContentScriptStylesInjectedIntoExistingRenderers) { ASSERT_TRUE(StartEmbeddedTestServer()); content::WindowedNotificationObserver signal( extensions::NOTIFICATION_USER_SCRIPTS_UPDATED, content::Source(browser()->profile())); // Start with a renderer already open at a URL. GURL url(test_server()->GetURL("file/extensions/test_file.html")); ui_test_utils::NavigateToURL(browser(), url); LoadExtension( test_data_dir_.AppendASCII("content_scripts/existing_renderers")); signal.Wait(); // And check that its styles were affected by the styles that just got loaded. bool styles_injected; ASSERT_TRUE(content::ExecuteScriptAndExtractBool( browser()->tab_strip_model()->GetActiveWebContents(), "window.domAutomationController.send(" " document.defaultView.getComputedStyle(document.body, null)." " getPropertyValue('background-color') == 'rgb(255, 0, 0)')", &styles_injected)); ASSERT_TRUE(styles_injected); } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptCSSLocalization) { ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest("content_scripts/css_l10n")) << message_; } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptExtensionAPIs) { ASSERT_TRUE(StartEmbeddedTestServer()); const extensions::Extension* extension = LoadExtension( test_data_dir_.AppendASCII("content_scripts/extension_api")); ResultCatcher catcher; ui_test_utils::NavigateToURL( browser(), embedded_test_server()->GetURL( "/extensions/api_test/content_scripts/extension_api/functions.html")); EXPECT_TRUE(catcher.GetNextResult()); // Navigate to a page that will cause a content script to run that starts // listening for an extension event. ui_test_utils::NavigateToURL( browser(), embedded_test_server()->GetURL( "/extensions/api_test/content_scripts/extension_api/events.html")); // Navigate to an extension page that will fire the event events.js is // listening for. ui_test_utils::NavigateToURLWithDisposition( browser(), extension->GetResourceURL("fire_event.html"), NEW_FOREGROUND_TAB, ui_test_utils::BROWSER_TEST_NONE); EXPECT_TRUE(catcher.GetNextResult()); } // Flaky on Windows. http://crbug.com/248418 #if defined(OS_WIN) #define MAYBE_ContentScriptPermissionsApi DISABLED_ContentScriptPermissionsApi #else #define MAYBE_ContentScriptPermissionsApi ContentScriptPermissionsApi #endif IN_PROC_BROWSER_TEST_F(ExtensionApiTest, MAYBE_ContentScriptPermissionsApi) { extensions::PermissionsRequestFunction::SetIgnoreUserGestureForTests(true); extensions::PermissionsRequestFunction::SetAutoConfirmForTests(true); host_resolver()->AddRule("*.com", "127.0.0.1"); ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest("content_scripts/permissions")) << message_; } IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptBypassPageCSP) { ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(RunExtensionTest("content_scripts/bypass_page_csp")) << message_; } // Test that when injecting a blocking content script, other scripts don't run // until the blocking script finishes. IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptBlockingScript) { ASSERT_TRUE(StartEmbeddedTestServer()); // Load up two extensions. TestExtensionDir ext_dir1; ext_dir1.WriteManifest( base::StringPrintf(kManifest, "ext1", "document_start")); ext_dir1.WriteFile(FILE_PATH_LITERAL("script.js"), kBlockingScript); const Extension* ext1 = LoadExtension(ext_dir1.unpacked_path()); ASSERT_TRUE(ext1); TestExtensionDir ext_dir2; ext_dir2.WriteManifest(base::StringPrintf(kManifest, "ext2", "document_end")); ext_dir2.WriteFile(FILE_PATH_LITERAL("script.js"), kNonBlockingScript); const Extension* ext2 = LoadExtension(ext_dir2.unpacked_path()); ASSERT_TRUE(ext2); content::WebContents* web_contents = browser()->tab_strip_model()->GetActiveWebContents(); DialogHelper dialog_helper(web_contents); base::RunLoop run_loop; dialog_helper.set_quit_closure(run_loop.QuitClosure()); ExtensionTestMessageListener listener("done", false); listener.set_extension_id(ext2->id()); // Navigate! Both extensions will try to inject. ui_test_utils::NavigateToURLWithDisposition( browser(), embedded_test_server()->GetURL("/empty.html"), CURRENT_TAB, ui_test_utils::BROWSER_TEST_NONE); run_loop.Run(); // Right now, the alert dialog is showing and blocking injection of anything // after it, so the listener shouldn't be satisfied. EXPECT_TRUE(RunAllPending(web_contents)); EXPECT_FALSE(listener.was_satisfied()); EXPECT_EQ(1u, dialog_helper.dialog_count()); dialog_helper.CloseDialogs(); // After closing the dialog, the rest of the scripts should be able to // inject. EXPECT_TRUE(listener.WaitUntilSatisfied()); } // Test that closing a tab with a blocking script results in no further scripts // running (and we don't crash). IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptBlockingScriptTabClosed) { ASSERT_TRUE(StartEmbeddedTestServer()); // We're going to close a tab in this test, so make a new one (to ensure // we don't close the browser). ui_test_utils::NavigateToURLWithDisposition( browser(), embedded_test_server()->GetURL("/empty.html"), NEW_FOREGROUND_TAB, ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); // Set up the same as the previous test case. TestExtensionDir ext_dir1; ext_dir1.WriteManifest( base::StringPrintf(kManifest, "ext1", "document_start")); ext_dir1.WriteFile(FILE_PATH_LITERAL("script.js"), kBlockingScript); const Extension* ext1 = LoadExtension(ext_dir1.unpacked_path()); ASSERT_TRUE(ext1); TestExtensionDir ext_dir2; ext_dir2.WriteManifest(base::StringPrintf(kManifest, "ext2", "document_end")); ext_dir2.WriteFile(FILE_PATH_LITERAL("script.js"), kNonBlockingScript); const Extension* ext2 = LoadExtension(ext_dir2.unpacked_path()); ASSERT_TRUE(ext2); content::WebContents* web_contents = browser()->tab_strip_model()->GetActiveWebContents(); DialogHelper dialog_helper(web_contents); base::RunLoop run_loop; dialog_helper.set_quit_closure(run_loop.QuitClosure()); ExtensionTestMessageListener listener("done", false); listener.set_extension_id(ext2->id()); // Navitate! ui_test_utils::NavigateToURLWithDisposition( browser(), embedded_test_server()->GetURL("/empty.html"), CURRENT_TAB, ui_test_utils::BROWSER_TEST_NONE); // Now, instead of closing the dialog, just close the tab. Later scripts // should never get a chance to run (and we shouldn't crash). run_loop.Run(); EXPECT_TRUE(RunAllPending(web_contents)); EXPECT_FALSE(listener.was_satisfied()); EXPECT_TRUE(browser()->tab_strip_model()->CloseWebContentsAt( browser()->tab_strip_model()->active_index(), 0)); EXPECT_FALSE(listener.was_satisfied()); } // There was a bug by which content scripts that blocked and ran on // document_idle could be injected twice (crbug.com/431263). Test for // regression. IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContentScriptBlockingScriptsDontRunTwice) { ASSERT_TRUE(StartEmbeddedTestServer()); // Load up an extension. TestExtensionDir ext_dir1; ext_dir1.WriteManifest( base::StringPrintf(kManifest, "ext1", "document_idle")); ext_dir1.WriteFile(FILE_PATH_LITERAL("script.js"), kBlockingScript); const Extension* ext1 = LoadExtension(ext_dir1.unpacked_path()); ASSERT_TRUE(ext1); content::WebContents* web_contents = browser()->tab_strip_model()->GetActiveWebContents(); DialogHelper dialog_helper(web_contents); base::RunLoop run_loop; dialog_helper.set_quit_closure(run_loop.QuitClosure()); // Navigate! ui_test_utils::NavigateToURLWithDisposition( browser(), embedded_test_server()->GetURL("/empty.html"), CURRENT_TAB, ui_test_utils::BROWSER_TEST_NONE); run_loop.Run(); // The extension will have injected at idle, but it should only inject once. EXPECT_TRUE(RunAllPending(web_contents)); EXPECT_EQ(1u, dialog_helper.dialog_count()); dialog_helper.CloseDialogs(); EXPECT_TRUE(RunAllPending(web_contents)); EXPECT_EQ(1u, dialog_helper.dialog_count()); } } // namespace extensions