summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorasargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-11-13 22:22:55 +0000
committerasargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-11-13 22:22:55 +0000
commit1307901f3bb5ab694530551f05e3898596acd854 (patch)
tree8a47789504223edce1995ee19afba3dcb7426f93
parent95ff808aa05c3de690fed32e92a940a6877cd3ed (diff)
downloadchromium_src-1307901f3bb5ab694530551f05e3898596acd854.zip
chromium_src-1307901f3bb5ab694530551f05e3898596acd854.tar.gz
chromium_src-1307901f3bb5ab694530551f05e3898596acd854.tar.bz2
Add stub functions for chrome.* APIs in content scripts.
Most of the extensions API is not supported in content scripts for security purposes. Instead the typical pattern is to use messaging between content scripts and a background page which executes them on behalf of the script. This is not immediately obvious to many extension developers, so this change adds an error message to help them better understand what's going on. BUG=26128 TEST=Try writing a content script that uses some of the extensions API stuff in chrome.tabs, chrome.windows, etc. You should see errors in the console telling you that it isn't supported in content scripts. Review URL: http://codereview.chromium.org/366024 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@31958 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/extensions/stubs_apitest.cc30
-rwxr-xr-xchrome/chrome.gyp2
-rwxr-xr-xchrome/common/extensions/api/extension_api.json6
-rw-r--r--chrome/renderer/resources/extension_process_bindings.js8
-rw-r--r--chrome/renderer/resources/renderer_extension_bindings.js50
-rw-r--r--chrome/renderer/user_script_slave.cc3
-rw-r--r--chrome/test/data/extensions/api_test/stubs/background.html16
-rw-r--r--chrome/test/data/extensions/api_test/stubs/content_script.js150
-rw-r--r--chrome/test/data/extensions/api_test/stubs/manifest.json11
9 files changed, 272 insertions, 4 deletions
diff --git a/chrome/browser/extensions/stubs_apitest.cc b/chrome/browser/extensions/stubs_apitest.cc
new file mode 100644
index 0000000..4ba81c7
--- /dev/null
+++ b/chrome/browser/extensions/stubs_apitest.cc
@@ -0,0 +1,30 @@
+// Copyright (c) 2009 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 "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/test/ui_test_utils.h"
+
+#if defined(OS_WIN) // TODO(asargent) get this working on linux
+// Tests that we throw errors when you try using extension APIs that aren't
+// supported in content scripts.
+//
+// If you have added a new API to extension_api.json and this test starts
+// failing, most likely you need to either mark it as "unprivileged" (if it
+// should be available in content scripts) or update the list of privileged APIs
+// in renderer_extension_bindings.js.
+IN_PROC_BROWSER_TEST_F(ExtensionApiTest, Stubs) {
+ ASSERT_TRUE(RunExtensionTest("stubs")) << message_;
+
+ // Navigate to a simple file:// page, which should get the content script
+ // injected and run the rest of the test.
+ FilePath test_dir;
+ ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &test_dir));
+ FilePath simple_html_path = test_dir.AppendASCII("simple.html");
+ ui_test_utils::NavigateToURL(browser(),
+ GURL(simple_html_path.value()));
+ ResultCatcher catcher;
+ ASSERT_TRUE(catcher.GetNextResult());
+}
+#endif
diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp
index 5cc7e42..f0b31a0 100755
--- a/chrome/chrome.gyp
+++ b/chrome/chrome.gyp
@@ -75,6 +75,7 @@
'browser/extensions/extension_toolstrip_apitest.cc',
'browser/extensions/isolated_world_apitest.cc',
'browser/extensions/page_action_apitest.cc',
+ 'browser/extensions/stubs_apitest.cc',
'browser/gtk/bookmark_manager_browsertest.cc',
'browser/net/ftp_browsertest.cc',
'browser/privacy_blacklist/blacklist_manager_browsertest.cc',
@@ -122,6 +123,7 @@
'browser/extensions/extension_toolstrip_apitest.cc',
'browser/extensions/isolated_world_apitest.cc',
'browser/extensions/page_action_apitest.cc',
+ 'browser/extensions/stubs_apitest.cc',
'browser/privacy_blacklist/blacklist_manager_browsertest.cc',
'browser/ssl/ssl_browser_tests.cc',
],
diff --git a/chrome/common/extensions/api/extension_api.json b/chrome/common/extensions/api/extension_api.json
index 6c09c51..86e93ed 100755
--- a/chrome/common/extensions/api/extension_api.json
+++ b/chrome/common/extensions/api/extension_api.json
@@ -1,6 +1,7 @@
[
{
"namespace": "extension",
+ "unprivileged": true,
"types": [
{
"id": "MessageSender",
@@ -55,6 +56,7 @@
{
"name": "connect",
"type": "function",
+ "unprivileged": true,
"description": "Attempts to connect to other listeners within the extension (listeners may be toolstrips or the extension's background page). This is primarily useful for content scripts connecting to their extension processes. Extensions may connect to content scripts embedded in tabs via <a href='broken'><var>chrome.tabs.connectToTab</var></a>.",
"parameters": [
{"type": "string", "name": "extensionId", "optional": true, "description": "The extension ID of the extension you want to connect to. If omitted, default is your own extension."},
@@ -75,6 +77,7 @@
{
"name": "sendRequest",
"type": "function",
+ "unprivileged": true,
"description": "Sends a single request to other listeners within the extension. Similar to chrome.extension.connect, but only sends a single request with an optional response.",
"parameters": [
{"type": "string", "name": "extensionId", "optional": true, "description": "The extension ID of the extension you want to connect to. If omitted, default is your own extension."},
@@ -96,6 +99,7 @@
{
"name": "getURL",
"type": "function",
+ "unprivileged": true,
"description": "Convert a relative path within an extension install directory to a fully-qualified URL.",
"parameters": [
{
@@ -160,6 +164,7 @@
{
"name": "onConnect",
"type": "function",
+ "unprivileged": true,
"description": "Fired when a connection is made from either an extension process or a content script.",
"parameters": [
{"$ref": "Port", "name": "port"}
@@ -176,6 +181,7 @@
{
"name": "onRequest",
"type": "function",
+ "unprivileged": true,
"description": "Fired when a request is sent from either an extension process or a content script.",
"parameters": [
{"name": "request", "type": "any", "description": "The request sent by the calling script."},
diff --git a/chrome/renderer/resources/extension_process_bindings.js b/chrome/renderer/resources/extension_process_bindings.js
index 85b65ea..2dbf7b4 100644
--- a/chrome/renderer/resources/extension_process_bindings.js
+++ b/chrome/renderer/resources/extension_process_bindings.js
@@ -272,7 +272,7 @@ var chrome = chrome || {};
}
chromeHidden.onLoad.addListener(function (extensionId) {
- chrome.initExtension(extensionId);
+ chrome.initExtension(extensionId, false);
// |apiFunctions| is a hash of name -> object that stores the
// name & definition of the apiFunction. Custom handling of api functions
@@ -317,7 +317,7 @@ var chrome = chrome || {};
module[functionDef.name] = bind(apiFunction, function() {
chromeHidden.validate(arguments, this.definition.parameters);
-
+
var retval;
if (this.handleRequest)
retval = this.handleRequest.apply(this, arguments);
@@ -498,6 +498,10 @@ var chrome = chrome || {};
setIconCommon(details, this.name, this.definition.parameters);
};
+ if (chrome.test) {
+ chrome.test.getApiDefinitions = GetExtensionAPIDefinition;
+ }
+
setupBrowserActionEvent(extensionId);
setupPageActionEvents(extensionId);
setupToolstripEvents(GetRenderViewId());
diff --git a/chrome/renderer/resources/renderer_extension_bindings.js b/chrome/renderer/resources/renderer_extension_bindings.js
index 718c414..5e28537 100644
--- a/chrome/renderer/resources/renderer_extension_bindings.js
+++ b/chrome/renderer/resources/renderer_extension_bindings.js
@@ -141,7 +141,7 @@ var chrome = chrome || {};
// This function is called on context initialization for both content scripts
// and extension contexts.
- chrome.initExtension = function(extensionId) {
+ chrome.initExtension = function(extensionId, warnOnPrivilegedApiAccess) {
delete chrome.initExtension;
chromeHidden.extensionId = extensionId;
@@ -200,5 +200,53 @@ var chrome = chrome || {};
chrome.extension.getURL = function(path) {
return "chrome-extension://" + extensionId + "/" + path;
};
+
+ if (warnOnPrivilegedApiAccess) {
+ setupApiStubs();
+ }
+ }
+
+ var notSupportedSuffix = " is not supported in content scripts. " +
+ "See the content scripts documentation for more details.";
+
+ // Setup to throw an error message when trying to access |name| on the chrome
+ // object. The |name| can be a dot-separated path.
+ function createStub(name) {
+ var module = chrome;
+ var parts = name.split(".");
+ for (var i = 0; i < parts.length - 1; i++) {
+ var nextPart = parts[i];
+ // Make sure an object at the path so far is defined.
+ module[nextPart] = module[nextPart] || {};
+ module = module[nextPart];
+ }
+ var finalPart = parts[parts.length-1];
+ module.__defineGetter__(finalPart, function() {
+ throw new Error("chrome." + name + notSupportedSuffix);
+ });
}
+
+ // Sets up stubs to throw a better error message for the common case of
+ // developers trying to call extension API's that aren't allowed to be
+ // called from content scripts.
+ function setupApiStubs() {
+ // TODO(asargent) It would be nice to eventually generate this
+ // programmatically from extension_api.json (there is already a browser test
+ // that should prevent it from getting stale).
+ var privileged = [
+ // Entire namespaces.
+ "bookmarks", "browserAction", "devtools", "experimental.extension",
+ "experimental.history", "experimental.popup", "i18n", "pageAction",
+ "pageActions", "tabs", "test", "toolstrip", "windows",
+
+ // Functions/events/properties within the extension namespace.
+ "extension.getBackgroundPage", "extension.getExtensionTabs",
+ "extension.getToolstrips", "extension.getViews", "extension.lastError",
+ "extension.onConnectExternal", "extension.onRequestExternal"
+ ];
+ for (var i = 0; i < privileged.length; i++) {
+ createStub(privileged[i]);
+ }
+ }
+
})();
diff --git a/chrome/renderer/user_script_slave.cc b/chrome/renderer/user_script_slave.cc
index 6aafd4a..c91e566 100644
--- a/chrome/renderer/user_script_slave.cc
+++ b/chrome/renderer/user_script_slave.cc
@@ -28,7 +28,8 @@ static const char kUserScriptTail[] = "\n})(window);";
// Sets up the chrome.extension module. This may be run multiple times per
// context, but the init method deletes itself after the first time.
static const char kInitExtension[] =
- "if (chrome.initExtension) chrome.initExtension('%s');";
+ "if (chrome.initExtension) chrome.initExtension('%s', true);";
+
int UserScriptSlave::GetIsolatedWorldId(const std::string& extension_id) {
typedef std::map<std::string, int> IsolatedWorldMap;
diff --git a/chrome/test/data/extensions/api_test/stubs/background.html b/chrome/test/data/extensions/api_test/stubs/background.html
new file mode 100644
index 0000000..b32d520
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/stubs/background.html
@@ -0,0 +1,16 @@
+<script>
+
+chrome.extension.onRequest.addListener(function(msg, sender, responseFunc) {
+ if (msg == "getApi") {
+ responseFunc(JSON.parse(chrome.test.getApiDefinitions()));
+ } else if (msg == "pass") {
+ chrome.test.notifyPass();
+ } else {
+ chrome.test.notifyFail("failed");
+ }
+});
+
+// On first install, send a success message so the test can continue.
+chrome.test.notifyPass();
+
+</script>
diff --git a/chrome/test/data/extensions/api_test/stubs/content_script.js b/chrome/test/data/extensions/api_test/stubs/content_script.js
new file mode 100644
index 0000000..0f9ec84
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/stubs/content_script.js
@@ -0,0 +1,150 @@
+
+// We ask the background page to get the extension API to test against. When it
+// responds we start the test.
+console.log("asking for api ...");
+chrome.extension.sendRequest("getApi", function(apis) {
+ console.log("got api response");
+ var privilegedPaths = [];
+ var unprivilegedPaths = [];
+ apis.forEach(function(module) {
+ var namespace = module.namespace;
+ if (!module.unprivileged) {
+ privilegedPaths.push(namespace);
+ return;
+ }
+
+ ["functions", "events"].forEach(function(section) {
+ if (typeof(module[section]) == "undefined")
+ return;
+ module[section].forEach(function(entry) {
+ var path = namespace + "." + entry.name;
+ if (entry.unprivileged) {
+ unprivilegedPaths.push(path);
+ } else {
+ privilegedPaths.push(path);
+ }
+ });
+ });
+
+ if (module.properties) {
+ for (var propName in module.properties) {
+ var path = namespace + "." + propName;
+ if (module.properties[propName].unprivileged) {
+ unprivilegedPaths.push(path);
+ } else {
+ privilegedPaths.push(path);
+ }
+ }
+ }
+ });
+ doTest(privilegedPaths, unprivilegedPaths);
+});
+
+
+// Tests whether missing properties of the chrome object correctly throw an
+// error on access. The path is a namespace or function/property/event etc.
+// within a namespace, and is dot-separated.
+function testPath(path, expectError) {
+ console.log("trying " + path);
+ var parts = path.split('.');
+
+ // Iterate over each component of the path, making sure all but the last part
+ // is defined. The last part should either be defined or throw an error on
+ // attempted access.
+ var module = chrome;
+ for (var i = 0; i < parts.length; i++) {
+ if (i < parts.length - 1) {
+ // Not the last component, so expect non-null / no exception.
+ try {
+ module = module[parts[i]];
+ } catch (err) {
+ console.log("testPath failed on subcomponent of " + path);
+ return false;
+ }
+ } else {
+ // This is the last component - we expect it to either be defined or
+ // to throw an error on access.
+ try {
+ if (typeof(module[parts[i]]) == "undefined") {
+ console.log(" fail (undefined and not throwing error): " +
+ path);
+ return false;
+ } else if (!expectError) {
+ console.log(" ok (defined): " + path);
+ return true;
+ }
+ } catch (err) {
+ if (!expectError) {
+ console.log(" fail (did not expect error): " + path);
+ return false;
+ }
+ var str = err.toString();
+ if (str.search("is not supported in content scripts") != -1) {
+ console.log(" ok (correct error thrown): " + path);
+ return true;
+ } else {
+ console.log(" fail (wrong error: '" + str + "')");
+ return false;
+ }
+ }
+ }
+ }
+ console.log(" fail (no error when we were expecting one): " + path);
+ return false;
+}
+
+function displayResult(status) {
+ var div = document.createElement("div");
+ div.innerHTML = "<h1>" + status + "</h2>";
+ document.body.appendChild(div);
+}
+
+function reportSuccess() {
+ displayResult("pass");
+ chrome.extension.sendRequest("pass");
+}
+
+function reportFailure() {
+ displayResult("fail");
+ // Let the "fail" show for a little while so you can see it when running
+ // browser_tests in the debugger.
+ setTimeout(function() {
+ chrome.extension.sendRequest("fail");
+ }, 1000);
+}
+
+// Runs over each string path in privilegedPaths and unprivilegedPaths, testing
+// to ensure a proper error is thrown on access or the path is defined.
+function doTest(privilegedPaths, unprivilegedPaths) {
+ console.log("starting");
+
+ if (!privilegedPaths || privilegedPaths.length < 1 || !unprivilegedPaths ||
+ unprivilegedPaths.length < 1) {
+ port.postMessage("fail");
+ return;
+ }
+
+ var failures = [];
+ var success = true;
+
+ // Returns a function that will test a path and record any failures.
+ function makeTestFunction(expectError) {
+ return function(path) {
+ if (!testPath(path, expectError)) {
+ success = false;
+ failures.push(path);
+ }
+ };
+ }
+ privilegedPaths.forEach(makeTestFunction(true));
+ unprivilegedPaths.forEach(makeTestFunction(false));
+
+ console.log(success ? "pass" : "fail");
+ if (success) {
+ reportSuccess();
+ } else {
+ console.log("failures on:\n" + failures.join("\n"));
+ reportFailure();
+ }
+}
+
diff --git a/chrome/test/data/extensions/api_test/stubs/manifest.json b/chrome/test/data/extensions/api_test/stubs/manifest.json
new file mode 100644
index 0000000..0149ae6
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/stubs/manifest.json
@@ -0,0 +1,11 @@
+{
+ "name": "Content Script Extension API Stubs Test",
+ "version": "1.0",
+ "background_page": "background.html",
+ "content_scripts": [
+ {
+ "matches": ["http://*/*", "file://*"],
+ "js":["content_script.js"]
+ }
+ ]
+}