Commit 618e202b authored by Devlin Cronin's avatar Devlin Cronin Committed by Commit Bot

[Extensions] Add support for allFrames in scripting.executeScript()

Add support for an allFrames boolean in the scripting.executeScript()
API. If specified, the code is executed in all frames within the tab
(that the extension has permission to access).

Bug: 1144839
Change-Id: Idecc69cf9599dfc821e2d309078324a6e34b0a59
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2532901Reviewed-by: default avatarDavid Bertoni <dbertoni@chromium.org>
Commit-Queue: Devlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#827032}
parent 6afb5772
......@@ -65,11 +65,12 @@ bool HasPermissionToInject(const PermissionsData& permissions,
void ExecuteScript(ScriptExecutor* script_executor,
const std::string& code,
const Extension& extension,
ScriptExecutor::FrameScope frame_scope,
bool user_gesture,
ScriptExecutor::ScriptFinishedCallback callback) {
script_executor->ExecuteScript(
HostID(HostID::EXTENSIONS, extension.id()), UserScript::ADD_JAVASCRIPT,
code, ScriptExecutor::SINGLE_FRAME, ExtensionApiFrameIdMap::kTopFrameId,
code, frame_scope, ExtensionApiFrameIdMap::kTopFrameId,
ScriptExecutor::MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE,
ScriptExecutor::DEFAULT_PROCESS,
/* webview_src */ GURL(), /* script_url */ GURL(), user_gesture,
......@@ -103,9 +104,20 @@ ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() {
DCHECK(script_executor);
std::string error;
if (!HasPermissionToInject(*extension()->permissions_data(),
injection.target.tab_id, tab, &error)) {
return RespondNow(Error(std::move(error)));
ScriptExecutor::FrameScope frame_scope =
injection.target.all_frames && *injection.target.all_frames == true
? ScriptExecutor::INCLUDE_SUB_FRAMES
: ScriptExecutor::SINGLE_FRAME;
// TODO(devlin): It'd be best to do all the permission checks for the frames
// on the browser side, including child frames. Today, we only check the
// parent frame, and then let the ScriptExecutor inject into all child frames
// (there's a permission check at the time of the injection).
if (frame_scope == ScriptExecutor::SINGLE_FRAME) {
if (!HasPermissionToInject(*extension()->permissions_data(),
injection.target.tab_id, tab, &error)) {
return RespondNow(Error(std::move(error)));
}
}
EXTENSION_FUNCTION_VALIDATE(injection.function);
......@@ -117,7 +129,8 @@ ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() {
base::StringPrintf("(%s)()", injection.function->c_str());
ExecuteScript(
script_executor, code_to_execute, *extension(), user_gesture(),
script_executor, code_to_execute, *extension(), frame_scope,
user_gesture(),
base::BindOnce(&ScriptingExecuteScriptFunction::OnScriptExecuted, this));
return RespondLater();
......@@ -129,9 +142,6 @@ void ScriptingExecuteScriptFunction::OnScriptExecuted(
const base::ListValue& result) {
std::vector<api::scripting::InjectionResult> injection_results;
// TODO(devlin): Remove this check when we support multiple frame injection.
DCHECK_EQ(1u, result.GetList().size());
// TODO(devlin): This results in a few copies of values. It'd be better if our
// auto-generated code supported moved-in parameters for result construction.
for (const auto& value : result.GetList()) {
......
......@@ -27,6 +27,7 @@ class ScriptingAPITest : public ExtensionApiTest {
void SetUpOnMainThread() override {
ExtensionApiTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
content::SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(StartEmbeddedTestServer());
}
......@@ -71,4 +72,41 @@ IN_PROC_BROWSER_TEST_F(ScriptingAPITest, MainFrameTests) {
<< message_;
}
IN_PROC_BROWSER_TEST_F(ScriptingAPITest, SubFramesTests) {
// Open up two tabs, each with cross-site iframes, one at a.com and one at
// d.com.
// In both cases, the cross-site iframes point to b.com and c.com.
{
const GURL a_com =
embedded_test_server()->GetURL("a.com", "/iframe_cross_site.html");
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
content::TestNavigationObserver nav_observer(web_contents);
ui_test_utils::NavigateToURL(browser(), a_com);
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(a_com, web_contents->GetLastCommittedURL());
}
{
const GURL d_com =
embedded_test_server()->GetURL("d.com", "/iframe_cross_site.html");
content::TestNavigationObserver nav_observer(d_com);
nav_observer.StartWatchingNewWebContents();
ui_test_utils::NavigateToURLWithDisposition(
browser(), d_com, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(d_com, browser()
->tab_strip_model()
->GetActiveWebContents()
->GetLastCommittedURL());
}
// From there, the test continues in the JS.
ASSERT_TRUE(RunExtensionTestIgnoreManifestWarnings("scripting/sub_frames"));
}
} // namespace extensions
......@@ -10,6 +10,9 @@ namespace scripting {
dictionary InjectionTarget {
// The ID of the tab into which to inject.
long tabId;
// Whether the script should inject into all frames within the tab.
boolean? allFrames;
};
dictionary ScriptInjection {
......
{
"name": "Scripting API Test",
"manifest_version": 3,
"version": "0.1",
"permissions": ["scripting", "tabs"],
"background": {"service_worker": "worker.js"},
"host_permissions": ["http://a.com/*", "http://b.com/*"]
}
// Copyright 2020 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.
function injectedFunction() {
return location.href;
}
async function getSingleTab(query) {
const tabs = await new Promise(resolve => {
chrome.tabs.query(query, resolve);
});
chrome.test.assertEq(1, tabs.length);
return tabs[0];
}
chrome.test.runTests([
async function allowedTopFrameAccess() {
const query = {url: 'http://a.com/*'};
let tab = await getSingleTab(query);
const results = await new Promise(resolve => {
chrome.scripting.executeScript(
{
target: {
tabId: tab.id,
allFrames: true,
},
function: injectedFunction,
},
resolve);
});
chrome.test.assertNoLastError();
chrome.test.assertEq(2, results.length);
// Note: The 'a.com' result is guaranteed to be first, since it's the root
// frame.
const url1 = new URL(results[0].result);
chrome.test.assertEq('a.com', url1.hostname);
const url2 = new URL(results[1].result);
chrome.test.assertEq('b.com', url2.hostname);
chrome.test.succeed();
},
async function disallowedTopFrameAccess() {
const query = {url: 'http://d.com/*'};
let tab = await getSingleTab(query);
const results = await new Promise(resolve => {
chrome.scripting.executeScript(
{
target: {
tabId: tab.id,
allFrames: true,
},
function: injectedFunction,
},
resolve);
});
chrome.test.assertNoLastError();
// The extension doesn't have access to the top frame (d.com), but does to
// one of the subframes (b.com). This injection should succeed (leading to a
// single result).
chrome.test.assertEq(1, results.length);
const url1 = new URL(results[0].result);
chrome.test.assertEq('b.com', url1.hostname);
chrome.test.succeed();
},
]);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment