--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -70,16 +70,17 @@ Cu.import("resource://gre/modules/Extens
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
+const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
let schemaURLs = new Set();
if (!AppConstants.RELEASE_BUILD) {
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
}
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
@@ -137,16 +138,24 @@ var Management = new class extends Schem
}
return Promise.all(promises);
});
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
this.loadScript(value);
}
+ // TODO(robwu): This should be removed when addons can conceptually run in
+ // a separate process. This category should be used for content scripts only,
+ // but since the current content script API implementations (i18n, extension
+ // and runtime) are also needed in adddons, we re-use the category.
+ for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
+ this.loadScript(value);
+ }
+
this.initialized = promise;
return this.initialized;
}
registerSchemaAPI(namespace, envType, getAPI) {
if (envType == "addon_parent" || envType == "content_parent") {
super.registerSchemaAPI(namespace, envType, getAPI);
}
@@ -599,17 +608,22 @@ GlobalManager = {
promise = findPathInObject(apis, path)[name](...args);
} catch (e) {
promise = Promise.reject(e);
}
return context.wrapPromise(promise || Promise.resolve(), callback);
},
- shouldInject(namespace, name) {
+ shouldInject(namespace, name, restrictions) {
+ // Do not generate content script APIs, unless explicitly allowed.
+ if (context.envType === "content_parent" &&
+ (!restrictions || !restrictions.includes("content"))) {
+ return false;
+ }
return findPathInObject(apis, [namespace]) != null;
},
getProperty(pathObj, path, name) {
return findPathInObject(apis, path)[name];
},
setProperty(pathObj, path, name, value) {
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -431,26 +431,27 @@ class ExtensionContext extends BaseConte
this.chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "browser"});
// Sandboxes don't get Xrays for some weird compatibility
// reason. However, we waive here anyway in case that changes.
Cu.waiveXrays(this.sandbox).chrome = this.chromeObj;
let incognito = PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow);
- this.childManager = new ChildAPIManager(this, mm, ["storage", "test"], {
+ let localApis = {};
+ apiManager.generateAPIs(this, localApis);
+ this.childManager = new ChildAPIManager(this, mm, localApis, {
type: "content_script",
url,
incognito,
});
Schemas.inject(this.chromeObj, this.childManager);
injectAPI(api(this), this.chromeObj);
- injectAPI(apiManager.generateAPIs(this), this.chromeObj);
// This is an iframe with content script API enabled. (See Bug 1214658 for rationale)
if (isExtensionPage) {
Cu.waiveXrays(this.contentWindow).chrome = this.chromeObj;
Cu.waiveXrays(this.contentWindow).browser = this.chromeObj;
}
}
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1422,20 +1422,24 @@ function detectLanguage(text) {
let nextId = 1;
// We create one instance of this class for every extension context
// that needs to use remote APIs. It uses the message manager to
// communicate with the ParentAPIManager singleton in
// Extension.jsm. It handles asynchronous function calls as well as
// event listeners.
class ChildAPIManager {
- constructor(context, messageManager, namespaces, contextData) {
+ constructor(context, messageManager, localApis, contextData) {
this.context = context;
this.messageManager = messageManager;
- this.namespaces = namespaces;
+
+ // The root namespace of all locally implemented APIs. If an extension calls
+ // an API that does not exist in this object, then the implementation is
+ // delegated to the ParentAPIManager.
+ this.localApis = localApis;
let id = String(context.extension.id) + "." + String(context.contextId);
this.id = id;
let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
Object.assign(data, contextData);
messageManager.sendAsyncMessage("API:CreateProxyContext", data);
@@ -1480,57 +1484,103 @@ class ChildAPIManager {
this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
}
get cloneScope() {
return this.context.cloneScope;
}
callFunction(pathObj, path, name, args) {
+ if (pathObj) {
+ return pathObj[name](...args);
+ }
throw new Error("Not implemented");
}
callFunctionNoReturn(pathObj, path, name, args) {
+ if (pathObj) {
+ pathObj[name](...args);
+ return;
+ }
this.messageManager.sendAsyncMessage("API:Call", {
childId: this.id,
path, name, args,
});
}
callAsyncFunction(pathObj, path, name, args, callback) {
+ if (pathObj) {
+ let promise;
+ try {
+ promise = pathObj[name](...args) || Promise.resolve();
+ } catch (e) {
+ promise = Promise.reject(e);
+ }
+ return this.context.wrapPromise(promise, callback);
+ }
let callId = nextId++;
let deferred = PromiseUtils.defer();
this.callPromises.set(callId, deferred);
this.messageManager.sendAsyncMessage("API:Call", {
childId: this.id,
callId,
path, name, args,
});
return this.context.wrapPromise(deferred.promise, callback);
}
- shouldInject(namespace, name) {
- return this.namespaces.includes(namespace);
+ shouldInject(namespace, name, restrictions) {
+ // Do not generate content script APIs, unless explicitly allowed.
+ if (this.context.envType === "content_child" &&
+ (!restrictions || !restrictions.includes("content"))) {
+ return false;
+ }
+ let pathObj = this.localApis;
+ if (pathObj) {
+ for (let part of namespace.split(".")) {
+ pathObj = pathObj[part];
+ if (!pathObj) {
+ break;
+ }
+ }
+ if (pathObj && (name === null || name in pathObj)) {
+ return pathObj;
+ }
+ }
+
+ // No local API found, defer implementation to the parent.
+ return true;
}
hasPermission(permission) {
return this.context.extension.permissions.has(permission);
}
getProperty(pathObj, path, name) {
+ if (pathObj) {
+ return pathObj[name];
+ }
throw new Error("Not implemented");
}
setProperty(pathObj, path, name, value) {
+ if (pathObj) {
+ pathObj[name] = value;
+ return;
+ }
throw new Error("Not implemented");
}
addListener(pathObj, path, name, listener, args) {
+ if (pathObj) {
+ pathObj[name].addListener(listener, ...args);
+ return;
+ }
let ref = path.concat(name).join(".");
let set;
if (this.listeners.has(ref)) {
set = this.listeners.get(ref);
} else {
set = new Set();
this.listeners.set(ref, set);
}
@@ -1543,29 +1593,36 @@ class ChildAPIManager {
this.messageManager.sendAsyncMessage("API:AddListener", {
childId: this.id,
path, name, args,
});
}
}
removeListener(pathObj, path, name, listener) {
+ if (pathObj) {
+ pathObj[name].removeListener(listener);
+ return;
+ }
let ref = path.concat(name).join(".");
let set = this.listeners.get(ref) || new Set();
set.remove(listener);
if (set.size == 0) {
this.messageManager.sendAsyncMessage("Extension:RemoveListener", {
childId: this.id,
path, name,
});
}
}
hasListener(pathObj, path, name, listener) {
+ if (pathObj) {
+ return pathObj[name].hasListener(listener);
+ }
let ref = path.concat(name).join(".");
let set = this.listeners.get(ref) || new Set();
return set.has(listener);
}
}
/**
* This object loads the ext-*.js scripts that define the extension API.
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -357,19 +357,22 @@ class InjectionContext extends Context {
* return pathObj[name];
* // else the local API does not exist, so fall back or throw an error.
* }
*
* @abstract
* @param {string} namespace The namespace of the API. This may contain dots,
* e.g. in the case of "devtools.inspectedWindow".
* @param {string} [name] The name of the property in the namespace.
+ * `null` if we are checking whether the namespace should be injected.
+ * @param {Array} restrictions An arbitrary list of restrictions as declared
+ * by the schema for a given API node.
* @returns {*} An object with the property `name`, `true` or a falsey value.
*/
- shouldInject(namespace, name) {
+ shouldInject(namespace, name, restrictions) {
throw new Error("Not implemented");
}
/**
* Calls function `path`.`name` and returns its return value.
*
* @abstract
* @param {*} pathObj See `shouldInject`.
@@ -584,16 +587,23 @@ class Entry {
/**
* @property {string} [preprocessor]
* If set to a string value, and a preprocessor of the same is
* defined in the validation context, it will be applied to this
* value prior to any normalization.
*/
this.preprocessor = schema.preprocess || null;
+
+ /**
+ * @property {Array<string>} [restrictions]
+ * A list of restrictions to consider before generating the API.
+ * These are not parsed by the schema, but passed to `shouldInject`.
+ */
+ this.restrictions = schema.restrictions || null;
}
/**
* Preprocess the given value with the preprocessor declared in
* `preprocessor`.
*
* @param {*} value
* @param {Context} context
@@ -1223,18 +1233,18 @@ class SubModuleProperty extends Entry {
// form of sub-module properties, where "$ref" points to a
// SubModuleType containing a list of functions and "properties" is
// a list of additional simple properties.
//
// name: Name of the property stuff is being added to.
// namespaceName: Namespace in which the property lives.
// reference: Name of the type defining the functions to add to the property.
// properties: Additional properties to add to the module (unsupported).
- constructor(name, namespaceName, reference, properties) {
- super();
+ constructor(schema, name, namespaceName, reference, properties) {
+ super(schema);
this.name = name;
this.namespaceName = namespaceName;
this.reference = reference;
this.properties = properties;
}
inject(pathObj, path, name, dest, context) {
let obj = Cu.createObjectIn(dest, {defineAs: name});
@@ -1248,17 +1258,17 @@ class SubModuleProperty extends Entry {
}
if (!type || !(type instanceof SubModuleType)) {
throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
}
let functions = type.functions;
for (let fun of functions) {
let subpath = path.concat(name);
- let pathObj = context.shouldInject(subpath.join("."), fun.name);
+ let pathObj = context.shouldInject(subpath.join("."), fun.name, fun.restrictions || ns.defaultRestrictions);
if (pathObj) {
pathObj = pathObj === true ? null : pathObj;
fun.inject(pathObj, subpath, fun.name, obj, context);
}
}
// TODO: Inject this.properties.
}
@@ -1462,28 +1472,30 @@ this.Schemas = {
// This keeps track of all the schemas that have been loaded so far.
namespaces: new Map(),
register(namespaceName, symbol, value) {
let ns = this.namespaces.get(namespaceName);
if (!ns) {
ns = new Map();
ns.permissions = null;
+ ns.restrictions = null;
+ ns.defeaultRestrictions = null;
this.namespaces.set(namespaceName, ns);
}
ns.set(symbol, value);
},
// FIXME: Bug 1265371 - Refactor normalize and parseType in Schemas.jsm to reduce complexity
parseType(path, type, extraProperties = []) { // eslint-disable-line complexity
let allowedProperties = new Set(extraProperties);
// Do some simple validation of our own schemas.
function checkTypeProperties(...extra) {
- let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated", "preprocess"]);
+ let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated", "preprocess", "restrictions"]);
for (let prop of Object.keys(type)) {
if (!allowedSet.has(prop)) {
throw new Error(`Internal error: Namespace ${path.join(".")} has invalid type property "${prop}" in type "${type.id || JSON.stringify(type)}"`);
}
}
}
if ("choices" in type) {
@@ -1695,17 +1707,17 @@ this.Schemas = {
}
targetType.extend(parsed);
},
loadProperty(namespaceName, name, prop) {
if ("$ref" in prop) {
if (!prop.unsupported) {
- this.register(namespaceName, name, new SubModuleProperty(name, namespaceName, prop.$ref,
+ this.register(namespaceName, name, new SubModuleProperty(prop, name, namespaceName, prop.$ref,
prop.properties || {}));
}
} else if ("value" in prop) {
this.register(namespaceName, name, new ValueProperty(prop, name, prop.value));
} else {
// We ignore the "optional" attribute on properties since we
// don't inject anything here anyway.
let type = this.parseType([namespaceName], prop, ["optional", "writable"]);
@@ -1814,20 +1826,20 @@ this.Schemas = {
this.loadFunction(name, fun);
}
let events = namespace.events || [];
for (let event of events) {
this.loadEvent(name, event);
}
- if (namespace.permissions) {
- let ns = this.namespaces.get(name);
- ns.permissions = namespace.permissions;
- }
+ let ns = this.namespaces.get(name);
+ ns.permissions = namespace.permissions || null;
+ ns.restrictions = namespace.restrictions || null;
+ ns.defaultRestrictions = namespace.defaultRestrictions || null;
}
},
load(url) {
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
return readJSON(url).then(json => {
this.schemaJSON.set(url, json);
@@ -1863,19 +1875,23 @@ this.Schemas = {
inject(dest, wrapperFuncs) {
let context = new InjectionContext(wrapperFuncs);
for (let [namespace, ns] of this.namespaces) {
if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
continue;
}
+ if (!wrapperFuncs.shouldInject(namespace, null, ns.restrictions)) {
+ continue;
+ }
+
let obj = Cu.createObjectIn(dest, {defineAs: namespace});
for (let [name, entry] of ns) {
- let pathObj = context.shouldInject(namespace, name);
+ let pathObj = context.shouldInject(namespace, name, entry.restrictions || ns.defaultRestrictions);
if (pathObj) {
pathObj = pathObj === true ? null : pathObj;
entry.inject(pathObj, [namespace], name, obj, context);
}
}
// Remove the namespace object if it is empty
if (!Object.keys(obj).length) {
--- a/toolkit/components/extensions/schemas/storage.json
+++ b/toolkit/components/extensions/schemas/storage.json
@@ -1,15 +1,17 @@
// Copyright 2014 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.
[
{
"namespace": "storage",
+ "restrictions": ["content"],
+ "defaultRestrictions": ["content"],
"description": "Use the <code>browser.storage</code> API to store, retrieve, and track changes to user data.",
"permissions": ["storage"],
"types": [
{
"id": "StorageChange",
"type": "object",
"properties": {
"oldValue": {
--- a/toolkit/components/extensions/schemas/test.json
+++ b/toolkit/components/extensions/schemas/test.json
@@ -1,15 +1,17 @@
// Copyright 2014 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.
[
{
"namespace": "test",
+ "restrictions": ["content"],
+ "defaultRestrictions": ["content"],
"description": "none",
"functions": [
{
"name": "notifyFail",
"type": "function",
"description": "Notifies the browser process that test code running in the extension failed. This is only used for internal unit testing.",
"parameters": [
{"type": "string", "name": "message"}
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
@@ -88,16 +88,19 @@ add_task(function* testPathObj() {
let submoduleApi = {
_count: 0,
retObj() {
return "submoduleApi value " + (++submoduleApi._count);
},
};
let localWrapper = {
shouldInject(ns, name) {
+ if (name === null) {
+ return true; // Accept any namespace. Properties are checked below.
+ }
if (ns == "ret_obj") {
return localApi;
} else if (ns == "dot.obj") {
return dotApi;
} else if (ns == "ret_true" || ns == "ret_true_noval") {
return true;
} else if (ns == "ret_false" || ns == "ret_false_noval") {
return false;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_restrictions.js
@@ -0,0 +1,125 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+let schemaJson = [
+ {
+ namespace: "noRestrict",
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", restrictions: ["test_zero", "test_one"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, restrictions: ["numeric_one"]},
+ },
+ },
+ {
+ namespace: "defaultRestrict",
+ defaultRestrictions: ["test_two"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", restrictions: ["test_three"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, restrictions: ["numeric_two"]},
+ },
+ },
+ {
+ namespace: "withRestrict",
+ restrictions: ["test_four"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", restrictions: ["test_five"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, restrictions: ["numeric_three"]},
+ },
+ },
+ {
+ namespace: "withRestrictAndDefault",
+ restrictions: ["test_six"],
+ defaultRestrictions: ["test_seven"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", restrictions: ["test_eight"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, restrictions: ["numeric_four"]},
+ },
+ },
+ {
+ namespace: "with_submodule",
+ defaultRestrictions: ["test_nine"],
+ types: [{
+ id: "subtype",
+ type: "object",
+ functions: [{
+ name: "restrictNo",
+ type: "function",
+ parameters: [],
+ }, {
+ name: "restrictYes",
+ restrictions: ["test_ten"],
+ type: "function",
+ parameters: [],
+ }],
+ }],
+ properties: {
+ prop1: {$ref: "subtype"},
+ prop2: {$ref: "subtype", restrictions: ["test_eleven"]},
+ },
+ },
+];
+add_task(function* testPathObj() {
+ let url = "data:," + JSON.stringify(schemaJson);
+ yield Schemas.load(url);
+ let results = {};
+ let localWrapper = {
+ shouldInject(ns, name, restrictions) {
+ restrictions = restrictions ? restrictions.join() : "none";
+ name = name === null ? ns : ns + "." + name;
+ results[name] = restrictions;
+ return true;
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+
+ function verify(path, expected) {
+ let result = results[path];
+ do_check_eq(result, expected);
+ }
+
+ verify("noRestrict", "none");
+ verify("noRestrict.prop1", "none");
+ verify("noRestrict.prop2", "test_zero,test_one");
+ verify("noRestrict.prop3", "none");
+ verify("noRestrict.prop4", "numeric_one");
+
+ verify("defaultRestrict", "none");
+ verify("defaultRestrict.prop1", "test_two");
+ verify("defaultRestrict.prop2", "test_three");
+ verify("defaultRestrict.prop3", "test_two");
+ verify("defaultRestrict.prop4", "numeric_two");
+
+ verify("withRestrict", "test_four");
+ verify("withRestrict.prop1", "none");
+ verify("withRestrict.prop2", "test_five");
+ verify("withRestrict.prop3", "none");
+ verify("withRestrict.prop4", "numeric_three");
+
+ verify("withRestrictAndDefault", "test_six");
+ verify("withRestrictAndDefault.prop1", "test_seven");
+ verify("withRestrictAndDefault.prop2", "test_eight");
+ verify("withRestrictAndDefault.prop3", "test_seven");
+ verify("withRestrictAndDefault.prop4", "numeric_four");
+
+ verify("with_submodule", "none");
+ verify("with_submodule.prop1", "test_nine");
+ verify("with_submodule.prop1.restrictNo", "test_nine");
+ verify("with_submodule.prop1.restrictYes", "test_ten");
+ verify("with_submodule.prop2", "test_eleven");
+ // Note: test_nine inherits restrictions from the namespace, not from
+ // submodule. There is no "defaultRestrictions" for submodule types to not
+ // complicate things.
+ verify("with_submodule.prop1.restrictNo", "test_nine");
+ verify("with_submodule.prop1.restrictYes", "test_ten");
+});
+
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -40,15 +40,16 @@ skip-if = release_build
[test_ext_runtime_connect_no_receiver.js]
[test_ext_runtime_getPlatformInfo.js]
[test_ext_runtime_sendMessage.js]
[test_ext_runtime_sendMessage_errors.js]
[test_ext_runtime_sendMessage_no_receiver.js]
[test_ext_schemas.js]
[test_ext_schemas_api_injection.js]
[test_ext_schemas_pathObj.js]
+[test_ext_schemas_restrictions.js]
[test_ext_simple.js]
[test_ext_storage.js]
[test_getAPILevelForWindow.js]
[test_locale_converter.js]
[test_locale_data.js]
[test_native_messaging.js]
skip-if = os == "android"