--- a/browser/components/extensions/ext-bookmarks.js
+++ b/browser/components/extensions/ext-bookmarks.js
@@ -74,17 +74,17 @@ function convert(result) {
node.url = result.url.href; // Output is always URL object.
} else {
node.dateGroupModified = result.lastModified.getTime();
}
return node;
}
-extensions.registerSchemaAPI("bookmarks", "bookmarks", (extension, context) => {
+extensions.registerSchemaAPI("bookmarks", (extension, context) => {
return {
bookmarks: {
get: function(idOrIdList) {
let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];
return Task.spawn(function* () {
let bookmarks = [];
for (let id of list) {
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -221,17 +221,17 @@ extensions.on("manifest_browser_action",
extensions.on("shutdown", (type, extension) => {
if (browserActionMap.has(extension)) {
browserActionMap.get(extension).shutdown();
browserActionMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
-extensions.registerSchemaAPI("browserAction", null, (extension, context) => {
+extensions.registerSchemaAPI("browserAction", (extension, context) => {
return {
browserAction: {
onClicked: new EventManager(context, "browserAction.onClicked", fire => {
let listener = () => {
let tab = TabManager.activeTab;
fire(TabManager.convert(extension, tab));
};
BrowserAction.for(extension).on("click", listener);
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -219,17 +219,17 @@ extensions.on("shutdown", (type, extensi
let commandsList = commandsMap.get(extension);
if (commandsList) {
commandsList.unregister();
commandsMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
-extensions.registerSchemaAPI("commands", null, (extension, context) => {
+extensions.registerSchemaAPI("commands", (extension, context) => {
return {
commands: {
getAll() {
let commands = commandsMap.get(extension).commands;
return Promise.resolve(Array.from(commands, ([name, command]) => {
return ({
name,
description: command.description,
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -465,17 +465,17 @@ extensions.on("shutdown", (type, extensi
gContextMenuMap.delete(extension);
if (--gExtensionCount == 0) {
Services.obs.removeObserver(contextMenuObserver,
"on-build-contextmenu");
}
});
/* eslint-enable mozilla/balanced-listeners */
-extensions.registerSchemaAPI("contextMenus", "contextMenus", (extension, context) => {
+extensions.registerSchemaAPI("contextMenus", (extension, context) => {
return {
contextMenus: {
create: function(createProperties, callback) {
let menuItem = new MenuItem(extension, context, createProperties);
gContextMenuMap.get(extension).set(menuItem.id, menuItem);
if (callback) {
runSafe(context, callback);
}
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -113,17 +113,17 @@ function getObserver() {
},
};
EventEmitter.decorate(_observer);
PlacesUtils.history.addObserver(_observer, false);
}
return _observer;
}
-extensions.registerSchemaAPI("history", "history", (extension, context) => {
+extensions.registerSchemaAPI("history", (extension, context) => {
return {
history: {
addUrl: function(details) {
let transition, date;
try {
transition = getTransitionType(details.transition);
} catch (error) {
return Promise.reject({message: error.message});
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -200,17 +200,17 @@ extensions.on("shutdown", (type, extensi
/* eslint-enable mozilla/balanced-listeners */
PageAction.for = extension => {
return pageActionMap.get(extension);
};
global.pageActionFor = PageAction.for;
-extensions.registerSchemaAPI("pageAction", null, (extension, context) => {
+extensions.registerSchemaAPI("pageAction", (extension, context) => {
return {
pageAction: {
onClicked: new EventManager(context, "pageAction.onClicked", fire => {
let listener = (evt, tab) => {
fire(TabManager.convert(extension, tab));
};
let pageAction = PageAction.for(extension);
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -256,17 +256,17 @@ let tabListener = {
awaitTabReady(tab) {
return new Promise((resolve, reject) => {
this.tabReadyPromises.set(tab, {resolve, reject});
});
},
};
-extensions.registerSchemaAPI("tabs", null, (extension, context) => {
+extensions.registerSchemaAPI("tabs", (extension, context) => {
let self = {
tabs: {
onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
let tab = event.originalTarget;
let tabId = TabManager.getId(tab);
let windowId = WindowManager.getId(tab.ownerDocument.defaultView);
fire({tabId, windowId});
}).api(),
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -10,17 +10,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
} = ExtensionUtils;
-extensions.registerSchemaAPI("windows", null, (extension, context) => {
+extensions.registerSchemaAPI("windows", (extension, context) => {
return {
windows: {
onCreated:
new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
fire(WindowManager.convert(extension, window));
}).api(),
onRemoved:
--- a/browser/components/extensions/schemas/bookmarks.json
+++ b/browser/components/extensions/schemas/bookmarks.json
@@ -15,16 +15,17 @@
]
}]
}
]
},
{
"namespace": "bookmarks",
"description": "Use the <code>browser.bookmarks</code> API to create, organize, and otherwise manipulate bookmarks. Also see $(topic:override)[Override Pages], which you can use to create a custom Bookmark Manager page.",
+ "permissions": ["bookmarks"],
"types": [
{
"id": "BookmarkTreeNodeUnmodifiable",
"type": "string",
"enum": ["managed"],
"description": "Indicates the reason why this node is unmodifiable. The <var>managed</var> value indicates that this node was configured by the system administrator or by the custodian of a supervised user. Omitted if the node can be modified by the user and the extension (default)."
},
{
--- a/browser/components/extensions/schemas/context_menus.json
+++ b/browser/components/extensions/schemas/context_menus.json
@@ -15,16 +15,17 @@
]
}]
}
]
},
{
"namespace": "contextMenus",
"description": "Use the <code>browser.contextMenus</code> API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
+ "permissions": ["contextMenus"],
"properties": {
"ACTION_MENU_TOP_LEVEL_LIMIT": {
"value": 6,
"description": "The maximum number of top level extension items that can be added to an extension action context menu. Any items beyond this limit will be ignored."
}
},
"types": [
{
--- a/browser/components/extensions/schemas/history.json
+++ b/browser/components/extensions/schemas/history.json
@@ -15,16 +15,17 @@
]
}]
}
]
},
{
"namespace": "history",
"description": "Use the <code>browser.history</code> API to interact with the browser's record of visited pages. You can add, remove, and query for URLs in the browser's history. To override the history page with your own version, see $(topic:override)[Override Pages].",
+ "permissions": ["history"],
"types": [
{
"id": "TransitionType",
"type": "string",
"enum": ["link", "typed", "auto_bookmark", "auto_subframe", "manual_subframe", "generated", "auto_toplevel", "form_submit", "reload", "keyword", "keyword_generated"],
"description": "The $(topic:transition-types)[transition type] for this visit from its referrer."
},
{
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -61,19 +61,19 @@
"windowId": {"type": "integer", "minimum": 0, "description": "The ID of the window the tab is contained within."},
"openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
"selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
"highlighted": {"type": "boolean", "description": "Whether the tab is highlighted."},
"active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
"pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
"audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
"mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
- "url": {"type": "string", "optional": true, "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
- "title": {"type": "string", "optional": true, "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
- "favIconUrl": {"type": "string", "optional": true, "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
+ "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
+ "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
+ "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
"status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
"incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
"width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
"height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."},
"sessionId": {"unsupported": true, "type": "string", "optional": true, "description": "The session ID used to uniquely identify a Tab obtained from the $(ref:sessions) API."}
}
},
{
@@ -772,16 +772,17 @@
]
}
]
},
{
"name": "captureVisibleTab",
"type": "function",
"description": "Captures the visible area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[<all_urls>] permission to use this method.",
+ "permissions": ["<all_urls>"],
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "windowId",
"minimum": -2,
"optional": true,
"description": "The target window. Defaults to the $(topic:current-window)[current window]."
--- a/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_captureVisibleTab.js
@@ -143,28 +143,20 @@ add_task(function* testCaptureVisibleTab
});
add_task(function* testCaptureVisibleTabPermissions() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": ["tabs"],
},
- background: function(x) {
- browser.tabs.query({currentWindow: true, active: true}, tab => {
- browser.tabs.captureVisibleTab(tab.windowId).then(
- () => {
- browser.test.notifyFail("captureVisibleTabPermissions");
- },
- (e) => {
- browser.test.assertEq("The <all_urls> permission is required to use the captureVisibleTab API",
- e.message, "Expected permissions error message");
- browser.test.notifyPass("captureVisibleTabPermissions");
- });
- });
+ background() {
+ browser.test.assertFalse("captureVisibleTab" in browser.tabs,
+ 'Extension without "<all_tabs>" permission should not have access to captureVisibleTab');
+ browser.test.notifyPass("captureVisibleTabPermissions");
},
});
yield extension.startup();
yield extension.awaitFinish("captureVisibleTabPermissions");
yield extension.unload();
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -87,17 +87,17 @@ extensions.on("manifest_page_action", (t
extensions.on("shutdown", (type, extension) => {
if (pageActionMap.has(extension)) {
pageActionMap.get(extension).shutdown();
pageActionMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
-extensions.registerSchemaAPI("pageAction", null, (extension, context) => {
+extensions.registerSchemaAPI("pageAction", (extension, context) => {
return {
pageAction: {
onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
let listener = (event) => {
fire();
};
pageActionMap.get(extension).on("click", listener);
return () => {
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -173,18 +173,18 @@ var Management = {
},
// Same as above, but only register the API is the add-on has the
// given permission.
registerPrivilegedAPI(permission, api) {
this.apis.push({api, permission});
},
- registerSchemaAPI(namespace, permission, api) {
- this.schemaApis.push({namespace, permission, api});
+ registerSchemaAPI(namespace, api) {
+ this.schemaApis.push({namespace, api});
},
// Mash together into a single object all the APIs registered by the
// functions above. Return the merged object.
generateAPIs(extension, context, apis, namespaces = null) {
let obj = {};
// Recursively copy properties from source to dest.
@@ -536,16 +536,20 @@ GlobalManager = {
get principal() {
return context.principal;
},
get cloneScope() {
return context.cloneScope;
},
+ hasPermission(permission) {
+ return extension.hasPermission(permission);
+ },
+
callFunction(path, name, args) {
return findPathInObject(schemaApi, path)[name](...args);
},
callFunctionNoReturn(path, name, args) {
return findPathInObject(schemaApi, path)[name](...args);
},
@@ -562,23 +566,16 @@ GlobalManager = {
promise = findPathInObject(schemaApi, path)[name](...args);
} catch (e) {
promise = Promise.reject(e);
}
return context.wrapPromise(promise || Promise.resolve(), callback);
},
- shouldInject(namespace, name) {
- if (namespaces && namespaces.indexOf(namespace) == -1) {
- return false;
- }
- return findPathInObject(schemaApi, [namespace]) != null;
- },
-
getProperty(path, name) {
return findPathInObject(schemaApi, path)[name];
},
setProperty(path, name, value) {
findPathInObject(schemaApi, path)[name] = value;
},
@@ -1301,16 +1298,17 @@ Extension.prototype = extend(Object.crea
uuid: this.uuid,
manifest: this.manifest,
resourceURL: this.addonData.resourceURI.spec,
baseURL: this.baseURI.spec,
content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
webAccessibleResources: this.webAccessibleResources.serialize(),
whiteListedHosts: this.whiteListedHosts.serialize(),
localeData: this.localeData.serialize(),
+ permissions: this.permissions,
};
},
broadcast(msg, data) {
return new Promise(resolve => {
let count = Services.ppmm.childCount;
if (AppConstants.MOZ_NUWA_PROCESS) {
// The nuwa process is frozen, so don't expect it to answer.
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -739,16 +739,17 @@ DocumentManager = {
// Represents a browser extension in the content process.
function BrowserExtensionContent(data) {
this.id = data.id;
this.uuid = data.uuid;
this.data = data;
this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData));
this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
+ this.permissions = data.permissions;
this.localeData = new LocaleData(data.localeData);
this.manifest = data.manifest;
this.baseURI = Services.io.newURI(data.baseURL, null, null);
let uri = Services.io.newURI(data.resourceURL, null, null);
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1140,18 +1140,18 @@ class ChildAPIManager {
childId: this.id,
callId,
path, name, args,
});
return this.context.wrapPromise(deferred.promise, callback);
}
- shouldInject(namespace, name) {
- return this.namespaces.includes(namespace);
+ hasPermission(permission) {
+ return this.context.extension.permissions.has(permission);
}
getProperty(path, name) {
throw new Error("Not implemented");
}
setProperty(path, name, value) {
throw new Error("Not implemented");
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -93,16 +93,17 @@ class Context {
},
};
this.currentChoices = new Set();
this.choicePathIndex = 0;
let methods = ["addListener", "callFunction",
"callFunctionNoReturn", "callAsyncFunction",
+ "hasPermission",
"hasListener", "removeListener",
"getProperty", "setProperty",
"checkLoadURL", "logError"];
for (let method of methods) {
if (method in params) {
this[method] = params[method].bind(params);
}
}
@@ -143,16 +144,28 @@ class Context {
ssm.DISALLOW_INHERIT_PRINCIPAL);
} catch (e) {
return false;
}
return true;
}
/**
+ * Checks whether this context has the given permission.
+ *
+ * @param {string} permission
+ * The name of the permission to check.
+ *
+ * @returns {boolean} True if the context has the given permission.
+ */
+ hasPermission(permission) {
+ return false;
+ }
+
+ /**
* Returns an error result object with the given message, for return
* by Type normalization functions.
*
* If the context has a `currentTarget` value, this is prepended to
* the message to indicate the location of the error.
*
* @param {string} errorMessage
* The error message which will be displayed when this is the
@@ -1131,29 +1144,34 @@ class CallEntry extends Entry {
});
return fixedArgs;
}
}
// Represents a "function" defined in a schema namespace.
class FunctionEntry extends CallEntry {
- constructor(schema, path, name, type, unsupported, allowAmbiguousOptionalArguments, returns) {
+ constructor(schema, path, name, type, unsupported, allowAmbiguousOptionalArguments, returns, permissions) {
super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
this.unsupported = unsupported;
this.returns = returns;
+ this.permissions = permissions;
this.isAsync = type.isAsync;
}
inject(path, name, dest, context) {
if (this.unsupported) {
return;
}
+ if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
+ return;
+ }
+
let stub;
if (this.isAsync) {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
let callback = actuals.pop();
return context.callAsyncFunction(path, name, actuals, callback);
};
@@ -1171,35 +1189,40 @@ class FunctionEntry extends CallEntry {
};
}
Cu.exportFunction(stub, dest, {defineAs: name});
}
}
// Represents an "event" defined in a schema namespace.
class Event extends CallEntry {
- constructor(schema, path, name, type, extraParameters, unsupported) {
+ constructor(schema, path, name, type, extraParameters, unsupported, permissions) {
super(schema, path, name, extraParameters);
this.type = type;
this.unsupported = unsupported;
+ this.permissions = permissions;
}
checkListener(listener, context) {
let r = this.type.normalize(listener, context);
if (r.error) {
this.throwError(context, "Invalid listener");
}
return r.value;
}
inject(path, name, dest, context) {
if (this.unsupported) {
return;
}
+ if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
+ return;
+ }
+
let addStub = (listener, ...args) => {
listener = this.checkListener(listener, context);
let actuals = this.checkParameters(args, context);
return context.addListener(this.path, name, listener, actuals);
};
let removeStub = (listener) => {
listener = this.checkListener(listener, context);
@@ -1231,16 +1254,17 @@ this.Schemas = {
// Map[<schema-name> -> Map[<symbol-name> -> Entry]]
// 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;
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);
@@ -1321,17 +1345,17 @@ this.Schemas = {
// The path we pass in here is only used for error messages.
let functions = type.functions.map(fun => this.parseFunction(path.concat(type.id), fun));
return new SubModuleType(functions);
} else if (type.type == "object") {
let parseProperty = (type, extraProps = []) => {
return {
type: this.parseType(path, type,
- ["unsupported", "onError", ...extraProps]),
+ ["unsupported", "onError", "permissions", ...extraProps]),
optional: type.optional || false,
unsupported: type.unsupported || false,
onError: type.onError || null,
};
};
let properties = Object.create(null);
for (let propName of Object.keys(type.properties || {})) {
@@ -1416,20 +1440,22 @@ this.Schemas = {
throw new Error(`Unexpected type ${type.type}`);
}
},
parseFunction(path, fun) {
let f = new FunctionEntry(fun, path, fun.name,
this.parseType(path, fun,
["name", "unsupported", "returns",
+ "permissions",
"allowAmbiguousOptionalArguments"]),
fun.unsupported || false,
fun.allowAmbiguousOptionalArguments || false,
- fun.returns || null);
+ fun.returns || null,
+ fun.permissions || null);
return f;
},
loadType(namespaceName, type) {
if ("$extend" in type) {
this.extendType(namespaceName, type);
} else {
this.register(namespaceName, type.id, this.parseType([namespaceName], type, ["id"]));
@@ -1490,21 +1516,22 @@ this.Schemas = {
// We ignore these properties for now.
/* eslint-disable no-unused-vars */
let returns = event.returns;
let filters = event.filters;
/* eslint-enable no-unused-vars */
let type = this.parseType([namespaceName], event,
- ["name", "unsupported",
+ ["name", "unsupported", "permissions",
"extraParameters", "returns", "filters"]);
let e = new Event(event, [namespaceName], event.name, type, extras,
- event.unsupported || false);
+ event.unsupported || false,
+ event.permissions || null);
this.register(namespaceName, event.name, e);
},
init() {
if (this.initialized) {
return;
}
this.initialized = true;
@@ -1546,16 +1573,21 @@ this.Schemas = {
for (let fun of functions) {
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;
+ }
}
};
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
return readJSON(url).then(json => {
this.schemaJSON.set(url, json);
let data = Services.ppmm.initialProcessData;
@@ -1572,22 +1604,26 @@ this.Schemas = {
this.loadedUrls.add(url);
let schema = this.schemaJSON.get(url);
loadFromJSON(schema);
}
},
inject(dest, wrapperFuncs) {
+ let context = new Context(wrapperFuncs);
+
for (let [namespace, ns] of this.namespaces) {
+ if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
+ continue;
+ }
+
let obj = Cu.createObjectIn(dest, {defineAs: namespace});
for (let [name, entry] of ns) {
- if (wrapperFuncs.shouldInject(namespace, name)) {
- entry.inject([namespace], name, obj, new Context(wrapperFuncs));
- }
+ entry.inject([namespace], name, obj, context);
}
if (!Object.keys(obj).length) {
delete dest[namespace];
}
}
},
--- a/toolkit/components/extensions/ext-alarms.js
+++ b/toolkit/components/extensions/ext-alarms.js
@@ -86,17 +86,17 @@ extensions.on("shutdown", (type, extensi
for (let alarm of alarmsMap.get(extension).values()) {
alarm.clear();
}
alarmsMap.delete(extension);
alarmCallbacksMap.delete(extension);
});
/* eslint-enable mozilla/balanced-listeners */
-extensions.registerSchemaAPI("alarms", "alarms", (extension, context) => {
+extensions.registerSchemaAPI("alarms", (extension, context) => {
return {
alarms: {
create: function(name, alarmInfo) {
name = name || "";
let alarms = alarmsMap.get(extension);
if (alarms.has(name)) {
alarms.get(name).clear();
}
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -147,17 +147,17 @@ extensions.on("manifest_background", (ty
extensions.on("shutdown", (type, extension) => {
if (backgroundPagesMap.has(extension)) {
backgroundPagesMap.get(extension).shutdown();
backgroundPagesMap.delete(extension);
}
});
/* eslint-enable mozilla/balanced-listeners */
-extensions.registerSchemaAPI("extension", null, (extension, context) => {
+extensions.registerSchemaAPI("extension", (extension, context) => {
return {
extension: {
getBackgroundPage: function() {
return backgroundPagesMap.get(extension).contentWindow;
},
},
runtime: {
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -233,17 +233,17 @@ function* query(detailsIn, props, extens
while (enumerator.hasMoreElements()) {
let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
if (matches(cookie)) {
yield cookie;
}
}
}
-extensions.registerSchemaAPI("cookies", "cookies", (extension, context) => {
+extensions.registerSchemaAPI("cookies", (extension, context) => {
let self = {
cookies: {
get: function(details) {
// FIXME: We don't sort by length of path and creation time.
for (let cookie of query(details, ["url", "name", "storeId"], extension)) {
return Promise.resolve(convert(cookie));
}
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -383,17 +383,17 @@ function queryHelper(query) {
if (matchFn(download)) {
results.push(download);
}
}
return results;
});
}
-extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
+extensions.registerSchemaAPI("downloads", (extension, context) => {
return {
downloads: {
download(options) {
if (options.filename != null) {
if (options.filename.length == 0) {
return Promise.reject({message: "filename must not be empty"});
}
@@ -561,20 +561,16 @@ extensions.registerSchemaAPI("downloads"
promises.push(DownloadMap.erase(item));
results.push(item.id);
}
return Promise.all(promises).then(() => results);
});
},
open(downloadId) {
- if (!extension.hasPermission("downloads.open")) {
- throw new context.cloneScope.Error(
- "Permission denied because 'downloads.open' permission is missing.");
- }
return DownloadMap.lazyInit().then(() => {
let download = DownloadMap.fromId(downloadId).download;
if (download.succeeded) {
return download.launch();
} else {
return Promise.reject({message: "Download has not completed."});
}
}).catch((error) => {
--- a/toolkit/components/extensions/ext-extension.js
+++ b/toolkit/components/extensions/ext-extension.js
@@ -1,11 +1,11 @@
"use strict";
-extensions.registerSchemaAPI("extension", null, (extension, context) => {
+extensions.registerSchemaAPI("extension", (extension, context) => {
return {
extension: {
getURL: function(url) {
return extension.baseURI.resolve(url);
},
getViews: function(fetchProperties) {
let result = Cu.cloneInto([], context.cloneScope);
--- a/toolkit/components/extensions/ext-i18n.js
+++ b/toolkit/components/extensions/ext-i18n.js
@@ -2,17 +2,17 @@
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
detectLanguage,
} = ExtensionUtils;
-extensions.registerSchemaAPI("i18n", null, (extension, context) => {
+extensions.registerSchemaAPI("i18n", (extension, context) => {
return {
i18n: {
getMessage: function(messageName, substitutions) {
return extension.localizeMessage(messageName, substitutions, {cloneScope: context.cloneScope});
},
getAcceptLanguages: function() {
let result = extension.localeData.acceptLanguages;
--- a/toolkit/components/extensions/ext-idle.js
+++ b/toolkit/components/extensions/ext-idle.js
@@ -1,11 +1,11 @@
"use strict";
-extensions.registerSchemaAPI("idle", "idle", (extension, context) => {
+extensions.registerSchemaAPI("idle", (extension, context) => {
return {
idle: {
queryState: function(detectionIntervalInSeconds) {
return Promise.resolve("active");
},
},
};
});
--- a/toolkit/components/extensions/ext-notifications.js
+++ b/toolkit/components/extensions/ext-notifications.js
@@ -85,17 +85,17 @@ extensions.on("shutdown", (type, extensi
notification.clear();
}
notificationsMap.delete(extension);
});
/* eslint-enable mozilla/balanced-listeners */
var nextId = 0;
-extensions.registerSchemaAPI("notifications", "notifications", (extension, context) => {
+extensions.registerSchemaAPI("notifications", (extension, context) => {
return {
notifications: {
create: function(notificationId, options) {
if (!notificationId) {
notificationId = String(nextId++);
}
let notifications = notificationsMap.get(extension);
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -3,17 +3,17 @@
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
ignoreEvent,
} = ExtensionUtils;
-extensions.registerSchemaAPI("runtime", null, (extension, context) => {
+extensions.registerSchemaAPI("runtime", (extension, context) => {
return {
runtime: {
onStartup: new EventManager(context, "runtime.onStartup", fire => {
extension.onStartup = fire;
return () => {
extension.onStartup = null;
};
}).api(),
--- a/toolkit/components/extensions/ext-storage.js
+++ b/toolkit/components/extensions/ext-storage.js
@@ -5,17 +5,17 @@ var {classes: Cc, interfaces: Ci, utils:
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
"resource://gre/modules/ExtensionStorage.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
} = ExtensionUtils;
-extensions.registerSchemaAPI("storage", "storage", (extension, context) => {
+extensions.registerSchemaAPI("storage", (extension, context) => {
return {
storage: {
local: {
get: function(keys) {
return ExtensionStorage.get(extension.id, keys);
},
set: function(items) {
return ExtensionStorage.set(extension.id, items, context.cloneScope);
--- a/toolkit/components/extensions/ext-test.js
+++ b/toolkit/components/extensions/ext-test.js
@@ -20,17 +20,17 @@ extensions.on("shutdown", (type, extensi
extensions.on("test-message", (type, extension, ...args) => {
let handlers = messageHandlers.get(extension);
for (let handler of handlers) {
handler(...args);
}
});
/* eslint-enable mozilla/balanced-listeners */
-extensions.registerSchemaAPI("test", null, (extension, context) => {
+extensions.registerSchemaAPI("test", (extension, context) => {
return {
test: {
sendMessage: function(...args) {
extension.emit("test-message", ...args);
},
notifyPass: function(msg) {
extension.emit("test-done", true, msg);
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -145,17 +145,17 @@ function convertGetFrameResult(tabId, da
errorOccurred: data.errorOccurred,
url: data.url,
tabId,
frameId: ExtensionManagement.getFrameId(data.windowId),
parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
};
}
-extensions.registerSchemaAPI("webNavigation", "webNavigation", (extension, context) => {
+extensions.registerSchemaAPI("webNavigation", (extension, context) => {
return {
webNavigation: {
onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -93,17 +93,17 @@ function WebRequestEventManager(context,
};
};
return SingletonEventManager.call(this, context, name, register);
}
WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype);
-extensions.registerSchemaAPI("webRequest", "webRequest", (extension, context) => {
+extensions.registerSchemaAPI("webRequest", (extension, context) => {
return {
webRequest: {
onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(),
onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(),
onBeforeRedirect: new WebRequestEventManager(context, "onBeforeRedirect").api(),
onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
--- a/toolkit/components/extensions/schemas/alarms.json
+++ b/toolkit/components/extensions/schemas/alarms.json
@@ -1,15 +1,16 @@
// Copyright (c) 2012 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": "alarms",
+ "permissions": ["alarms"],
"types": [
{
"id": "Alarm",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of this alarm."
--- a/toolkit/components/extensions/schemas/cookies.json
+++ b/toolkit/components/extensions/schemas/cookies.json
@@ -15,16 +15,17 @@
]
}]
}
]
},
{
"namespace": "cookies",
"description": "Use the <code>browser.cookies</code> API to query and modify cookies, and to be notified when they change.",
+ "permissions": ["cookies"],
"types": [
{
"id": "Cookie",
"type": "object",
"description": "Represents information about an HTTP cookie.",
"properties": {
"name": {"type": "string", "description": "The name of the cookie."},
"value": {"type": "string", "description": "The value of the cookie."},
--- a/toolkit/components/extensions/schemas/downloads.json
+++ b/toolkit/components/extensions/schemas/downloads.json
@@ -12,16 +12,17 @@
"downloads.shelf"
]
}]
}
]
},
{
"namespace": "downloads",
+ "permissions": ["downloads"],
"types": [
{
"id": "FilenameConflictAction",
"type": "string",
"enum": [
"uniquify",
"overwrite",
"prompt"
@@ -555,16 +556,17 @@
}
]
},
{
"name": "open",
"type": "function",
"async": "callback",
"description": "Open the downloaded file.",
+ "permissions": ["downloads.open"],
"parameters": [
{
"name": "downloadId",
"type": "integer"
},
{
"name": "callback",
"type": "function",
--- a/toolkit/components/extensions/schemas/idle.json
+++ b/toolkit/components/extensions/schemas/idle.json
@@ -1,16 +1,17 @@
// Copyright (c) 2012 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": "idle",
"description": "Use the <code>browser.idle</code> API to detect when the machine's idle state changes.",
+ "permissions": ["idle"],
"types": [
{
"id": "IdleState",
"type": "string",
"enum": ["active", "idle", "locked"]
}
],
"functions": [
--- a/toolkit/components/extensions/schemas/notifications.json
+++ b/toolkit/components/extensions/schemas/notifications.json
@@ -1,15 +1,16 @@
// Copyright (c) 2012 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": "notifications",
+ "permissions": ["notifications"],
"types": [
{
"id": "TemplateType",
"type": "string",
"enum": [
"basic",
"image",
"list",
--- a/toolkit/components/extensions/schemas/storage.json
+++ b/toolkit/components/extensions/schemas/storage.json
@@ -1,16 +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",
"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": {
"type": "any",
"description": "The old value of the item, if there was an old value.",
--- a/toolkit/components/extensions/schemas/web_navigation.json
+++ b/toolkit/components/extensions/schemas/web_navigation.json
@@ -15,16 +15,17 @@
]
}]
}
]
},
{
"namespace": "webNavigation",
"description": "Use the <code>browser.webNavigation</code> API to receive notifications about the status of navigation requests in-flight.",
+ "permissions": ["webNavigation"],
"types": [
{
"id": "TransitionType",
"type": "string",
"enum": ["link", "typed", "auto_bookmark", "auto_subframe", "manual_subframe", "generated", "start_page", "form_submit", "reload", "keyword", "keyword_generated"],
"description": "Cause of the navigation. The same transition types as defined in the history API are used. These are the same transition types as defined in the $(topic:transition_types)[history API] except with <code>\"start_page\"</code> in place of <code>\"auto_toplevel\"</code> (for backwards compatibility)."
},
{
--- a/toolkit/components/extensions/schemas/web_request.json
+++ b/toolkit/components/extensions/schemas/web_request.json
@@ -16,16 +16,17 @@
]
}]
}
]
},
{
"namespace": "webRequest",
"description": "Use the <code>browser.webRequest</code> API to observe and analyze traffic and to intercept, block, or modify requests in-flight.",
+ "permissions": ["webRequest"],
"properties": {
"MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES": {
"value": 20,
"description": "The maximum number of times that <code>handlerBehaviorChanged</code> can be called per 10 minute sustained interval. <code>handlerBehaviorChanged</code> is an expensive function call that shouldn't be called often."
}
},
"types": [
{
--- a/toolkit/components/extensions/test/mochitest/test_ext_downloads.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_downloads.html
@@ -39,23 +39,19 @@ add_task(function* test_downloads_api_na
info("extension loaded");
yield extension.awaitFinish("downloads tests");
yield extension.unload();
info("extension unloaded");
});
add_task(function* test_downloads_open_permission() {
function backgroundScript() {
- browser.downloads.open(10, () => {
- let error = chrome.runtime.lastError;
- browser.test.assertTrue(!!error, "`downloads.open` raises an error.");
- browser.test.assertTrue(error.message.indexOf("Permission denied") > -1,
- "`downloads.open` permission is required.");
- browser.test.notifyPass("downloads tests");
- });
+ browser.test.assertFalse("open" in browser.downloads,
+ "`downloads.open` permission is required.");
+ browser.test.notifyPass("downloads tests");
}
let extensionData = {
background: "(" + backgroundScript.toString() + ")()",
manifest: {
permissions: ["downloads"],
},
};
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -336,28 +336,16 @@ let json = [
type: "function",
extraParameters: [{
name: "filter",
type: "integer",
}],
},
],
},
- {
- namespace: "inject",
- properties: {
- PROP1: {value: "should inject"},
- },
- },
- {
- namespace: "do-not-inject",
- properties: {
- PROP1: {value: "should not inject"},
- },
- },
];
let tallied = null;
function tally(kind, ns, name, args) {
tallied = [kind, ns, name, args];
}
@@ -373,16 +361,18 @@ function checkErrors(errors) {
for (let [i, error] of errors.entries()) {
do_check_true(i in talliedErrors && talliedErrors[i].includes(error),
`${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`);
}
talliedErrors.length = 0;
}
+let permissions = new Set();
+
let wrapper = {
url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
checkLoadURL(url) {
return !url.startsWith("chrome:");
},
preprocessors: {
@@ -390,30 +380,30 @@ let wrapper = {
return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`);
},
},
logError(message) {
talliedErrors.push(message);
},
+ hasPermission(permission) {
+ return permissions.has(permission);
+ },
+
callFunction(path, name, args) {
let ns = path.join(".");
tally("call", ns, name, args);
},
callFunctionNoReturn(path, name, args) {
let ns = path.join(".");
tally("call", ns, name, args);
},
- shouldInject(ns) {
- return ns != "do-not-inject";
- },
-
getProperty(path, name) {
let ns = path.join(".");
tally("get", ns, name);
},
setProperty(path, name, value) {
let ns = path.join(".");
tally("set", ns, name, value);
@@ -439,19 +429,16 @@ add_task(function* () {
let root = {};
Schemas.inject(root, wrapper);
do_check_eq(root.testing.PROP1, 20, "simple value property");
do_check_eq(root.testing.type1.VALUE1, "value1", "enum type");
do_check_eq(root.testing.type1.VALUE2, "value2", "enum type");
- do_check_eq("inject" in root, true, "namespace 'inject' should be injected");
- do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected");
-
root.testing.foo(11, true);
verify("call", "testing", "foo", [11, true]);
root.testing.foo(true);
verify("call", "testing", "foo", [null, true]);
root.testing.foo(null, true);
verify("call", "testing", "foo", [null, true]);
@@ -1076,8 +1063,98 @@ add_task(function* testChoices() {
/contain the required "baz" property, or be an array value/);
Assert.throws(() => root.choices.bar({baz: "x", quux: "y"}),
/not contain an unexpected "quux" property, or be an array value/);
Assert.throws(() => root.choices.bar({baz: "x", quux: "y", foo: "z"}),
/not contain the unexpected properties \[foo, quux\], or be an array value/);
});
+
+
+let permissionsJson = [
+ {namespace: "noPerms",
+
+ types: [],
+
+ functions: [
+ {
+ name: "noPerms",
+ type: "function",
+ parameters: [],
+ },
+
+ {
+ name: "fooPerm",
+ type: "function",
+ permissions: ["foo"],
+ parameters: [],
+ },
+ ]},
+
+ {namespace: "fooPerm",
+
+ permissions: ["foo"],
+
+ types: [],
+
+ functions: [
+ {
+ name: "noPerms",
+ type: "function",
+ parameters: [],
+ },
+
+ {
+ name: "fooBarPerm",
+ type: "function",
+ permissions: ["foo.bar"],
+ parameters: [],
+ },
+ ]},
+
+];
+
+add_task(function* testChoices() {
+ let url = "data:," + JSON.stringify(permissionsJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+
+ ok(!("fooPerm" in root.noPerms), "noPerms.fooPerm should not method exist");
+
+ ok(!("fooPerm" in root), "fooPerm namespace should not exist");
+
+
+ do_print('Add "foo" permission');
+ permissions.add("foo");
+
+ root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist");
+
+ equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
+ equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist");
+
+ ok(!("fooBarPerm" in root.fooPerm), "fooPerm.fooBarPerm method should not exist");
+
+
+ do_print('Add "foo.bar" permission');
+ permissions.add("foo.bar");
+
+ root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist");
+
+ equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
+ equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.fooPerm.fooBarPerm, "function", "noPerms.fooBarPerm method should exist");
+});