[STRAW MAN]
Bug 1208775: [webext] Make `this` and `window` consistent in content script scopes.
--- a/js/xpconnect/idl/mozIJSSubScriptLoader.idl
+++ b/js/xpconnect/idl/mozIJSSubScriptLoader.idl
@@ -34,16 +34,21 @@ interface mozIJSSubScriptLoader : nsISup
* In JS, the signature looks like:
* rv = loadSubScript (url, optionsObject)
* @param url the url of the sub-script, it MUST be either a file:,
* resource:, or chrome: url, and MUST be local.
* @param optionsObject an object with parameters. Valid parameters are:
* - charset: specifying the character encoding of the file (default: ASCII)
* - target: an object to evaluate onto (default: global object of the caller)
* - ignoreCache: if set to true, will bypass the cache for reading the file.
+ * - targetGlobal: if set, code is evaluated in the context of the given global,
+ * rather than in the scope of the unwrapped target object. If
+ * the target object does not already belong to that global, it
+ * will be wrapped in the appropriate security wrappers for the
+ * target.
* @retval rv the value returned by the sub-script
*/
[implicit_jscontext]
jsval loadSubScriptWithOptions(in AString url, in jsval options);
/*
* Compiles a JS script off the main thread and calls back the
* observer once it's done.
--- a/js/xpconnect/loader/mozJSSubScriptLoader.cpp
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
@@ -42,29 +42,32 @@ using namespace mozilla;
using namespace mozilla::dom;
class MOZ_STACK_CLASS LoadSubScriptOptions : public OptionsBase {
public:
explicit LoadSubScriptOptions(JSContext* cx = xpc_GetSafeJSContext(),
JSObject* options = nullptr)
: OptionsBase(cx, options)
, target(cx)
+ , targetGlobal(cx)
, charset(NullString())
, ignoreCache(false)
, async(false)
{ }
virtual bool Parse() {
return ParseObject("target", &target) &&
+ ParseObject("targetGlobal", &targetGlobal) &&
ParseString("charset", charset) &&
ParseBoolean("ignoreCache", &ignoreCache) &&
ParseBoolean("async", &async);
}
RootedObject target;
+ RootedObject targetGlobal;
nsString charset;
bool ignoreCache;
bool async;
};
/* load() error msgs, XXX localize? */
#define LOAD_ERROR_NOSERVICE "Error creating IO Service."
@@ -579,21 +582,32 @@ mozJSSubScriptLoader::DoLoadSubScriptWit
if (options.target)
targetObj = options.target;
// Remember an object out of the calling compartment so that we
// can properly wrap the result later.
nsCOMPtr<nsIPrincipal> principal = mSystemPrincipal;
RootedObject result_obj(cx, targetObj);
- targetObj = JS_FindCompilationScope(cx, targetObj);
- if (!targetObj)
- return NS_ERROR_FAILURE;
+
+ if (options.targetGlobal) {
+ RootedObject scope(cx, JS_FindCompilationScope(cx, options.targetGlobal));
+ JSAutoCompartment ac(cx, scope);
- if (targetObj != result_obj)
+ if (!JS_WrapObject(cx, &targetObj))
+ return NS_ERROR_FAILURE;
+ } else {
+ // If we're passed a cross-compartment wrapper or window proxy, and we're asked to unwrap
+ // it, do so, and evaluate the script in the unwrapped target scope.
+ targetObj = JS_FindCompilationScope(cx, targetObj);
+ if (!targetObj)
+ return NS_ERROR_FAILURE;
+ }
+
+ if (options.targetGlobal || targetObj != result_obj)
principal = GetObjectPrincipal(targetObj);
JSAutoCompartment ac(cx, targetObj);
/* load up the url. From here on, failures are reflected as ``custom''
* js exceptions */
nsCOMPtr<nsIURI> uri;
nsAutoCString uriStr;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -187,19 +187,20 @@ Script.prototype = {
// `document_idle` state.
if (AppConstants.platform == "gonk" && scheduled != "document_idle") {
Cu.reportError(`Script injection: ignoring ${url} at ${scheduled}`);
continue;
}
url = extension.baseURI.resolve(url);
let options = {
- target: sandbox,
+ target: sandbox.window,
charset: "UTF-8",
async: AppConstants.platform == "gonk",
+ targetGlobal: sandbox,
};
try {
result = Services.scriptloader.loadSubScriptWithOptions(url, options);
} catch (e) {
Cu.reportError(e);
this.deferred.reject(e.message);
}
}
@@ -286,16 +287,21 @@ class ExtensionContext extends BaseConte
});
} else {
this.sandbox = Cu.Sandbox(prin, {
sandboxPrototype: contentWindow,
wantXrays: true,
isWebExtensionContentScript: true,
wantGlobalProperties: ["XMLHttpRequest"],
});
+
+ // Copy the sandbox's semi-privileged XMLHttpRequest constructor
+ // to the window security wrapper, which is the execution context
+ // for content scripts.
+ Cu.evalInSandbox(`window.XMLHttpRequest = XMLHttpRequest;`, this.sandbox);
}
let delegate = {
getSender(context, target, sender) {
// Nothing to do here.
},
};
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -23,16 +23,17 @@ support-files =
file_ext_test_api_injection.js
file_permission_xhr.html
[test_ext_simple.html]
[test_ext_schema.html]
[test_ext_geturl.html]
[test_ext_contentscript.html]
skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
+[test_ext_contentscript_scope.html]
[test_ext_contentscript_create_iframe.html]
[test_ext_contentscript_api_injection.html]
[test_ext_i18n_css.html]
[test_ext_generate.html]
[test_ext_localStorage.html]
[test_ext_onmessage_removelistener.html]
[test_ext_notifications.html]
[test_ext_permission_xhr.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_scope.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script scopes</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_scope() {
+ function background() {
+ browser.runtime.onMessage.addListener(([msg, ...args]) => {
+ if (msg == "content-script-complete") {
+ browser.test.notifyPass(msg);
+ } else if (msg == "assert-true") {
+ browser.test.assertTrue(...args);
+ } else if (msg == "assert-eq") {
+ browser.test.assertEq(...args);
+ } else {
+ browser.test.fail(`Unexpected message: ${msg}`);
+ }
+ });
+ }
+
+ function contentScript() {
+ /* eslint-disable mozilla/var-only-at-top-level */
+
+ function assertTrue(val, msg) {
+ browser.runtime.sendMessage(["assert-true", Boolean(val), msg]);
+ }
+ function assertEq(...args) {
+ browser.runtime.sendMessage(["assert-eq", ...args]);
+ }
+
+ assertTrue(!("chrome" in window), "No window.chrome property");
+ assertTrue(!("browser" in window), "No window.browser property");
+
+ assertTrue(!("chrome" in this), "No this.chrome property");
+ assertTrue(!("browser" in this), "No this.browser property");
+
+ assertTrue(this == window, "this == window");
+ assertTrue(this === window, "this === window");
+
+ var foo = "bar";
+ assertTrue("foo" in this, "this.foo is present");
+ assertTrue("foo" in window, "window.foo is present");
+
+ assertEq("bar", foo, "foo is correct");
+ assertEq("bar", this.foo, "this.foo is correct");
+ assertEq("bar", window.foo, "window.foo is correct");
+
+ for (let varName of ["foo", "chrome", "browser"]) {
+ assertTrue(window.wrappedJSObject.eval(`!("${varName}" in window)`),
+ `No window.${varName} property in unprivileged context`);
+ }
+
+ chrome.runtime.sendMessage(["content-script-complete"]);
+
+ /* eslint-enable mozilla/var-only-at-top-level */
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ },
+ background: `(${background})()`,
+
+ files: {
+ // We need to have a raw script rather than a self-invoking function,
+ // here, for various tests to be valid.
+ "content_script.js": String(contentScript).replace(/^.*?\{([^]*)\}$/, "$1"),
+ },
+ });
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield extension.awaitFinish("content-script-complete");
+
+ win.close();
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>