|  | // Copyright 2020 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include <optional> | 
|  |  | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "base/test/bind.h" | 
|  | #include "build/build_config.h" | 
|  | #include "chrome/browser/extensions/api/scripting/scripting_api.h" | 
|  | #include "chrome/browser/extensions/extension_apitest.h" | 
|  | #include "chrome/browser/extensions/extension_tab_util.h" | 
|  | #include "chrome/browser/extensions/extension_util.h" | 
|  | #include "chrome/browser/profiles/profile.h" | 
|  | #include "chrome/browser/ssl/https_upgrades_util.h" | 
|  | #include "components/version_info/channel.h" | 
|  | #include "content/public/browser/web_contents.h" | 
|  | #include "content/public/test/browser_test.h" | 
|  | #include "content/public/test/browser_test_utils.h" | 
|  | #include "content/public/test/prerender_test_util.h" | 
|  | #include "content/public/test/test_navigation_observer.h" | 
|  | #include "content/public/test/test_utils.h" | 
|  | #include "extensions/browser/api_test_utils.h" | 
|  | #include "extensions/browser/background_script_executor.h" | 
|  | #include "extensions/browser/disable_reason.h" | 
|  | #include "extensions/browser/extension_registrar.h" | 
|  | #include "extensions/browser/extension_registry.h" | 
|  | #include "extensions/browser/script_executor.h" | 
|  | #include "extensions/buildflags/buildflags.h" | 
|  | #include "extensions/common/features/feature_channel.h" | 
|  | #include "extensions/common/utils/content_script_utils.h" | 
|  | #include "extensions/test/extension_test_message_listener.h" | 
|  | #include "extensions/test/result_catcher.h" | 
|  | #include "extensions/test/test_extension_dir.h" | 
|  | #include "net/dns/mock_host_resolver.h" | 
|  | #include "net/test/embedded_test_server/controllable_http_response.h" | 
|  | #include "pdf/buildflags.h" | 
|  | #include "ui/base/window_open_disposition.h" | 
|  | #include "url/gurl.h" | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_EXTENSIONS) | 
|  | #include "chrome/browser/ui/browser.h" | 
|  | #include "chrome/browser/ui/tabs/tab_strip_model.h" | 
|  | #include "chrome/test/base/ui_test_utils.h" | 
|  | #endif | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_PDF) | 
|  | #include "base/test/scoped_feature_list.h" | 
|  | #include "pdf/pdf_features.h" | 
|  | #endif  // BUILDFLAG(ENABLE_PDF) | 
|  |  | 
|  | static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); | 
|  |  | 
|  | namespace extensions { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | constexpr const char kSimulatedResourcePath[] = "/simulated-resource.html"; | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_EXTENSIONS) | 
|  | // Returns the IDs of all divs in a page; used for testing script injections. | 
|  | constexpr char kGetDivIds[] = | 
|  | R"(let childIds = []; | 
|  | for (const child of document.body.children) | 
|  | childIds.push(child.id); | 
|  | JSON.stringify(childIds.sort());)"; | 
|  | #endif | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class ScriptingAPITest : public ExtensionApiTest { | 
|  | public: | 
|  | ScriptingAPITest() = default; | 
|  | ScriptingAPITest(const ScriptingAPITest&) = delete; | 
|  | ScriptingAPITest& operator=(const ScriptingAPITest&) = delete; | 
|  | ~ScriptingAPITest() override = default; | 
|  |  | 
|  | void SetUpOnMainThread() override { | 
|  | ExtensionApiTest::SetUpOnMainThread(); | 
|  | controllable_http_response_.emplace(embedded_test_server(), | 
|  | kSimulatedResourcePath); | 
|  | host_resolver()->AddRule("*", "127.0.0.1"); | 
|  | content::SetupCrossSiteRedirector(embedded_test_server()); | 
|  | ASSERT_TRUE(StartEmbeddedTestServer()); | 
|  | } | 
|  |  | 
|  | void OpenURLInCurrentTab(const GURL& url) { | 
|  | content::WebContents* web_contents = GetActiveWebContents(); | 
|  | ASSERT_TRUE(web_contents); | 
|  | // NavigateToURL() waits for the load to stop and verifies the navigation | 
|  | // succeeded. | 
|  | ASSERT_TRUE(NavigateToURL(web_contents, url)); | 
|  | EXPECT_EQ(url, web_contents->GetLastCommittedURL()); | 
|  | } | 
|  |  | 
|  | void OpenURLInNewTab(const GURL& url) { | 
|  | content::TestNavigationObserver nav_observer(url); | 
|  | nav_observer.StartWatchingNewWebContents(); | 
|  | NavigateToURLInNewTab(url); | 
|  | nav_observer.Wait(); | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | content::WaitForLoadStop(web_contents); | 
|  | EXPECT_TRUE(nav_observer.last_navigation_succeeded()); | 
|  | EXPECT_EQ(url, web_contents->GetLastCommittedURL()); | 
|  | } | 
|  |  | 
|  | net::test_server::ControllableHttpResponse& controllable_http_response() { | 
|  | return *controllable_http_response_; | 
|  | } | 
|  |  | 
|  | private: | 
|  | // A controllable HTTP response for tests that need fine-grained timing. | 
|  | // Must be constructed as part of the test suite because it needs to | 
|  | // happen before the embedded test server is initialized. | 
|  | std::optional<net::test_server::ControllableHttpResponse> | 
|  | controllable_http_response_; | 
|  | }; | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, GetContentScripts) { | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/get_scripts")) << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, MainFrameTests) { | 
|  | OpenURLInCurrentTab(embedded_test_server()->GetURL( | 
|  | "example.com", "/extensions/main_world_script_flag.html")); | 
|  | OpenURLInNewTab( | 
|  | embedded_test_server()->GetURL("chromium.org", "/title2.html")); | 
|  |  | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/main_frame", {}, | 
|  | {.ignore_manifest_warnings = true})) | 
|  | << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, SubFramesTests) { | 
|  | OpenURLInCurrentTab( | 
|  | embedded_test_server()->GetURL("a.com", "/iframe_cross_site.html")); | 
|  | OpenURLInNewTab( | 
|  | embedded_test_server()->GetURL("d.com", "/iframe_cross_site.html")); | 
|  | OpenURLInNewTab( | 
|  | embedded_test_server()->GetURL("e.com", "/iframe_sandboxed_srcdoc.html")); | 
|  | OpenURLInNewTab( | 
|  | embedded_test_server()->GetURL("f.com", "/iframe_blob_url.html")); | 
|  |  | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/sub_frames")) << message_; | 
|  | } | 
|  |  | 
|  | // Test validating we don't insert content into nested WebContents. | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, NestedWebContents) { | 
|  | OpenURLInCurrentTab( | 
|  | embedded_test_server()->GetURL("a.com", "/iframe_about_blank.html")); | 
|  |  | 
|  | content::RenderFrameHost* iframe_host = | 
|  | content::ChildFrameAt(GetActiveWebContents(), 0); | 
|  | ASSERT_TRUE(iframe_host); | 
|  | content::WebContents* inner_web_contents = | 
|  | content::CreateAndAttachInnerContents(iframe_host); | 
|  |  | 
|  | EXPECT_TRUE(content::NavigateToURL( | 
|  | inner_web_contents, embedded_test_server()->GetURL("/title1.html"))); | 
|  |  | 
|  | // From there, the test continues in the JS. | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/nested_web_contents")) << message_; | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_PDF) | 
|  | class ScriptingAPIOopifPdfTest : public ScriptingAPITest { | 
|  | public: | 
|  | ScriptingAPIOopifPdfTest() { | 
|  | feature_list_.InitAndEnableFeature(chrome_pdf::features::kPdfOopif); | 
|  | } | 
|  |  | 
|  | private: | 
|  | base::test::ScopedFeatureList feature_list_; | 
|  | }; | 
|  |  | 
|  | // Validate that extensions are not allowed to execute scripts within the PDF | 
|  | // extension frame and the PDF content frame. | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPIOopifPdfTest, PdfFrames) { | 
|  | OpenURLInCurrentTab( | 
|  | embedded_test_server()->GetURL("a.com", "/page_with_embedded_pdf.html")); | 
|  |  | 
|  | // From there, the test continues in the JS. | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/pdf")) << message_; | 
|  | } | 
|  | #endif  // BUILDFLAG(ENABLE_PDF) | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, CSSInjection) { | 
|  | OpenURLInCurrentTab( | 
|  | embedded_test_server()->GetURL("example.com", "/simple.html")); | 
|  | OpenURLInNewTab( | 
|  | embedded_test_server()->GetURL("chromium.org", "/title2.html")); | 
|  | OpenURLInNewTab(embedded_test_server()->GetURL("subframes.example", | 
|  | "/iframe_cross_site.html")); | 
|  | OpenURLInNewTab(embedded_test_server()->GetURL( | 
|  | "subframes-sandboxed.example", "/iframe_sandboxed_srcdoc.html")); | 
|  |  | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/css_injection")) << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, CSSRemoval) { | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/remove_css")) << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, RegisterContentScripts) { | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/register_scripts")) << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, UnregisterContentScripts) { | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/unregister_scripts")) << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, UpdateContentScripts) { | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/update_scripts")) << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, DynamicContentScriptParameters) { | 
|  | // Dynamic content script parameters are currently limited to trunk. | 
|  | ScopedCurrentChannel scoped_channel(version_info::Channel::UNKNOWN); | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/dynamic_script_parameters")) | 
|  | << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, DynamicContentScriptsMainWorld) { | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/dynamic_scripts_main_world")) | 
|  | << message_; | 
|  | } | 
|  |  | 
|  | // Unregisters a pending script and verifies that the script is unregistered | 
|  | // and doesn't inject. | 
|  | // Regression test for https://crbug.com/1496907. | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, | 
|  | RapidDynamicContentScriptRegistrationAndUnregistration) { | 
|  | static constexpr char kManifest[] = | 
|  | R"({ | 
|  | "name": "test", | 
|  | "manifest_version": 3, | 
|  | "version": "0.1", | 
|  | "background": {"service_worker": "background.js"}, | 
|  | "permissions": ["scripting"], | 
|  | "host_permissions": ["*://example.com/*"] | 
|  | })"; | 
|  | static constexpr char kBackgroundJs[] = | 
|  | R"(chrome.test.runTests([ | 
|  | async function registerScripts() { | 
|  | const scripts = | 
|  | [ | 
|  | { | 
|  | id: 'script_1', | 
|  | matches: ['http://example.com/*'], | 
|  | js: ['script1.js'], | 
|  | runAt: 'document_end', | 
|  | }, | 
|  | { | 
|  | id: 'script_2', | 
|  | matches: ['http://example.com/*'], | 
|  | js: ['script2.js'], | 
|  | runAt: 'document_end', | 
|  | } | 
|  | ]; | 
|  |  | 
|  | // Call to register the two scripts, then immediately (before | 
|  | // registration is complete) unregister script 1. | 
|  | const registered = | 
|  | chrome.scripting.registerContentScripts(scripts); | 
|  | const unregistered = | 
|  | chrome.scripting.unregisterContentScripts({ids: ['script_1']}); | 
|  | await Promise.allSettled([registered, unregistered]); | 
|  |  | 
|  | // Only script 2 should still be registered. | 
|  | const registeredScripts = | 
|  | await chrome.scripting.getRegisteredContentScripts(); | 
|  | chrome.test.assertEq(['script_2'], | 
|  | registeredScripts.map(script => script.id)); | 
|  |  | 
|  | chrome.test.succeed(); | 
|  | }]);)"; | 
|  | static constexpr char kScript1Js[] = | 
|  | R"(var div = document.createElement('div'); | 
|  | div.id = 'injected_1'; | 
|  | document.body.appendChild(div);)"; | 
|  | static constexpr char kScript2Js[] = | 
|  | R"(console.warn('injectory');var div = document.createElement('div'); | 
|  | div.id = 'injected_2'; | 
|  | document.body.appendChild(div);)"; | 
|  |  | 
|  | TestExtensionDir test_dir; | 
|  | test_dir.WriteManifest(kManifest); | 
|  | test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); | 
|  | test_dir.WriteFile(FILE_PATH_LITERAL("script1.js"), kScript1Js); | 
|  | test_dir.WriteFile(FILE_PATH_LITERAL("script2.js"), kScript2Js); | 
|  |  | 
|  | // The extension registers two scripts and then immediately unregistered | 
|  | // the first; it also verifies the API indicates only the second script | 
|  | // is still registered. | 
|  | ResultCatcher result_catcher; | 
|  | const Extension* extension = LoadExtension(test_dir.UnpackedPath()); | 
|  | ASSERT_TRUE(extension); | 
|  | ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); | 
|  |  | 
|  | // Verify that only the second script injects (i.e., that the first script | 
|  | // really was unregistered). Regression test for https://crbug.com/1496907. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | const GURL url = | 
|  | embedded_test_server()->GetURL("example.com", "/simple.html"); | 
|  | ASSERT_TRUE(NavigateToURL(web_contents, url)); | 
|  | content::RenderFrameHost* new_frame = web_contents->GetPrimaryMainFrame(); | 
|  |  | 
|  | static constexpr char kGetInjectedIds[] = | 
|  | R"(const divs = document.body.getElementsByTagName('div'); | 
|  | JSON.stringify(Array.from(divs).map(div => div.id));)"; | 
|  |  | 
|  | EXPECT_EQ(R"(["injected_2"])", content::EvalJs(new_frame, kGetInjectedIds)); | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_EXTENSIONS) | 
|  | // Test that if an extension with persistent scripts is quickly unloaded while | 
|  | // these scripts are being fetched, requests that wait on that extension's | 
|  | // script load will be unblocked. Regression for crbug.com/1250575 | 
|  | // TODO(crbug.com/40282331): Disabled on ASAN due to leak caused by renderer gin | 
|  | // objects which are intended to be leaked. | 
|  | #if defined(ADDRESS_SANITIZER) | 
|  | #define MAYBE_RapidLoadUnload DISABLED_RapidLoadUnload | 
|  | #else | 
|  | #define MAYBE_RapidLoadUnload RapidLoadUnload | 
|  | #endif | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, MAYBE_RapidLoadUnload) { | 
|  | ResultCatcher result_catcher; | 
|  | const Extension* extension = LoadExtension( | 
|  | test_data_dir_.AppendASCII("scripting/register_one_script")); | 
|  | ASSERT_TRUE(extension); | 
|  | EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); | 
|  |  | 
|  | DisableExtension(extension->id()); | 
|  |  | 
|  | // Load another extension with a content script that is injected into all | 
|  | // sites. This extension is necessary because while the register_one_script | 
|  | // extension is loading its scripts, requests which match ANY extension's | 
|  | // scripts are throttled. When an extension unloads before its script load is | 
|  | // finished and there are no more loads in progress, we want to verify that | 
|  | // ALL throttled requests are resumed, not just the ones matching the unloaded | 
|  | // extension's scripts. | 
|  | ASSERT_TRUE(LoadExtension( | 
|  | test_data_dir_.AppendASCII("content_scripts/css_injection"))); | 
|  |  | 
|  | // First, trigger an OnLoaded event for `extension`, then quickly trigger an | 
|  | // OnUnloaded event by calling Enable/DisableExtension. Since `extension` | 
|  | // contains persistent dynamic scripts, it must fetch them from the StateStore | 
|  | // which yields control of the thread so TriggerOnUnloaded is called | 
|  | // immediately, which unloads the extension before the Statestore fetch can | 
|  | // finish. | 
|  | extension_registrar()->EnableExtension(extension->id()); | 
|  | extension_registrar()->DisableExtension( | 
|  | extension->id(), {disable_reason::DISABLE_USER_ACTION}); | 
|  |  | 
|  | // Verify that the navigation to google.com, which matches a script in the | 
|  | // css_injection extension, will complete. | 
|  | OpenURLInCurrentTab( | 
|  | embedded_test_server()->GetURL("google.com", "/simple.html")); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, DynamicContentScriptsSizeLimits) { | 
|  | auto single_scripts_limit_reset = | 
|  | script_parsing::CreateScopedMaxScriptLengthForTesting(700u); | 
|  | auto extension_scripts_limit_reset = | 
|  | script_parsing::CreateScopedMaxScriptsLengthPerExtensionForTesting(1200u); | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/dynamic_scripts_size_limits")) | 
|  | << message_; | 
|  | } | 
|  |  | 
|  | // Tests that scripting.executeScript called with files exceeding the max size | 
|  | // limit will return an error and not execute. | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, ExecuteScriptSizeLimit) { | 
|  | auto single_scripts_limit_reset = | 
|  | script_parsing::CreateScopedMaxScriptLengthForTesting(700u); | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/execute_script_size_limit")) | 
|  | << message_; | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, ExecuteScriptSpecialCharacters) { | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/execute_script_special_characters")) | 
|  | << message_; | 
|  | } | 
|  |  | 
|  | // Tests that calling scripting.executeScript works on a newly created tab | 
|  | // before the initial commit has happened. Regression for crbug.com/1191971. | 
|  | // TODO(crbug.com/391921606): Port to desktop Android when we have test | 
|  | // navigation utilities that support "wait for tab". | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, ExecuteScriptBeforeInitialCommit) { | 
|  | constexpr char kManifest[] = | 
|  | R"({ | 
|  | "name": "Scripting API Test", | 
|  | "manifest_version": 3, | 
|  | "version": "0.1", | 
|  | "permissions": ["scripting", "tabs"], | 
|  | "host_permissions": ["http://example.com/*"] | 
|  | })"; | 
|  |  | 
|  | constexpr char kArgTemplate[] = | 
|  | R"([{ | 
|  | "target": {"tabId": %d}, | 
|  | "func": "() => { | 
|  | document.title = 'Modified Title'; | 
|  | return document.title; | 
|  | }" | 
|  | }])"; | 
|  |  | 
|  | TestExtensionDir test_dir; | 
|  | test_dir.WriteManifest(kManifest); | 
|  |  | 
|  | { | 
|  | GURL target_url = | 
|  | embedded_test_server()->GetURL("example.com", "/simple.html"); | 
|  | auto execute_script_function = | 
|  | base::MakeRefCounted<ScriptingExecuteScriptFunction>(); | 
|  |  | 
|  | const Extension* extension = LoadExtension(test_dir.UnpackedPath()); | 
|  | ASSERT_TRUE(extension); | 
|  | execute_script_function->set_extension(extension); | 
|  | execute_script_function->set_has_callback(true); | 
|  |  | 
|  | // We only want to wait for the tab to have loaded here and not for the | 
|  | // navigation to have ended because we want executeScript to run before the | 
|  | // navigation has committed. | 
|  | ui_test_utils::NavigateToURLWithDisposition( | 
|  | browser(), target_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, | 
|  | ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); | 
|  | content::WebContents* web_contents = | 
|  | browser()->tab_strip_model()->GetActiveWebContents(); | 
|  | ASSERT_TRUE(web_contents); | 
|  | EXPECT_TRUE(web_contents->GetLastCommittedURL().is_empty()); | 
|  | EXPECT_EQ(target_url, web_contents->GetVisibleURL()); | 
|  |  | 
|  | // To avoid as much async delay as possible we invoke the execute script | 
|  | // extension function manually rather than calling it in JS. | 
|  | int tab_id = ExtensionTabUtil::GetTabId(web_contents); | 
|  | std::string args = base::StringPrintf(kArgTemplate, tab_id); | 
|  | std::optional<base::Value> result = | 
|  | api_test_utils::RunFunctionAndReturnSingleResult( | 
|  | execute_script_function.get(), args, profile()); | 
|  |  | 
|  | // Now we check the function call returned what we expected in the result. | 
|  | ASSERT_TRUE(result); | 
|  | base::Value::List& result_list = result->GetList(); | 
|  | ASSERT_EQ(1u, result_list.size()); | 
|  | const std::string* result_returned = | 
|  | result_list[0].GetDict().FindString("result"); | 
|  | ASSERT_TRUE(result_returned); | 
|  | EXPECT_EQ("Modified Title", *result_returned); | 
|  |  | 
|  | // We also check that the tab itself was modified by the call. | 
|  | EXPECT_EQ(u"Modified Title", web_contents->GetTitle()); | 
|  |  | 
|  | // Ensure that once the page has entirely finished loading and the | 
|  | // navigation has committed, our executeScript changes have stuck. | 
|  | EXPECT_TRUE(WaitForLoadStop(web_contents)); | 
|  | EXPECT_EQ(target_url, web_contents->GetLastCommittedURL()); | 
|  | EXPECT_EQ(u"Modified Title", web_contents->GetTitle()); | 
|  | } | 
|  |  | 
|  | // Same as above, but for a page which the extension does not have access to. | 
|  | { | 
|  | GURL target_url = | 
|  | embedded_test_server()->GetURL("noAccess.com", "/simple.html"); | 
|  | auto execute_script_function = | 
|  | base::MakeRefCounted<ScriptingExecuteScriptFunction>(); | 
|  |  | 
|  | const Extension* extension = LoadExtension(test_dir.UnpackedPath()); | 
|  | ASSERT_TRUE(extension); | 
|  | execute_script_function->set_extension(extension); | 
|  | execute_script_function->set_has_callback(true); | 
|  |  | 
|  | // We only want to wait for the tab to have loaded here and not for the | 
|  | // navigation to have ended because we want executeScript to run before the | 
|  | // navigation has committed. | 
|  | ui_test_utils::NavigateToURLWithDisposition( | 
|  | browser(), target_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, | 
|  | ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); | 
|  | content::WebContents* web_contents = | 
|  | browser()->tab_strip_model()->GetActiveWebContents(); | 
|  | ASSERT_TRUE(web_contents); | 
|  | EXPECT_TRUE(web_contents->GetLastCommittedURL().is_empty()); | 
|  | EXPECT_EQ(target_url, web_contents->GetVisibleURL()); | 
|  |  | 
|  | // To avoid as much async delay as possible we invoke the execute script | 
|  | // extension function manually rather than calling it in JS. | 
|  | int tab_id = ExtensionTabUtil::GetTabId(web_contents); | 
|  | std::string args = base::StringPrintf(kArgTemplate, tab_id); | 
|  | std::string error(api_test_utils::RunFunctionAndReturnError( | 
|  | execute_script_function.get(), args, profile())); | 
|  |  | 
|  | // We should have gotten back an error that the page could not be accessed. | 
|  | // The URL for the pending navigation will be included because the extension | 
|  | // has the tabs persmission. | 
|  | std::string expected_error = base::StringPrintf( | 
|  | "Cannot access contents of url \"%s\". Extension manifest must request " | 
|  | "permission to access this host.", | 
|  | target_url.spec().c_str()); | 
|  | EXPECT_EQ(expected_error, error); | 
|  |  | 
|  | // We also need to verify the page was not modified by the execute script | 
|  | // call. Since this is a still loading page the title will be blank. | 
|  | EXPECT_EQ(u"", web_contents->GetTitle()); | 
|  |  | 
|  | // After the page has finished loading, there should be a title that is | 
|  | // still unmodified by our executeScript call. | 
|  | EXPECT_TRUE(WaitForLoadStop(web_contents)); | 
|  | EXPECT_EQ(target_url, web_contents->GetLastCommittedURL()); | 
|  | EXPECT_EQ(u"OK", web_contents->GetTitle()); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Tests that extensions are able to specify that a script should be able to | 
|  | // inject into a page as soon as possible. The testing for this is a bit tricky | 
|  | // because we need to craft a page that is guaranteed to not load by the time | 
|  | // the injections are triggered. | 
|  | // TODO(crbug.com/391921606): Port to desktop Android when we have test | 
|  | // navigation utilities that support "wait for tab". | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, InjectImmediately) { | 
|  | static constexpr char kManifest[] = | 
|  | R"({ | 
|  | "name": "Scripting Extension", | 
|  | "manifest_version": 3, | 
|  | "version": "0.1", | 
|  | "background": {"service_worker": "worker.js"}, | 
|  | "permissions": ["scripting"], | 
|  | "host_permissions": ["http://example.com/*"] | 
|  | })"; | 
|  | static constexpr char kWorker[] = "// Intentionally blank"; | 
|  |  | 
|  | TestExtensionDir test_dir; | 
|  | test_dir.WriteManifest(kManifest); | 
|  | test_dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorker); | 
|  | const Extension* extension = LoadExtension(test_dir.UnpackedPath()); | 
|  | ASSERT_TRUE(extension); | 
|  |  | 
|  | // Navigate to a URL with a controllable response. This allows us to guarantee | 
|  | // that the page hasn't finished loading by the time we try to inject. | 
|  | const GURL page_url = | 
|  | embedded_test_server()->GetURL("example.com", kSimulatedResourcePath); | 
|  | ui_test_utils::NavigateToURLWithDisposition( | 
|  | browser(), page_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, | 
|  | ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); | 
|  | controllable_http_response().WaitForRequest(); | 
|  |  | 
|  | content::WebContents* web_contents = | 
|  | browser()->tab_strip_model()->GetActiveWebContents(); | 
|  | EXPECT_TRUE(web_contents->IsLoading()); | 
|  | const int tab_id = ExtensionTabUtil::GetTabId(web_contents); | 
|  |  | 
|  | // A script to call executeScript() twice - once with the default injection | 
|  | // timing and once with `injectImmediately` specified. The script with | 
|  | // `injectImmediately` should inject before the page finishes loading. | 
|  | // Upon injection completion, stores the result of the injection (which is the | 
|  | // target's URL) and sends a message. | 
|  | static constexpr char kInjectScripts[] = | 
|  | R"(function injectImmediate() { | 
|  | return 'Immediate: ' + window.location.href; | 
|  | } | 
|  |  | 
|  | function injectDefault() { | 
|  | return 'Default: ' + window.location.href; | 
|  | } | 
|  |  | 
|  | self.defaultResult = 'unset'; | 
|  | self.immediateResult = 'unset'; | 
|  |  | 
|  | const target = {tabId: %d}; | 
|  | chrome.scripting.executeScript( | 
|  | { | 
|  | target: target, | 
|  | func: injectDefault, | 
|  | }, | 
|  | (results) => { | 
|  | self.defaultResult = results[0].result; | 
|  | chrome.test.sendMessage('default complete'); | 
|  | }); | 
|  |  | 
|  | chrome.scripting.executeScript( | 
|  | { | 
|  | target: target, | 
|  | func: injectImmediate, | 
|  | injectImmediately: true, | 
|  | }, | 
|  | (results) => { | 
|  | self.immediateResult = results[0].result; | 
|  | chrome.test.sendMessage('immediate complete'); | 
|  | });)"; | 
|  |  | 
|  | std::string expected_immediate_result = "Immediate: " + page_url.spec(); | 
|  | std::string expected_default_result = "Default: " + page_url.spec(); | 
|  |  | 
|  | // A helper function to run the script in the worker context. | 
|  | auto run_script_in_worker = [this, extension](const std::string& script) { | 
|  | return BackgroundScriptExecutor::ExecuteScript( | 
|  | profile(), extension->id(), script, | 
|  | BackgroundScriptExecutor::ResultCapture::kSendScriptResult); | 
|  | }; | 
|  |  | 
|  | auto get_default_result = [run_script_in_worker]() { | 
|  | return run_script_in_worker( | 
|  | "chrome.test.sendScriptResult(self.defaultResult);"); | 
|  | }; | 
|  | auto get_immediate_result = [run_script_in_worker]() { | 
|  | return run_script_in_worker( | 
|  | "chrome.test.sendScriptResult(self.immediateResult);"); | 
|  | }; | 
|  |  | 
|  | // Send back some HTML for the request (this can only be done once the | 
|  | // request is received via WaitForRequest(), above). This doesn't complete | 
|  | // the request; the request is considered ongoing until | 
|  | // ControllableHttpResponse::Done() is called. | 
|  | controllable_http_response().Send(net::HTTP_OK, "text/html", | 
|  | "<html>Hello, World!</html>"); | 
|  |  | 
|  | EXPECT_TRUE(web_contents->IsLoading()); | 
|  |  | 
|  | ExtensionTestMessageListener immediate_listener("immediate complete"); | 
|  | ExtensionTestMessageListener default_listener("default complete"); | 
|  | BackgroundScriptExecutor::ExecuteScriptAsync( | 
|  | profile(), extension->id(), base::StringPrintf(kInjectScripts, tab_id)); | 
|  |  | 
|  | // The script with immediate injection should finish (but it's still round | 
|  | // trips to the browser and renderer, so it won't be synchronous). | 
|  | ASSERT_TRUE(immediate_listener.WaitUntilSatisfied()); | 
|  |  | 
|  | // The web contents should still be loading. The immediate injection should | 
|  | // have finished properly, and the default injection should still be pending. | 
|  | EXPECT_TRUE(web_contents->IsLoading()); | 
|  | EXPECT_FALSE(default_listener.was_satisfied()); | 
|  | EXPECT_EQ(base::Value("unset"), get_default_result()); | 
|  | EXPECT_EQ(base::Value(expected_immediate_result), get_immediate_result()); | 
|  |  | 
|  | // Finish loading the page by completing the HTTP response. | 
|  | controllable_http_response().Done(); | 
|  | ASSERT_TRUE(content::WaitForLoadStop(web_contents)); | 
|  |  | 
|  | // The default injection should now be able to complete. | 
|  | ASSERT_TRUE(default_listener.WaitUntilSatisfied()); | 
|  |  | 
|  | EXPECT_EQ(base::Value(expected_default_result), get_default_result()); | 
|  | EXPECT_EQ(base::Value(expected_immediate_result), get_immediate_result()); | 
|  | } | 
|  | #endif | 
|  |  | 
|  | #if !BUILDFLAG(IS_ANDROID) | 
|  | // Verifies dynamic scripts are properly injected in incognito. | 
|  | // Regression test for https://crbug.com/1495191. | 
|  | // TODO(crbug.com/40200835): Flaky on Android. | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, | 
|  | PRE_DynamicContentScriptsInjectInIncognito) { | 
|  | // TODO(crbug.com/40937027): Convert test to use HTTPS and then remove. | 
|  | ScopedAllowHttpForHostnamesForTesting allow_http({"example.com"}, | 
|  | profile()->GetPrefs()); | 
|  |  | 
|  | // Load up two extensions, one that's allowed in incognito and one that's | 
|  | // not. | 
|  | const Extension* incognito_allowed = | 
|  | LoadExtension(test_data_dir_.AppendASCII("scripting/incognito_allowed"), | 
|  | {.allow_in_incognito = true}); | 
|  | const Extension* incognito_disallowed = LoadExtension( | 
|  | test_data_dir_.AppendASCII("scripting/incognito_disallowed"), | 
|  | {.allow_in_incognito = false}); | 
|  | ASSERT_TRUE(incognito_allowed); | 
|  | ASSERT_TRUE(incognito_disallowed); | 
|  |  | 
|  | auto register_scripts = [this](const ExtensionId& extension_id) { | 
|  | ResultCatcher result_catcher; | 
|  | BackgroundScriptExecutor::ExecuteScriptAsync(profile(), extension_id, | 
|  | "registerScript();"); | 
|  | ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); | 
|  | }; | 
|  |  | 
|  | // In each extension, register a script that will inject a div with a given | 
|  | // ID indicating if it injected. | 
|  | register_scripts(incognito_allowed->id()); | 
|  | register_scripts(incognito_disallowed->id()); | 
|  |  | 
|  | // Navigate to a page in the on-the-record profile. Both extensions should | 
|  | // inject. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | const GURL page_url = | 
|  | embedded_test_server()->GetURL("example.com", "/simple.html"); | 
|  | ASSERT_TRUE(NavigateToURL(web_contents, page_url)); | 
|  | content::RenderFrameHost* regular_page = web_contents->GetPrimaryMainFrame(); | 
|  | EXPECT_EQ(R"(["incognito-allowed","incognito-disallowed"])", | 
|  | content::EvalJs(regular_page, kGetDivIds)); | 
|  |  | 
|  | // Now, navigate to a page in incognito. Only the incognito-allowed extension | 
|  | // should inject. | 
|  | content::WebContents* incognito_web_contents = | 
|  | PlatformOpenURLOffTheRecord(profile(), page_url); | 
|  | content::WaitForLoadStop(incognito_web_contents); | 
|  |  | 
|  | EXPECT_EQ(R"(["incognito-allowed"])", | 
|  | content::EvalJs(incognito_web_contents, kGetDivIds)); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPITest, | 
|  | DynamicContentScriptsInjectInIncognito) { | 
|  | // TODO(crbug.com/40937027): Convert test to use HTTPS and then remove. | 
|  | ScopedAllowHttpForHostnamesForTesting allow_http({"example.com"}, | 
|  | profile()->GetPrefs()); | 
|  |  | 
|  | // Repeat the steps of navigating to an on-the-record and off-the-record page | 
|  | // to validate injection after a restart. This verifies the incognito bit | 
|  | // is properly set when restoring scripts after a restart. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | const GURL page_url = | 
|  | embedded_test_server()->GetURL("example.com", "/simple.html"); | 
|  | ASSERT_TRUE(NavigateToURL(web_contents, page_url)); | 
|  | content::RenderFrameHost* regular_page = web_contents->GetPrimaryMainFrame(); | 
|  | EXPECT_EQ(R"(["incognito-allowed","incognito-disallowed"])", | 
|  | content::EvalJs(regular_page, kGetDivIds)); | 
|  |  | 
|  | content::WebContents* incognito_web_contents = | 
|  | PlatformOpenURLOffTheRecord(profile(), page_url); | 
|  | content::WaitForLoadStop(incognito_web_contents); | 
|  |  | 
|  | EXPECT_EQ(R"(["incognito-allowed"])", | 
|  | content::EvalJs(incognito_web_contents, kGetDivIds)); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_ANDROID) | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_EXTENSIONS) | 
|  | // Base test fixture for tests spanning multiple sessions where a custom arg is | 
|  | // set before the test is run. | 
|  | // Flaky on desktop Android. | 
|  | class PersistentScriptingAPITest : public ScriptingAPITest { | 
|  | public: | 
|  | PersistentScriptingAPITest() = default; | 
|  |  | 
|  | // ScriptingAPITest override. | 
|  | void SetUp() override { | 
|  | // Initialize the listener object here before calling SetUp. This avoids a | 
|  | // race condition where the extension loads (as part of browser startup) and | 
|  | // sends a message before a message listener in C++ has been initialized. | 
|  |  | 
|  | listener_ = std::make_unique<ExtensionTestMessageListener>( | 
|  | "ready", ReplyBehavior::kWillReply); | 
|  | ScriptingAPITest::SetUp(); | 
|  | } | 
|  |  | 
|  | // Reset listener before the browser gets torn down. | 
|  | void TearDownOnMainThread() override { | 
|  | listener_.reset(); | 
|  | ScriptingAPITest::TearDownOnMainThread(); | 
|  | } | 
|  |  | 
|  | protected: | 
|  | // Used to wait for results from extension tests. This is initialized before | 
|  | // the test is run which avoids a race condition where the extension is loaded | 
|  | // (as part of startup) and finishes its tests before the ResultCatcher is | 
|  | // created. | 
|  | ResultCatcher result_catcher_; | 
|  |  | 
|  | // Used to wait for the extension to load and send a ready message so the test | 
|  | // can reply which the extension waits for to start its testing functions. | 
|  | // This ensures that the testing functions will run after the browser has | 
|  | // finished initializing. | 
|  | std::unique_ptr<ExtensionTestMessageListener> listener_; | 
|  | }; | 
|  |  | 
|  | // Tests that registered content scripts which persist across sessions behave as | 
|  | // expected. The test is run across three sessions. | 
|  | IN_PROC_BROWSER_TEST_F(PersistentScriptingAPITest, | 
|  | PRE_PRE_PersistentDynamicContentScripts) { | 
|  | const Extension* extension = LoadExtension( | 
|  | test_data_dir_.AppendASCII("scripting/persistent_dynamic_scripts")); | 
|  | ASSERT_TRUE(extension); | 
|  | ASSERT_TRUE(listener_->WaitUntilSatisfied()); | 
|  | listener_->Reply( | 
|  | testing::UnitTest::GetInstance()->current_test_info()->name()); | 
|  | EXPECT_TRUE(result_catcher_.GetNextResult()) << result_catcher_.message(); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(PersistentScriptingAPITest, | 
|  | PRE_PersistentDynamicContentScripts) { | 
|  | ASSERT_TRUE(listener_->WaitUntilSatisfied()); | 
|  | listener_->Reply( | 
|  | testing::UnitTest::GetInstance()->current_test_info()->name()); | 
|  | EXPECT_TRUE(result_catcher_.GetNextResult()) << result_catcher_.message(); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(PersistentScriptingAPITest, | 
|  | PersistentDynamicContentScripts) { | 
|  | ASSERT_TRUE(listener_->WaitUntilSatisfied()); | 
|  | listener_->Reply( | 
|  | testing::UnitTest::GetInstance()->current_test_info()->name()); | 
|  | EXPECT_TRUE(result_catcher_.GetNextResult()) << result_catcher_.message(); | 
|  | } | 
|  |  | 
|  | class ScriptingAPIPrerenderingTest : public ScriptingAPITest { | 
|  | protected: | 
|  | ScriptingAPIPrerenderingTest() = default; | 
|  | ~ScriptingAPIPrerenderingTest() override = default; | 
|  |  | 
|  | private: | 
|  | content::test::ScopedPrerenderFeatureList scoped_feature_list_; | 
|  | }; | 
|  |  | 
|  | // TODO(crbug.com/40857271): disabled due to flakiness. | 
|  | IN_PROC_BROWSER_TEST_F(ScriptingAPIPrerenderingTest, DISABLED_Basic) { | 
|  | ASSERT_TRUE(RunExtensionTest("scripting/prerendering")) << message_; | 
|  | } | 
|  | #endif  // BUILDFLAG(ENABLE_EXTENSIONS) | 
|  |  | 
|  | }  // namespace extensions |