// Copyright 2014 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 #include #include #include "base/files/file_path.h" #include "base/macros.h" #include "base/memory/scoped_ptr.h" #include "base/strings/stringprintf.h" #include "chrome/browser/extensions/extension_action.h" #include "chrome/browser/extensions/extension_action_runner.h" #include "chrome/browser/extensions/extension_browsertest.h" #include "chrome/browser/extensions/extension_util.h" #include "chrome/browser/extensions/tab_helper.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/test/base/ui_test_utils.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/web_contents.h" #include "content/public/test/browser_test_utils.h" #include "extensions/common/feature_switch.h" #include "extensions/common/switches.h" #include "extensions/test/extension_test_message_listener.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "testing/gtest/include/gtest/gtest.h" namespace extensions { namespace { const char kAllHostsScheme[] = "*://*/*"; const char kExplicitHostsScheme[] = "http://127.0.0.1/*"; const char kBackgroundScript[] = "\"background\": {\"scripts\": [\"script.js\"]}"; const char kBackgroundScriptSource[] = "var listener = function(tabId) {\n" " chrome.tabs.onUpdated.removeListener(listener);\n" " chrome.tabs.executeScript(tabId, {\n" " code: \"chrome.test.sendMessage('inject succeeded');\"\n" " });" "};\n" "chrome.tabs.onUpdated.addListener(listener);"; const char kContentScriptSource[] = "chrome.test.sendMessage('inject succeeded');"; const char kInjectSucceeded[] = "inject succeeded"; enum InjectionType { CONTENT_SCRIPT, EXECUTE_SCRIPT }; enum HostType { ALL_HOSTS, EXPLICIT_HOSTS }; enum RequiresConsent { REQUIRES_CONSENT, DOES_NOT_REQUIRE_CONSENT }; // Runs all pending tasks in the renderer associated with |web_contents|. // Returns true on success. bool RunAllPendingInRenderer(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). return content::ExecuteScript(web_contents, "1 == 1;"); } // For use with blocked actions browsertests that put the result in // window.localStorage. Returns the result or "undefined" if the result is not // set. std::string GetValue(content::WebContents* web_contents) { std::string out; if (!content::ExecuteScriptAndExtractString( web_contents, "var res = window.localStorage.getItem('extResult') || 'undefined';" "window.localStorage.removeItem('extResult');" "window.domAutomationController.send(res);", &out)) { out = "Failed to inject script"; } return out; } } // namespace class ExtensionActionRunnerBrowserTest : public ExtensionBrowserTest { public: ExtensionActionRunnerBrowserTest() {} void SetUpCommandLine(base::CommandLine* command_line) override; void TearDownOnMainThread() override; // Returns an extension with the given |host_type| and |injection_type|. If // one already exists, the existing extension will be returned. Othewrwise, // one will be created. // This could potentially return NULL if LoadExtension() fails. const Extension* CreateExtension(HostType host_type, InjectionType injection_type); private: std::vector> test_extension_dirs_; std::vector extensions_; }; void ExtensionActionRunnerBrowserTest::SetUpCommandLine( base::CommandLine* command_line) { ExtensionBrowserTest::SetUpCommandLine(command_line); // We append the actual switch to the commandline because it needs to be // passed over to the renderer, which a FeatureSwitch::ScopedOverride will // not do. command_line->AppendSwitch(switches::kEnableScriptsRequireAction); } void ExtensionActionRunnerBrowserTest::TearDownOnMainThread() { test_extension_dirs_.clear(); } const Extension* ExtensionActionRunnerBrowserTest::CreateExtension( HostType host_type, InjectionType injection_type) { std::string name = base::StringPrintf( "%s %s", injection_type == CONTENT_SCRIPT ? "content_script" : "execute_script", host_type == ALL_HOSTS ? "all_hosts" : "explicit_hosts"); const char* const permission_scheme = host_type == ALL_HOSTS ? kAllHostsScheme : kExplicitHostsScheme; std::string permissions = base::StringPrintf( "\"permissions\": [\"tabs\", \"%s\"]", permission_scheme); std::string scripts; std::string script_source; if (injection_type == CONTENT_SCRIPT) { scripts = base::StringPrintf( "\"content_scripts\": [" " {" " \"matches\": [\"%s\"]," " \"js\": [\"script.js\"]," " \"run_at\": \"document_end\"" " }" "]", permission_scheme); } else { scripts = kBackgroundScript; } std::string manifest = base::StringPrintf( "{" " \"name\": \"%s\"," " \"version\": \"1.0\"," " \"manifest_version\": 2," " %s," " %s" "}", name.c_str(), permissions.c_str(), scripts.c_str()); scoped_ptr dir(new TestExtensionDir); dir->WriteManifest(manifest); dir->WriteFile(FILE_PATH_LITERAL("script.js"), injection_type == CONTENT_SCRIPT ? kContentScriptSource : kBackgroundScriptSource); const Extension* extension = LoadExtension(dir->unpacked_path()); if (extension) { test_extension_dirs_.push_back(std::move(dir)); extensions_.push_back(extension); } // If extension is NULL here, it will be caught later in the test. return extension; } class ActiveScriptTester { public: ActiveScriptTester(const std::string& name, const Extension* extension, Browser* browser, RequiresConsent requires_consent, InjectionType type); ~ActiveScriptTester(); testing::AssertionResult Verify(); private: // Returns the ExtensionActionRunner, or null if one does not exist. ExtensionActionRunner* GetExtensionActionRunner(); // Returns true if the extension has a pending request with the // ExtensionActionRunner. bool WantsToRun(); // The name of the extension, and also the message it sends. std::string name_; // The extension associated with this tester. const Extension* extension_; // The browser the tester is running in. Browser* browser_; // Whether or not the extension has permission to run the script without // asking the user. RequiresConsent requires_consent_; // The type of injection this tester uses. InjectionType type_; // All of these extensions should inject a script (either through content // scripts or through chrome.tabs.executeScript()) that sends a message with // the |kInjectSucceeded| message. linked_ptr inject_success_listener_; }; ActiveScriptTester::ActiveScriptTester(const std::string& name, const Extension* extension, Browser* browser, RequiresConsent requires_consent, InjectionType type) : name_(name), extension_(extension), browser_(browser), requires_consent_(requires_consent), type_(type), inject_success_listener_( new ExtensionTestMessageListener(kInjectSucceeded, false /* won't reply */)) { inject_success_listener_->set_extension_id(extension->id()); } ActiveScriptTester::~ActiveScriptTester() {} testing::AssertionResult ActiveScriptTester::Verify() { if (!extension_) return testing::AssertionFailure() << "Could not load extension: " << name_; content::WebContents* web_contents = browser_ ? browser_->tab_strip_model()->GetActiveWebContents() : NULL; if (!web_contents) return testing::AssertionFailure() << "No web contents."; // Give the extension plenty of time to inject. if (!RunAllPendingInRenderer(web_contents)) return testing::AssertionFailure() << "Could not run pending in renderer."; // Make sure all running tasks are complete. content::RunAllPendingInMessageLoop(); ExtensionActionRunner* runner = GetExtensionActionRunner(); if (!runner) return testing::AssertionFailure() << "Could not find runner."; bool wants_to_run = WantsToRun(); // An extension should have an action displayed iff it requires user consent. if ((requires_consent_ == REQUIRES_CONSENT && !wants_to_run) || (requires_consent_ == DOES_NOT_REQUIRE_CONSENT && wants_to_run)) { return testing::AssertionFailure() << "Improper wants to run for " << name_ << ": expected " << (requires_consent_ == REQUIRES_CONSENT) << ", found " << wants_to_run; } // If the extension has permission, we should be able to simply wait for it // to execute. if (requires_consent_ == DOES_NOT_REQUIRE_CONSENT) { inject_success_listener_->WaitUntilSatisfied(); return testing::AssertionSuccess(); } // Otherwise, we don't have permission, and have to grant it. Ensure the // script has *not* already executed. if (inject_success_listener_->was_satisfied()) { return testing::AssertionFailure() << name_ << "'s script ran without permission."; } // If we reach this point, we should always want to run. DCHECK(wants_to_run); // Grant permission by clicking on the extension action. runner->RunAction(extension_, true); // Now, the extension should be able to inject the script. inject_success_listener_->WaitUntilSatisfied(); // The extension should no longer want to run. wants_to_run = WantsToRun(); if (wants_to_run) { return testing::AssertionFailure() << "Extension " << name_ << " still wants to run after injecting."; } return testing::AssertionSuccess(); } ExtensionActionRunner* ActiveScriptTester::GetExtensionActionRunner() { return ExtensionActionRunner::GetForWebContents( browser_ ? browser_->tab_strip_model()->GetActiveWebContents() : nullptr); } bool ActiveScriptTester::WantsToRun() { ExtensionActionRunner* runner = GetExtensionActionRunner(); return runner ? runner->WantsToRun(extension_) : false; } IN_PROC_BROWSER_TEST_F(ExtensionActionRunnerBrowserTest, ActiveScriptsAreDisplayedAndDelayExecution) { base::FilePath active_script_path = test_data_dir_.AppendASCII("active_script"); const char* const kExtensionNames[] = { "inject_scripts_all_hosts", "inject_scripts_explicit_hosts", "content_scripts_all_hosts", "content_scripts_explicit_hosts"}; // First, we load up three extensions: // - An extension that injects scripts into all hosts, // - An extension that injects scripts into explicit hosts, // - An extension with a content script that runs on all hosts, // - An extension with a content script that runs on explicit hosts. // The extensions that operate on explicit hosts have permission; the ones // that request all hosts require user consent. ActiveScriptTester testers[] = { ActiveScriptTester(kExtensionNames[0], CreateExtension(ALL_HOSTS, EXECUTE_SCRIPT), browser(), REQUIRES_CONSENT, EXECUTE_SCRIPT), ActiveScriptTester(kExtensionNames[1], CreateExtension(EXPLICIT_HOSTS, EXECUTE_SCRIPT), browser(), DOES_NOT_REQUIRE_CONSENT, EXECUTE_SCRIPT), ActiveScriptTester(kExtensionNames[2], CreateExtension(ALL_HOSTS, CONTENT_SCRIPT), browser(), REQUIRES_CONSENT, CONTENT_SCRIPT), ActiveScriptTester(kExtensionNames[3], CreateExtension(EXPLICIT_HOSTS, CONTENT_SCRIPT), browser(), DOES_NOT_REQUIRE_CONSENT, CONTENT_SCRIPT), }; // Navigate to an URL (which matches the explicit host specified in the // extension content_scripts_explicit_hosts). All four extensions should // inject the script. ASSERT_TRUE(embedded_test_server()->Start()); ui_test_utils::NavigateToURL( browser(), embedded_test_server()->GetURL("/extensions/test_file.html")); for (size_t i = 0u; i < arraysize(testers); ++i) EXPECT_TRUE(testers[i].Verify()) << kExtensionNames[i]; } // Test that removing an extension with pending injections a) removes the // pending injections for that extension, and b) does not affect pending // injections for other extensions. IN_PROC_BROWSER_TEST_F(ExtensionActionRunnerBrowserTest, RemoveExtensionWithPendingInjections) { // Load up two extensions, each with content scripts. const Extension* extension1 = CreateExtension(ALL_HOSTS, CONTENT_SCRIPT); ASSERT_TRUE(extension1); const Extension* extension2 = CreateExtension(ALL_HOSTS, CONTENT_SCRIPT); ASSERT_TRUE(extension2); ASSERT_NE(extension1->id(), extension2->id()); content::WebContents* web_contents = browser()->tab_strip_model()->GetActiveWebContents(); ASSERT_TRUE(web_contents); ExtensionActionRunner* action_runner = ExtensionActionRunner::GetForWebContents(web_contents); ASSERT_TRUE(action_runner); ASSERT_TRUE(embedded_test_server()->Start()); ui_test_utils::NavigateToURL( browser(), embedded_test_server()->GetURL("/extensions/test_file.html")); // Both extensions should have pending requests. EXPECT_TRUE(action_runner->WantsToRun(extension1)); EXPECT_TRUE(action_runner->WantsToRun(extension2)); // Unload one of the extensions. UnloadExtension(extension2->id()); EXPECT_TRUE(RunAllPendingInRenderer(web_contents)); // We should have pending requests for extension1, but not the removed // extension2. EXPECT_TRUE(action_runner->WantsToRun(extension1)); EXPECT_FALSE(action_runner->WantsToRun(extension2)); // We should still be able to run the request for extension1. ExtensionTestMessageListener inject_success_listener( new ExtensionTestMessageListener(kInjectSucceeded, false /* won't reply */)); inject_success_listener.set_extension_id(extension1->id()); action_runner->RunAction(extension1, true); inject_success_listener.WaitUntilSatisfied(); } // Test that granting the extension all urls permission allows it to run on // pages, and that the permission update is sent to existing renderers. IN_PROC_BROWSER_TEST_F(ExtensionActionRunnerBrowserTest, GrantExtensionAllUrlsPermission) { // Loadup an extension and navigate. const Extension* extension = CreateExtension(ALL_HOSTS, CONTENT_SCRIPT); ASSERT_TRUE(extension); content::WebContents* web_contents = browser()->tab_strip_model()->GetActiveWebContents(); ASSERT_TRUE(web_contents); ExtensionActionRunner* action_runner = ExtensionActionRunner::GetForWebContents(web_contents); ASSERT_TRUE(action_runner); ExtensionTestMessageListener inject_success_listener( new ExtensionTestMessageListener(kInjectSucceeded, false /* won't reply */)); inject_success_listener.set_extension_id(extension->id()); ASSERT_TRUE(embedded_test_server()->Start()); GURL url = embedded_test_server()->GetURL("/extensions/test_file.html"); ui_test_utils::NavigateToURL(browser(), url); // The extension shouldn't be allowed to run. EXPECT_TRUE(action_runner->WantsToRun(extension)); EXPECT_EQ(1, action_runner->num_page_requests()); EXPECT_FALSE(inject_success_listener.was_satisfied()); // Enable the extension to run on all urls. util::SetAllowedScriptingOnAllUrls(extension->id(), profile(), true); EXPECT_TRUE(RunAllPendingInRenderer(web_contents)); // Navigate again - this time, the extension should execute immediately (and // should not need to ask the script controller for permission). ui_test_utils::NavigateToURL(browser(), url); EXPECT_FALSE(action_runner->WantsToRun(extension)); EXPECT_EQ(0, action_runner->num_page_requests()); EXPECT_TRUE(inject_success_listener.WaitUntilSatisfied()); // Revoke all urls permissions. inject_success_listener.Reset(); util::SetAllowedScriptingOnAllUrls(extension->id(), profile(), false); EXPECT_TRUE(RunAllPendingInRenderer(web_contents)); // Re-navigate; the extension should again need permission to run. ui_test_utils::NavigateToURL(browser(), url); EXPECT_TRUE(action_runner->WantsToRun(extension)); EXPECT_EQ(1, action_runner->num_page_requests()); EXPECT_FALSE(inject_success_listener.was_satisfied()); } IN_PROC_BROWSER_TEST_F(ExtensionActionRunnerBrowserTest, BlockedActionBrowserTest) { // Load an extension that wants to run on every page at document start, and // load a test page. ASSERT_TRUE(embedded_test_server()->Start()); const GURL url = embedded_test_server()->GetURL("/simple.html"); const Extension* extension = LoadExtension( test_data_dir_.AppendASCII("blocked_actions/content_scripts")); ASSERT_TRUE(extension); ui_test_utils::NavigateToURL(browser(), url); content::WebContents* web_contents = browser()->tab_strip_model()->GetActiveWebContents(); EXPECT_TRUE(content::WaitForLoadStop(web_contents)); // The extension should want to run on the page, and should not have // injected. ExtensionActionRunner* runner = ExtensionActionRunner::GetForWebContents(web_contents); ASSERT_TRUE(runner); EXPECT_TRUE(runner->WantsToRun(extension)); EXPECT_EQ("undefined", GetValue(web_contents)); // Wire up the runner to automatically accept the bubble to prompt for page // refresh. runner->set_default_bubble_close_action_for_testing( make_scoped_ptr(new ToolbarActionsBarBubbleDelegate::CloseAction( ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE))); content::NavigationEntry* entry = web_contents->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); const int first_nav_id = entry->GetUniqueID(); // Run the extension action, which should cause a page refresh (since we // automatically accepted the bubble prompting us), and the extension should // have injected at document start. runner->RunAction(extension, true); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(content::WaitForLoadStop(web_contents)); entry = web_contents->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); // Confirm that we refreshed the page. EXPECT_GE(entry->GetUniqueID(), first_nav_id); EXPECT_EQ("success", GetValue(web_contents)); EXPECT_FALSE(runner->WantsToRun(extension)); // Revoke permission and reload to try different bubble options. ActiveTabPermissionGranter* active_tab_granter = TabHelper::FromWebContents(web_contents)->active_tab_permission_granter(); ASSERT_TRUE(active_tab_granter); active_tab_granter->RevokeForTesting(); web_contents->GetController().Reload(true); EXPECT_TRUE(content::WaitForLoadStop(web_contents)); // The extension should again want to run. Automatically dismiss the bubble // that pops up prompting for page refresh. EXPECT_TRUE(runner->WantsToRun(extension)); EXPECT_EQ("undefined", GetValue(web_contents)); const int next_nav_id = web_contents->GetController().GetLastCommittedEntry()->GetUniqueID(); runner->set_default_bubble_close_action_for_testing( make_scoped_ptr(new ToolbarActionsBarBubbleDelegate::CloseAction( ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS_USER_ACTION))); // Try running the extension. Nothing should happen, because the user // didn't agree to refresh the page. The extension should still want to run. runner->RunAction(extension, true); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(content::WaitForLoadStop(web_contents)); EXPECT_EQ("undefined", GetValue(web_contents)); EXPECT_EQ( next_nav_id, web_contents->GetController().GetLastCommittedEntry()->GetUniqueID()); // Repeat with a dismissal from bubble deactivation - same story. runner->set_default_bubble_close_action_for_testing( make_scoped_ptr(new ToolbarActionsBarBubbleDelegate::CloseAction( ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS_DEACTIVATION))); runner->RunAction(extension, true); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(content::WaitForLoadStop(web_contents)); EXPECT_EQ("undefined", GetValue(web_contents)); EXPECT_EQ( next_nav_id, web_contents->GetController().GetLastCommittedEntry()->GetUniqueID()); } // A version of the test with the flag off, in order to test that everything // still works as expected. class FlagOffExtensionActionRunnerBrowserTest : public ExtensionActionRunnerBrowserTest { private: // Simply don't append the flag. void SetUpCommandLine(base::CommandLine* command_line) override { ExtensionBrowserTest::SetUpCommandLine(command_line); } }; IN_PROC_BROWSER_TEST_F(FlagOffExtensionActionRunnerBrowserTest, ScriptsExecuteWhenFlagAbsent) { const char* const kExtensionNames[] = { "content_scripts_all_hosts", "inject_scripts_all_hosts", }; ActiveScriptTester testers[] = { ActiveScriptTester(kExtensionNames[0], CreateExtension(ALL_HOSTS, CONTENT_SCRIPT), browser(), DOES_NOT_REQUIRE_CONSENT, CONTENT_SCRIPT), ActiveScriptTester(kExtensionNames[1], CreateExtension(ALL_HOSTS, EXECUTE_SCRIPT), browser(), DOES_NOT_REQUIRE_CONSENT, EXECUTE_SCRIPT), }; ASSERT_TRUE(embedded_test_server()->Start()); ui_test_utils::NavigateToURL( browser(), embedded_test_server()->GetURL("/extensions/test_file.html")); for (size_t i = 0u; i < arraysize(testers); ++i) EXPECT_TRUE(testers[i].Verify()) << kExtensionNames[i]; } } // namespace extensions