diff options
20 files changed, 608 insertions, 79 deletions
diff --git a/chrome/browser/chromeos/accessibility/accessibility_manager.cc b/chrome/browser/chromeos/accessibility/accessibility_manager.cc index b0a769c3..6da2f94 100644 --- a/chrome/browser/chromeos/accessibility/accessibility_manager.cc +++ b/chrome/browser/chromeos/accessibility/accessibility_manager.cc @@ -67,6 +67,7 @@ #include "content/public/browser/web_ui.h" #include "content/public/common/content_switches.h" #include "extensions/browser/event_router.h" +#include "extensions/browser/extension_api_frame_id_map.h" #include "extensions/browser/extension_registry.h" #include "extensions/browser/extension_system.h" #include "extensions/browser/file_reader.h" @@ -127,20 +128,21 @@ void ExecuteScriptHelper( return; if (!extensions::TabHelper::FromWebContents(web_contents)) extensions::TabHelper::CreateForWebContents(web_contents); - extensions::TabHelper::FromWebContents(web_contents)->script_executor()-> - ExecuteScript(HostID(HostID::EXTENSIONS, extension_id), - extensions::ScriptExecutor::JAVASCRIPT, - code, - extensions::ScriptExecutor::ALL_FRAMES, - extensions::ScriptExecutor::DONT_MATCH_ABOUT_BLANK, - extensions::UserScript::DOCUMENT_IDLE, - extensions::ScriptExecutor::ISOLATED_WORLD, - extensions::ScriptExecutor::DEFAULT_PROCESS, - GURL(), // No webview src. - GURL(), // No file url. - false, // Not user gesture. - extensions::ScriptExecutor::NO_RESULT, - extensions::ScriptExecutor::ExecuteScriptCallback()); + extensions::TabHelper::FromWebContents(web_contents) + ->script_executor() + ->ExecuteScript(HostID(HostID::EXTENSIONS, extension_id), + extensions::ScriptExecutor::JAVASCRIPT, code, + extensions::ScriptExecutor::INCLUDE_SUB_FRAMES, + extensions::ExtensionApiFrameIdMap::kTopFrameId, + extensions::ScriptExecutor::DONT_MATCH_ABOUT_BLANK, + extensions::UserScript::DOCUMENT_IDLE, + extensions::ScriptExecutor::ISOLATED_WORLD, + extensions::ScriptExecutor::DEFAULT_PROCESS, + GURL(), // No webview src. + GURL(), // No file url. + false, // Not user gesture. + extensions::ScriptExecutor::NO_RESULT, + extensions::ScriptExecutor::ExecuteScriptCallback()); } // Helper class that directly loads an extension's content scripts into diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc index f7a1103..c9b67e1 100644 --- a/chrome/browser/extensions/api/tabs/tabs_api.cc +++ b/chrome/browser/extensions/api/tabs/tabs_api.cc @@ -71,6 +71,7 @@ #include "content/public/browser/web_contents.h" #include "content/public/common/url_constants.h" #include "extensions/browser/app_window/app_window.h" +#include "extensions/browser/extension_api_frame_id_map.h" #include "extensions/browser/extension_function_dispatcher.h" #include "extensions/browser/extension_function_util.h" #include "extensions/browser/extension_host.h" @@ -1359,10 +1360,10 @@ bool TabsUpdateFunction::UpdateURL(const std::string &url_string, net::UnescapeURLComponent(url.GetContent(), net::UnescapeRule::URL_SPECIAL_CHARS | net::UnescapeRule::SPACES), - ScriptExecutor::TOP_FRAME, ScriptExecutor::DONT_MATCH_ABOUT_BLANK, - UserScript::DOCUMENT_IDLE, ScriptExecutor::MAIN_WORLD, - ScriptExecutor::DEFAULT_PROCESS, GURL(), GURL(), user_gesture_, - ScriptExecutor::NO_RESULT, + ScriptExecutor::SINGLE_FRAME, ExtensionApiFrameIdMap::kTopFrameId, + ScriptExecutor::DONT_MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE, + ScriptExecutor::MAIN_WORLD, ScriptExecutor::DEFAULT_PROCESS, GURL(), + GURL(), user_gesture_, ScriptExecutor::NO_RESULT, base::Bind(&TabsUpdateFunction::OnExecuteCodeFinished, this)); *is_async = true; @@ -1898,13 +1899,45 @@ bool ExecuteCodeInTabFunction::CanExecuteScriptOnPage() { CHECK(contents); + int frame_id = details_->frame_id ? *details_->frame_id + : ExtensionApiFrameIdMap::kTopFrameId; + content::RenderFrameHost* rfh = + ExtensionApiFrameIdMap::GetRenderFrameHostById(contents, frame_id); + if (!rfh) { + error_ = ErrorUtils::FormatErrorMessage(keys::kFrameNotFoundError, + base::IntToString(frame_id), + base::IntToString(execute_tab_id_)); + return false; + } + + // Content scripts declared in manifest.json can access frames at about:-URLs + // if the extension has permission to access the frame's origin, so also allow + // programmatic content scripts at about:-URLs for allowed origins. + GURL effective_document_url(rfh->GetLastCommittedURL()); + bool is_about_url = effective_document_url.SchemeIs(url::kAboutScheme); + if (is_about_url && details_->match_about_blank && + *details_->match_about_blank) { + effective_document_url = GURL(rfh->GetLastCommittedOrigin().Serialize()); + } + + if (!effective_document_url.is_valid()) { + // Unknown URL, e.g. because no load was committed yet. Allow for now, the + // renderer will check again and fail the injection if needed. + return true; + } + // NOTE: This can give the wrong answer due to race conditions, but it is OK, // we check again in the renderer. if (!extension()->permissions_data()->CanAccessPage( - extension(), - contents->GetURL(), - execute_tab_id_, - &error_)) { + extension(), effective_document_url, execute_tab_id_, &error_)) { + if (is_about_url && + extension()->permissions_data()->active_permissions().HasAPIPermission( + APIPermission::kTab)) { + error_ = ErrorUtils::FormatErrorMessage( + manifest_errors::kCannotAccessAboutUrl, + rfh->GetLastCommittedURL().spec(), + rfh->GetLastCommittedOrigin().Serialize()); + } return false; } diff --git a/chrome/browser/extensions/api/tabs/tabs_constants.cc b/chrome/browser/extensions/api/tabs/tabs_constants.cc index 5a2a1c9..2903ee9 100644 --- a/chrome/browser/extensions/api/tabs/tabs_constants.cc +++ b/chrome/browser/extensions/api/tabs/tabs_constants.cc @@ -75,6 +75,7 @@ const char kCanOnlyMoveTabsWithinNormalWindowsError[] = "Tabs can only be " "moved to and from normal windows."; const char kCanOnlyMoveTabsWithinSameProfileError[] = "Tabs can only be moved " "between windows in the same profile."; +const char kFrameNotFoundError[] = "No frame with id * in tab *."; const char kNoCrashBrowserError[] = "I'm sorry. I'm afraid I can't do that."; const char kNoCurrentWindowError[] = "No current window"; diff --git a/chrome/browser/extensions/api/tabs/tabs_constants.h b/chrome/browser/extensions/api/tabs/tabs_constants.h index df0064d..628f5ae 100644 --- a/chrome/browser/extensions/api/tabs/tabs_constants.h +++ b/chrome/browser/extensions/api/tabs/tabs_constants.h @@ -77,6 +77,7 @@ extern const char kWindowTypeValueDevTools[]; // Error messages. extern const char kCannotZoomDisabledTabError[]; +extern const char kFrameNotFoundError[]; extern const char kNoCrashBrowserError[]; extern const char kNoCurrentWindowError[]; extern const char kNoLastFocusedWindowError[]; diff --git a/chrome/browser/extensions/execute_script_apitest.cc b/chrome/browser/extensions/execute_script_apitest.cc index 82f3eab..7f81895 100644 --- a/chrome/browser/extensions/execute_script_apitest.cc +++ b/chrome/browser/extensions/execute_script_apitest.cc @@ -37,6 +37,12 @@ IN_PROC_BROWSER_TEST_F(ExecuteScriptApiTest, ExecuteScriptInFrame) { ASSERT_TRUE(RunExtensionTest("executescript/in_frame")) << message_; } +IN_PROC_BROWSER_TEST_F(ExecuteScriptApiTest, ExecuteScriptByFrameId) { + SetupDelayedHostResolver(); + ASSERT_TRUE(StartEmbeddedTestServer()); + ASSERT_TRUE(RunExtensionTest("executescript/frame_id")) << message_; +} + // Fails often on Windows. // http://crbug.com/174715 #if defined(OS_WIN) diff --git a/chrome/test/data/extensions/api_test/executescript/basic/test.js b/chrome/test/data/extensions/api_test/executescript/basic/test.js index 5937596..7185964 100644 --- a/chrome/test/data/extensions/api_test/executescript/basic/test.js +++ b/chrome/test/data/extensions/api_test/executescript/basic/test.js @@ -88,7 +88,13 @@ chrome.test.getConfig(function(config) { }, function executeJavaScriptCodeShouldFail() { - chrome.tabs.update(tabId, { url: testFailureUrl }, function() { + var doneListening = + chrome.test.listenForever(chrome.tabs.onUpdated, onUpdated); + chrome.tabs.update(tabId, {url: testFailureUrl}); + + function onUpdated(updatedTabId, changeInfo) { + if (updatedTabId !== tabId || changeInfo.url === testFailureUrl) + return; var script_file = {}; script_file.code = "document.title = 'executeScript';"; // The error message should contain the URL of the site for which it @@ -97,7 +103,8 @@ chrome.test.getConfig(function(config) { 'Cannot access contents of url "' + testFailureUrl + '". Extension manifest must request permission to access this ' + 'host.')); - }); + doneListening(); + } }, function executeJavaScriptWithNoneValueShouldFail() { diff --git a/chrome/test/data/extensions/api_test/executescript/frame_id/frame.html b/chrome/test/data/extensions/api_test/executescript/frame_id/frame.html new file mode 100644 index 0000000..75a3b13 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/frame_id/frame.html @@ -0,0 +1,3 @@ +Same-origin frame. + +<iframe src="nested.html"></iframe> diff --git a/chrome/test/data/extensions/api_test/executescript/frame_id/frames.html b/chrome/test/data/extensions/api_test/executescript/frame_id/frames.html new file mode 100644 index 0000000..886abb4 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/frame_id/frames.html @@ -0,0 +1,11 @@ +<iframe srcdoc="same-origin srcdoc frame"></iframe> + +<!-- Unique origin about:blank --> +<iframe sandbox="" src="about:blank"></iframe> + +<iframe src="frame.html"></iframe> + +<script> +document.write('<iframe src="http://c.com:' + location.port + + '/empty.html"></iframe>'); +</script> diff --git a/chrome/test/data/extensions/api_test/executescript/frame_id/manifest.json b/chrome/test/data/extensions/api_test/executescript/frame_id/manifest.json new file mode 100644 index 0000000..7b46d7b --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/frame_id/manifest.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "manifest_version": 2, + "name": "executeScript / insertCSS test by frameId", + "background": { + "scripts": ["test.js"] + }, + "permissions": ["webNavigation", "tabs", "http://a.com/"] +} diff --git a/chrome/test/data/extensions/api_test/executescript/frame_id/nested.html b/chrome/test/data/extensions/api_test/executescript/frame_id/nested.html new file mode 100644 index 0000000..b73bc1d --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/frame_id/nested.html @@ -0,0 +1 @@ +Deeply nested frame. diff --git a/chrome/test/data/extensions/api_test/executescript/frame_id/test.js b/chrome/test/data/extensions/api_test/executescript/frame_id/test.js new file mode 100644 index 0000000..cfd38c7 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/frame_id/test.js @@ -0,0 +1,383 @@ +// Copyright 2016 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. + +var pass = chrome.test.callbackPass; +var fail = chrome.test.callbackFail; +var assertEq = chrome.test.assertEq; +var assertTrue = chrome.test.assertTrue; +var relativePath = '/extensions/api_test/executescript/frame_id/frames.html'; +var testOrigin = 'http://a.com:PORT'; +var testUrl = 'http://a.com:PORT' + relativePath; + +var tabId; + +// Frame ID of every frame in this test, and the patterns of the frame URLs. +// Frame IDs are lazily initialized (and constant thereafter). +// All patterns are mutually exclusive. + +// Main frame. +var ID_FRAME_TOP = 0; +var R_FRAME_TOP = /frames\.html/; +// Frame with (same-origin) about:srcdoc. +var ID_FRAME_SRCDOC; +var R_FRAME_SRCDOC = /about:srcdoc/; +// Frame with (unique-origin) sandboxed about:blank. +var ID_FRAME_UNREACHABLE; +var R_FRAME_UNREACHABLE = /about:blank/; +// Frame with same-origin page. +var ID_FRAME_SECOND; +var R_FRAME_SECOND = /frame\.html/; +// Same-origin child frame of |frame_second|. +var ID_FRAME_THIRD; +var R_FRAME_THIRD = /nested\.html/; +// Frame for which the extension does not have the right permissions. +var ID_FRAME_NOPERMISSION; +var R_FRAME_NOPERMISSION = /empty\.html/; + +function matchesAny(urls, regex) { + return urls.some(function(url) { return regex.test(url); }); +} + +var gCssCounter = 0; + +// Calls chrome.tabs.insertCSS and invokes the callback with a list of affected +// URLs. This function assumes that the tab identified by |tabId| exists, and +// that |injectDetails| is a valid argument for insertCSS. +function insertCSS(tabId, injectDetails, callback) { + var marker = (++gCssCounter) + 'px'; + injectDetails.code = 'body { min-width: ' + marker + ';}'; + chrome.tabs.insertCSS(tabId, injectDetails, function() { + chrome.test.assertNoLastError(); + chrome.tabs.executeScript( + tabId, { + code: '[getComputedStyle(document.body).minWidth, document.URL];', + allFrames: true, + matchAboutBlank: true + }, + function(results) { + chrome.test.assertNoLastError(); + results = getAffectedUrls(results); + callback(results); + }); + }); + + // Selects the results from the frames whose CSS was changed by the insertCSS + // call, and returns the URLs of these frames. + function getAffectedUrls(results) { + return results.filter(function(result) { + return result && result[0] === marker; + }).map(function(result) { + return result[1]; // "document.URL" + }); + } +} + +chrome.test.getConfig(function(config) { + testOrigin = testOrigin.replace(/PORT/, config.testServer.port); + testUrl = testUrl.replace(/PORT/, config.testServer.port); + chrome.tabs.onUpdated.addListener(function(_, changeInfo, tab) { + if (changeInfo.status != 'complete' || tab.id !== tabId) { + return; + } + + chrome.webNavigation.getAllFrames({tabId: tabId}, function(frames) { + function getFrameId(urlRegex) { + var filtered = + frames.filter(function(frame) { return urlRegex.test(frame.url); }); + // Sanity check. + chrome.test.assertEq(1, filtered.length); + chrome.test.assertTrue(filtered[0].frameId > 0); + return filtered[0].frameId; + } + + ID_FRAME_SRCDOC = getFrameId(R_FRAME_SRCDOC); + ID_FRAME_UNREACHABLE = getFrameId(R_FRAME_UNREACHABLE); + ID_FRAME_SECOND = getFrameId(R_FRAME_SECOND); + ID_FRAME_THIRD = getFrameId(R_FRAME_THIRD); + ID_FRAME_NOPERMISSION = getFrameId(R_FRAME_NOPERMISSION); + + runTests(config); + }); + }); + + chrome.tabs.create({url: testUrl}, function(tab) { tabId = tab.id; }); +}); + +function runTests(config) { + // All of the following tests set the frameId parameter in the injection + // details. + chrome.test.runTests([ + function executeScriptInTopFrame() { + chrome.tabs.executeScript( + tabId, {frameId: 0, code: 'document.URL'}, pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_TOP)); + })); + }, + + function executeScriptInTopFrameIncludingAllFrames() { + chrome.tabs.executeScript( + tabId, { + frameId: 0, + matchAboutBlank: true, + allFrames: true, + code: 'document.URL' + }, + pass(function(results) { + assertEq(4, results.length); + assertTrue(matchesAny(results, R_FRAME_TOP)); + assertTrue(matchesAny(results, R_FRAME_SRCDOC)); + assertTrue(matchesAny(results, R_FRAME_SECOND)); + assertTrue(matchesAny(results, R_FRAME_THIRD)); + })); + }, + + function executeScriptInSrcdocFrame() { + chrome.tabs.executeScript( + tabId, { + frameId: ID_FRAME_SRCDOC, + matchAboutBlank: true, + code: 'document.URL' + }, + pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_SRCDOC)); + })); + }, + + function executeScriptInSrcdocFrameWithoutMatchAboutBlank() { + // TODO(robwu): Why is the origin serialized as "about:blank" instead of + // "about:srcdoc"? + chrome.tabs.executeScript( + tabId, {frameId: ID_FRAME_SRCDOC, code: 'document.URL'}, + fail( + 'Cannot access "about:blank" at origin "' + testOrigin + '". ' + + 'Extension must have permission to access the frame\'s origin, ' + + 'and matchAboutBlank must be true.')); + }, + + function executeScriptInSrcdocFrameIncludingAllFrames() { + chrome.tabs.executeScript( + tabId, { + frameId: ID_FRAME_SRCDOC, + matchAboutBlank: true, + allFrames: true, + code: 'document.URL' + }, + pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_SRCDOC)); + })); + }, + + function executeScriptInSandboxedFrame() { + chrome.tabs.executeScript( + tabId, { + frameId: ID_FRAME_UNREACHABLE, + matchAboutBlank: true, + code: 'document.URL' + }, + fail( + 'Cannot access "about:blank" at origin "null". Extension must ' + + 'have permission to access the frame\'s origin, and ' + + 'matchAboutBlank must be true.')); + }, + + function executeScriptInSubFrame() { + chrome.tabs.executeScript( + tabId, {frameId: ID_FRAME_SECOND, code: 'document.URL'}, + pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_SECOND)); + })); + }, + + function executeScriptInSubFrameIncludingAllFrames() { + chrome.tabs.executeScript( + tabId, + {frameId: ID_FRAME_SECOND, allFrames: true, code: 'document.URL'}, + pass(function(results) { + assertEq(2, results.length); + assertTrue(matchesAny(results, R_FRAME_SECOND)); + assertTrue(matchesAny(results, R_FRAME_THIRD)); + })); + }, + + function executeScriptInNestedFrame() { + chrome.tabs.executeScript( + tabId, {frameId: ID_FRAME_THIRD, code: 'document.URL'}, + pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_THIRD)); + })); + }, + + function executeScriptInNestedFrameIncludingAllFrames() { + chrome.tabs.executeScript( + tabId, + {frameId: ID_FRAME_THIRD, allFrames: true, code: 'document.URL'}, + pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_THIRD)); + })); + }, + + function executeScriptInFrameWithoutPermission() { + chrome.tabs.executeScript( + tabId, {frameId: ID_FRAME_NOPERMISSION, code: 'document.URL'}, + fail( + 'Cannot access contents of url "http://c.com:' + + config.testServer.port + '/empty.html". Extension manifest ' + + 'must request permission to access this host.')); + }, + + function executeScriptWithNonExistentFrameId() { + chrome.tabs.executeScript( + tabId, {frameId: 999999999, code: 'document.URL'}, + fail('No frame with id 999999999 in tab ' + tabId + '.')); + }, + + function executeScriptWithNegativeFrameId() { + try { + chrome.tabs.executeScript( + tabId, {frameId: -1, code: 'document.URL'}, function() { + chrome.test.fail( + 'executeScript should never have been executed!'); + }); + } catch (e) { + assertEq( + 'Invalid value for argument 2. Property \'frameId\': ' + + 'Value must not be less than 0.', + e.message); + chrome.test.succeed(); + } + }, + + function insertCSSInTopFrame() { + insertCSS(tabId, {frameId: 0}, pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_TOP)); + })); + }, + + function insertCSSInTopFrameIncludingAllFrames() { + insertCSS( + tabId, {frameId: 0, matchAboutBlank: true, allFrames: true}, + pass(function(results) { + assertEq(4, results.length); + assertTrue(matchesAny(results, R_FRAME_TOP)); + assertTrue(matchesAny(results, R_FRAME_SRCDOC)); + assertTrue(matchesAny(results, R_FRAME_SECOND)); + assertTrue(matchesAny(results, R_FRAME_THIRD)); + })); + }, + + function insertCSSInSrcdocFrame() { + insertCSS( + tabId, {frameId: ID_FRAME_SRCDOC, matchAboutBlank: true}, + pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_SRCDOC)); + })); + }, + + function insertCSSInSrcdocFrameWithoutMatchAboutBlank() { + // TODO(robwu): Why is the origin serialized as "about:blank" instead of + // "about:srcdoc"? + chrome.tabs.insertCSS( + tabId, {frameId: ID_FRAME_SRCDOC, code: 'body{color:red;}'}, + fail( + 'Cannot access "about:blank" at origin "' + testOrigin + '". ' + + 'Extension must have permission to access the frame\'s origin, ' + + 'and matchAboutBlank must be true.')); + }, + + function insertCSSInSrcdocFrameIncludingAllFrames() { + insertCSS( + tabId, + {frameId: ID_FRAME_SRCDOC, matchAboutBlank: true, allFrames: true}, + pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_SRCDOC)); + })); + }, + + function insertCSSInSandboxedFrame() { + chrome.tabs.insertCSS( + tabId, { + frameId: ID_FRAME_UNREACHABLE, + matchAboutBlank: true, + code: 'body{color:red}' + }, + fail( + 'Cannot access "about:blank" at origin "null". Extension must ' + + 'have permission to access the frame\'s origin, and ' + + 'matchAboutBlank must be true.')); + }, + + function insertCSSInSubFrame() { + insertCSS(tabId, {frameId: ID_FRAME_SECOND}, pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_SECOND)); + })); + }, + + function insertCSSInSubFrameIncludingAllFrames() { + insertCSS( + tabId, {frameId: ID_FRAME_SECOND, allFrames: true}, + pass(function(results) { + assertEq(2, results.length); + assertTrue(matchesAny(results, R_FRAME_SECOND)); + assertTrue(matchesAny(results, R_FRAME_THIRD)); + })); + }, + + function insertCSSInNestedFrame() { + insertCSS(tabId, {frameId: ID_FRAME_THIRD}, pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_THIRD)); + })); + }, + + function insertCSSInNestedFrameIncludingAllFrames() { + insertCSS( + tabId, {frameId: ID_FRAME_THIRD, allFrames: true}, + pass(function(results) { + assertEq(1, results.length); + assertTrue(matchesAny(results, R_FRAME_THIRD)); + })); + }, + + function insertCSSInFrameWithoutPermission() { + chrome.tabs.insertCSS( + tabId, {frameId: ID_FRAME_NOPERMISSION, code: 'body{color:red}'}, + fail( + 'Cannot access contents of url "http://c.com:' + + config.testServer.port + '/empty.html". Extension manifest ' + + 'must request permission to access this host.')); + }, + + function insertCSSWithNonExistentFrameId() { + chrome.tabs.insertCSS( + tabId, {frameId: 999999999, code: 'body{color:red}'}, + fail('No frame with id 999999999 in tab ' + tabId + '.')); + }, + + function insertCSSWithNegativeFrameId() { + try { + chrome.tabs.insertCSS( + tabId, {frameId: -1, code: 'body{color:red}'}, function() { + chrome.test.fail('insertCSS should never have been executed!'); + }); + } catch (e) { + assertEq( + 'Invalid value for argument 2. Property \'frameId\': ' + + 'Value must not be less than 0.', + e.message); + chrome.test.succeed(); + } + }, + + ]); +} diff --git a/chrome/test/data/extensions/api_test/executescript/navigation_race/test.js b/chrome/test/data/extensions/api_test/executescript/navigation_race/test.js index 02bd14b..fb4f7e0 100644 --- a/chrome/test/data/extensions/api_test/executescript/navigation_race/test.js +++ b/chrome/test/data/extensions/api_test/executescript/navigation_race/test.js @@ -17,9 +17,10 @@ chrome.test.getConfig(function(config) { // the script, so it's still showing a.com, where we don't have // permission to run it. if (chrome.runtime.lastError) { - chrome.test.assertTrue( - chrome.runtime.lastError.message.indexOf( - 'Cannot access contents of the page.') == 0); + chrome.test.assertLastError( + 'Cannot access contents of url "' + urlA + + '". Extension manifest must request permission to access this ' + + 'host.'); chrome.test.notifyPass(); } else { // If there were no errors, then the script did run, but it should diff --git a/extensions/browser/api/execute_code_function.cc b/extensions/browser/api/execute_code_function.cc index 0f98df3..53fcb44 100644 --- a/extensions/browser/api/execute_code_function.cc +++ b/extensions/browser/api/execute_code_function.cc @@ -8,6 +8,7 @@ #include "extensions/browser/api/execute_code_function.h" #include "extensions/browser/component_extension_resource_manager.h" +#include "extensions/browser/extension_api_frame_id_map.h" #include "extensions/browser/extensions_browser_client.h" #include "extensions/browser/file_reader.h" #include "extensions/common/error_utils.h" @@ -139,8 +140,11 @@ bool ExecuteCodeFunction::Execute(const std::string& code_string) { ScriptExecutor::FrameScope frame_scope = details_->all_frames.get() && *details_->all_frames - ? ScriptExecutor::ALL_FRAMES - : ScriptExecutor::TOP_FRAME; + ? ScriptExecutor::INCLUDE_SUB_FRAMES + : ScriptExecutor::SINGLE_FRAME; + + int frame_id = details_->frame_id.get() ? *details_->frame_id + : ExtensionApiFrameIdMap::kTopFrameId; ScriptExecutor::MatchAboutBlank match_about_blank = details_->match_about_blank.get() && *details_->match_about_blank @@ -163,18 +167,11 @@ bool ExecuteCodeFunction::Execute(const std::string& code_string) { CHECK_NE(UserScript::UNDEFINED, run_at); executor->ExecuteScript( - host_id_, - script_type, - code_string, - frame_scope, - match_about_blank, - run_at, - ScriptExecutor::ISOLATED_WORLD, + host_id_, script_type, code_string, frame_scope, frame_id, + match_about_blank, run_at, ScriptExecutor::ISOLATED_WORLD, IsWebView() ? ScriptExecutor::WEB_VIEW_PROCESS : ScriptExecutor::DEFAULT_PROCESS, - GetWebViewSrc(), - file_url_, - user_gesture_, + GetWebViewSrc(), file_url_, user_gesture_, has_callback() ? ScriptExecutor::JSON_SERIALIZED_RESULT : ScriptExecutor::NO_RESULT, base::Bind(&ExecuteCodeFunction::OnExecuteCodeFinished, this)); diff --git a/extensions/browser/extension_api_frame_id_map.cc b/extensions/browser/extension_api_frame_id_map.cc index 72949be..e844fac 100644 --- a/extensions/browser/extension_api_frame_id_map.cc +++ b/extensions/browser/extension_api_frame_id_map.cc @@ -24,6 +24,7 @@ base::LazyInstance<ExtensionApiFrameIdMap>::Leaky g_map_instance = } // namespace const int ExtensionApiFrameIdMap::kInvalidFrameId = -1; +const int ExtensionApiFrameIdMap::kTopFrameId = 0; ExtensionApiFrameIdMap::CachedFrameIdPair::CachedFrameIdPair() : frame_id(kInvalidFrameId), parent_frame_id(kInvalidFrameId) {} @@ -75,7 +76,7 @@ int ExtensionApiFrameIdMap::GetFrameId(content::RenderFrameHost* rfh) { return kInvalidFrameId; if (rfh->GetParent()) return rfh->GetFrameTreeNodeId(); - return 0; // Main frame. + return kTopFrameId; } int ExtensionApiFrameIdMap::GetParentFrameId(content::RenderFrameHost* rfh) { @@ -96,7 +97,7 @@ content::RenderFrameHost* ExtensionApiFrameIdMap::GetRenderFrameHostById( if (frame_id == kInvalidFrameId) return nullptr; - if (frame_id == 0) + if (frame_id == kTopFrameId) return web_contents->GetMainFrame(); DCHECK_GE(frame_id, 1); diff --git a/extensions/browser/extension_api_frame_id_map.h b/extensions/browser/extension_api_frame_id_map.h index c3af8b3..8bb1422 100644 --- a/extensions/browser/extension_api_frame_id_map.h +++ b/extensions/browser/extension_api_frame_id_map.h @@ -48,6 +48,9 @@ class ExtensionApiFrameIdMap { // An invalid extension API frame ID. static const int kInvalidFrameId; + // Extension API frame ID of the top-level frame. + static const int kTopFrameId; + static ExtensionApiFrameIdMap* Get(); // Get the extension API frame ID for |rfh|. diff --git a/extensions/browser/script_executor.cc b/extensions/browser/script_executor.cc index 687be48..230bca3 100644 --- a/extensions/browser/script_executor.cc +++ b/extensions/browser/script_executor.cc @@ -13,6 +13,7 @@ #include "content/public/browser/render_view_host.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_observer.h" +#include "extensions/browser/extension_api_frame_id_map.h" #include "extensions/browser/extension_registry.h" #include "extensions/browser/script_execution_observer.h" #include "extensions/common/extension_messages.h" @@ -28,6 +29,7 @@ namespace extensions { namespace { const char* kRendererDestroyed = "The tab was closed."; +const char* kFrameRemoved = "The frame was removed."; // A handler for a single injection request. On creation this will send the // injection request to the renderer, and it will be destroyed after either the @@ -38,18 +40,28 @@ class Handler : public content::WebContentsObserver { content::WebContents* web_contents, const ExtensionMsg_ExecuteCode_Params& params, ScriptExecutor::FrameScope scope, + int frame_id, const ScriptExecutor::ExecuteScriptCallback& callback) : content::WebContentsObserver(web_contents), script_observers_(AsWeakPtr(script_observers)), host_id_(params.host_id), request_id_(params.request_id), + include_sub_frames_(scope == ScriptExecutor::INCLUDE_SUB_FRAMES), + root_rfh_(ExtensionApiFrameIdMap::GetRenderFrameHostById(web_contents, + frame_id)), + root_is_main_frame_(root_rfh_ ? !root_rfh_->GetParent() : false), callback_(callback) { - if (scope == ScriptExecutor::ALL_FRAMES) { - web_contents->ForEachFrame(base::Bind(&Handler::SendExecuteCode, - base::Unretained(this), params)); - } else { - SendExecuteCode(params, web_contents->GetMainFrame()); + if (root_rfh_) { + if (include_sub_frames_) { + web_contents->ForEachFrame(base::Bind(&Handler::SendExecuteCode, + base::Unretained(this), params)); + } else { + SendExecuteCode(params, root_rfh_); + } } + + if (pending_render_frames_.empty()) + Finish(); } private: @@ -92,10 +104,25 @@ class Handler : public content::WebContentsObserver { // the number of pending messages. void SendExecuteCode(const ExtensionMsg_ExecuteCode_Params& params, content::RenderFrameHost* frame) { + if (!frame->IsRenderFrameLive()) + return; + DCHECK(!root_is_main_frame_ || ShouldIncludeFrame(frame)); + if (!root_is_main_frame_ && !ShouldIncludeFrame(frame)) + return; pending_render_frames_.insert(frame); frame->Send(new ExtensionMsg_ExecuteCode(frame->GetRoutingID(), params)); } + // Returns whether a frame is the root frame or a descendant of it. + bool ShouldIncludeFrame(content::RenderFrameHost* frame) { + while (frame) { + if (frame == root_rfh_) + return true; + frame = frame->GetParent(); + } + return false; + } + // Handles the ExecuteCodeFinished message. void OnExecuteCodeFinished(content::RenderFrameHost* render_frame_host, int request_id, @@ -106,22 +133,22 @@ class Handler : public content::WebContentsObserver { DCHECK(!pending_render_frames_.empty()); bool erased = pending_render_frames_.erase(render_frame_host) == 1; DCHECK(erased); - bool is_main_frame = web_contents()->GetMainFrame() == render_frame_host; + bool is_root_frame = root_rfh_ == render_frame_host; // Set the result, if there is one. const base::Value* script_value = nullptr; if (result_list.Get(0u, &script_value)) { // If this is the main result, we put it at index 0. Otherwise, we just // append it at the end. - if (is_main_frame && !results_.empty()) + if (is_root_frame && !results_.empty()) CHECK(results_.Insert(0u, script_value->DeepCopy())); else results_.Append(script_value->DeepCopy()); } - if (is_main_frame) { // Only use the main frame's error and url. - main_frame_error_ = error; - main_frame_url_ = on_url; + if (is_root_frame) { // Only use the root frame's error and url. + root_frame_error_ = error; + root_frame_url_ = on_url; } // Wait until the final request finishes before reporting back. @@ -130,23 +157,24 @@ class Handler : public content::WebContentsObserver { } void Finish() { - if (main_frame_url_.is_empty()) { - // We never finished the main frame injection. - main_frame_error_ = kRendererDestroyed; + if (root_frame_url_.is_empty()) { + // We never finished the root frame injection. + root_frame_error_ = + root_is_main_frame_ ? kRendererDestroyed : kFrameRemoved; results_.Clear(); } - if (script_observers_.get() && main_frame_error_.empty() && + if (script_observers_.get() && root_frame_error_.empty() && host_id_.type() == HostID::EXTENSIONS) { ScriptExecutionObserver::ExecutingScriptsMap id_map; id_map[host_id_.id()] = std::set<std::string>(); FOR_EACH_OBSERVER( ScriptExecutionObserver, *script_observers_, - OnScriptsExecuted(web_contents(), id_map, main_frame_url_)); + OnScriptsExecuted(web_contents(), id_map, root_frame_url_)); } if (!callback_.is_null()) - callback_.Run(main_frame_error_, main_frame_url_, results_); + callback_.Run(root_frame_error_, root_frame_url_, results_); delete this; } @@ -158,17 +186,27 @@ class Handler : public content::WebContentsObserver { // The request id of the injection. int request_id_; + // Whether to inject in |root_rfh_| and all of its descendant frames. + bool include_sub_frames_; + + // The frame (and optionally its descendant frames) where the injection will + // occur. + content::RenderFrameHost* root_rfh_; + + // Whether |root_rfh_| is the main frame of a tab. + bool root_is_main_frame_; + // The hosts of the still-running injections. std::set<content::RenderFrameHost*> pending_render_frames_; // The results of the injection. base::ListValue results_; - // The error from injecting into the main frame. - std::string main_frame_error_; + // The error from injecting into the root frame. + std::string root_frame_error_; - // The url of the main frame. - GURL main_frame_url_; + // The url of the root frame. + GURL root_frame_url_; // The callback to run after all injections complete. ScriptExecutor::ExecuteScriptCallback callback_; @@ -197,6 +235,7 @@ void ScriptExecutor::ExecuteScript(const HostID& host_id, ScriptExecutor::ScriptType script_type, const std::string& code, ScriptExecutor::FrameScope frame_scope, + int frame_id, ScriptExecutor::MatchAboutBlank about_blank, UserScript::RunLocation run_at, ScriptExecutor::WorldType world_type, @@ -232,7 +271,8 @@ void ScriptExecutor::ExecuteScript(const HostID& host_id, params.user_gesture = user_gesture; // Handler handles IPCs and deletes itself on completion. - new Handler(script_observers_, web_contents_, params, frame_scope, callback); + new Handler(script_observers_, web_contents_, params, frame_scope, frame_id, + callback); } } // namespace extensions diff --git a/extensions/browser/script_executor.h b/extensions/browser/script_executor.h index 4f81b78..25e6c54 100644 --- a/extensions/browser/script_executor.h +++ b/extensions/browser/script_executor.h @@ -44,8 +44,8 @@ class ScriptExecutor { // The scope of the script injection across the frames. enum FrameScope { - TOP_FRAME, - ALL_FRAMES, + SINGLE_FRAME, + INCLUDE_SUB_FRAMES, }; // Whether to insert the script in about: frames when its origin matches @@ -82,6 +82,10 @@ class ScriptExecutor { // Executes a script. The arguments match ExtensionMsg_ExecuteCode_Params in // extension_messages.h (request_id is populated automatically). // + // The script will be executed in the frame identified by |frame_id| (which is + // an extension API frame ID). If |frame_scope| is INCLUDE_SUB_FRAMES, then + // the script will also be executed in all descendants of the frame. + // // |callback| will always be called even if the IPC'd renderer is destroyed // before a response is received (in this case the callback will be with a // failure and appropriate error message). @@ -89,6 +93,7 @@ class ScriptExecutor { ScriptType script_type, const std::string& code, FrameScope frame_scope, + int frame_id, MatchAboutBlank match_about_blank, UserScript::RunLocation run_at, WorldType world_type, diff --git a/extensions/common/api/extension_types.json b/extensions/common/api/extension_types.json index 29562ee..9f27590 100644 --- a/extensions/common/api/extension_types.json +++ b/extensions/common/api/extension_types.json @@ -45,7 +45,17 @@ "properties": { "code": {"type": "string", "optional": true, "description": "JavaScript or CSS code to inject.<br><br><b>Warning:</b><br>Be careful using the <code>code</code> parameter. Incorrect use of it may open your extension to <a href=\"https://en.wikipedia.org/wiki/Cross-site_scripting\">cross site scripting</a> attacks."}, "file": {"type": "string", "optional": true, "description": "JavaScript or CSS file to inject."}, - "allFrames": {"type": "boolean", "optional": true, "description": "If allFrames is <code>true</code>, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame."}, + "allFrames": { + "type": "boolean", + "optional": true, + "description": "If allFrames is <code>true</code>, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame. If <code>true</code> and <code>frameId</code> is set, then the code is inserted in the selected frame and all of its child frames." + }, + "frameId": { + "type": "integer", + "optional": true, + "minimum": 0, + "description": "The <a href='webNavigation#frame_ids'>frame</a> where the script or CSS should be injected. Defaults to 0 (the top-level frame)." + }, "matchAboutBlank": {"type": "boolean", "optional": true, "description": "If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is <code>false</code>."}, "runAt": { "$ref": "RunAt", diff --git a/extensions/renderer/programmatic_script_injector.cc b/extensions/renderer/programmatic_script_injector.cc index 13f0a20..e19a4c9 100644 --- a/extensions/renderer/programmatic_script_injector.cc +++ b/extensions/renderer/programmatic_script_injector.cc @@ -13,14 +13,15 @@ #include "extensions/common/error_utils.h" #include "extensions/common/extension_messages.h" #include "extensions/common/manifest_constants.h" +#include "extensions/common/permissions/api_permission.h" #include "extensions/common/permissions/permissions_data.h" #include "extensions/renderer/injection_host.h" +#include "extensions/renderer/renderer_extension_registry.h" #include "extensions/renderer/script_context.h" #include "third_party/WebKit/public/platform/WebString.h" #include "third_party/WebKit/public/web/WebDocument.h" #include "third_party/WebKit/public/web/WebLocalFrame.h" #include "third_party/WebKit/public/web/WebScriptSource.h" -#include "url/origin.h" namespace extensions { @@ -31,8 +32,10 @@ ProgrammaticScriptInjector::ProgrammaticScriptInjector( url_( ScriptContext::GetDataSourceURLForFrame(render_frame->GetWebFrame())), finished_(false) { - effective_url_ = ScriptContext::GetEffectiveDocumentURL( - render_frame->GetWebFrame(), url_, params.match_about_blank); + if (url_.SchemeIs(url::kAboutScheme)) { + origin_for_about_error_ = + render_frame->GetWebFrame()->securityOrigin().toString().utf8(); + } } ProgrammaticScriptInjector::~ProgrammaticScriptInjector() { @@ -130,16 +133,15 @@ void ProgrammaticScriptInjector::OnWillNotInject( std::string error; switch (reason) { case NOT_ALLOWED: - if (url_.SchemeIs(url::kAboutScheme)) { + if (!CanShowUrlInError()) { + error = manifest_errors::kCannotAccessPage; + } else if (!origin_for_about_error_.empty()) { error = ErrorUtils::FormatErrorMessage( manifest_errors::kCannotAccessAboutUrl, url_.spec(), - url::Origin(effective_url_).Serialize()); + origin_for_about_error_); } else { - // TODO(?) It would be nice to show kCannotAccessPageWithUrl here if - // this is triggered by an extension with tabs permission. See - // https://codereview.chromium.org/1414223005/diff/1/extensions/ - // common/manifest_constants.cc#newcode269 - error = manifest_errors::kCannotAccessPage; + error = ErrorUtils::FormatErrorMessage( + manifest_errors::kCannotAccessPageWithUrl, url_.spec()); } break; case EXTENSION_REMOVED: // no special error here. @@ -149,6 +151,17 @@ void ProgrammaticScriptInjector::OnWillNotInject( Finish(error, render_frame); } +bool ProgrammaticScriptInjector::CanShowUrlInError() const { + if (params_->host_id.type() != HostID::EXTENSIONS) + return false; + const Extension* extension = + RendererExtensionRegistry::Get()->GetByID(params_->host_id.id()); + if (!extension) + return false; + return extension->permissions_data()->active_permissions().HasAPIPermission( + APIPermission::kTab); +} + UserScript::RunLocation ProgrammaticScriptInjector::GetRunLocation() const { return static_cast<UserScript::RunLocation>(params_->run_at); } diff --git a/extensions/renderer/programmatic_script_injector.h b/extensions/renderer/programmatic_script_injector.h index 159ff1c..a7af9ff 100644 --- a/extensions/renderer/programmatic_script_injector.h +++ b/extensions/renderer/programmatic_script_injector.h @@ -50,6 +50,9 @@ class ProgrammaticScriptInjector : public ScriptInjector { void OnWillNotInject(InjectFailureReason reason, content::RenderFrame* render_frame) override; + // Whether it is safe to include information about the URL in error messages. + bool CanShowUrlInError() const; + // Return the run location for this injector. UserScript::RunLocation GetRunLocation() const; @@ -63,10 +66,9 @@ class ProgrammaticScriptInjector : public ScriptInjector { // The url of the frame into which we are injecting. GURL url_; - // The URL of the frame's origin. This is usually identical to |url_|, but - // could be different for e.g. about:blank URLs. Do not use this value to make - // security decisions, to avoid race conditions (e.g. due to navigation). - GURL effective_url_; + // The serialization of the frame's origin if the frame is an about:-URL. This + // is used to provide user-friendly messages. + std::string origin_for_about_error_; // The results of the script execution. base::ListValue results_; |