new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-c-contextMenus.js
@@ -0,0 +1,158 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// If id is not specified for an item we use an integer.
+// This ID need only be unique within a single addon. Since all addon code that
+// can use this API runs in the same process, this local variable suffices.
+var gNextMenuItemID = 0;
+
+// Map[Extension -> Map[string or id, ContextMenusClickPropHandler]]
+var gPropHandlers = new Map();
+
+// The contextMenus API supports an "onclick" attribute in the create/update
+// methods to register a callback. This class manages these onclick properties.
+class ContextMenusClickPropHandler {
+ constructor(context) {
+ this.context = context;
+ // Map[string or integer -> callback]
+ this.onclickMap = new Map();
+ this.dispatchEvent = this.dispatchEvent.bind(this);
+ }
+
+ // A listener on contextMenus.onClicked that forwards the event to the only
+ // listener, if any.
+ dispatchEvent(info, tab) {
+ let onclick = this.onclickMap.get(info.menuItemId);
+ if (onclick) {
+ // No need for runSafe or anything because we are already being run inside
+ // an event handler -- the event is just being forwarded to the actual
+ // handler.
+ onclick(info, tab);
+ }
+ }
+
+ // Sets the `onclick` handler for the given menu item.
+ // The `onclick` function MUST be owned by `this.context`.
+ setListener(id, onclick) {
+ if (this.onclickMap.size === 0) {
+ this.context.childManager.getParentEvent("contextMenus.onClicked").addListener(this.dispatchEvent);
+ this.context.callOnClose(this);
+ }
+ this.onclickMap.set(id, onclick);
+
+ let propHandlerMap = gPropHandlers.get(this.context.extension);
+ if (!propHandlerMap) {
+ propHandlerMap = new Map();
+ } else {
+ // If the current callback was created in a different context, remove it
+ // from the other context.
+ let propHandler = propHandlerMap.get(id);
+ if (propHandler && propHandler !== this) {
+ propHandler.unsetListener(id);
+ }
+ }
+ propHandlerMap.set(id, this);
+ gPropHandlers.set(this.context.extension, propHandlerMap);
+ }
+
+ // Deletes the `onclick` handler for the given menu item.
+ // The `onclick` function MUST be owned by `this.context`.
+ unsetListener(id) {
+ if (!this.onclickMap.delete(id)) {
+ return;
+ }
+ if (this.onclickMap.size === 0) {
+ this.context.childManager.getParentEvent("contextMenus.onClicked").removeListener(this.dispatchEvent);
+ this.context.forgetOnClose(this);
+ }
+ let propHandlerMap = gPropHandlers.get(this.context.extension);
+ propHandlerMap.delete(id);
+ if (propHandlerMap.size === 0) {
+ gPropHandlers.delete(this.context.extension);
+ }
+ }
+
+ // Deletes the `onclick` handler for the given menu item, if any, regardless
+ // of the context where it was created.
+ unsetListenerFromAnyContext(id) {
+ let propHandlerMap = gPropHandlers.get(this.context.extension);
+ let propHandler = propHandlerMap && propHandlerMap.get(id);
+ if (propHandler) {
+ propHandler.unsetListener(id);
+ }
+ }
+
+ // Remove all `onclick` handlers of the extension.
+ deleteAllListenersFromExtension() {
+ let propHandlerMap = gPropHandlers.get(this.context.extension);
+ if (propHandlerMap) {
+ for (let [id, propHandler] of propHandlerMap) {
+ propHandler.unsetListener(id);
+ }
+ }
+ }
+
+ // Removes all `onclick` handlers from this context.
+ close() {
+ for (let id of this.onclickMap.keys()) {
+ this.unsetListener(id);
+ }
+ }
+}
+
+extensions.registerSchemaAPI("contextMenus", "addon_child", context => {
+ let onClickedProp = new ContextMenusClickPropHandler(context);
+
+ return {
+ contextMenus: {
+ create(createProperties, callback) {
+ if (createProperties.id === null) {
+ createProperties.id = ++gNextMenuItemID;
+ }
+ let {onclick} = createProperties;
+ delete createProperties.onclick;
+ context.childManager.callParentAsyncFunction("contextMenus.createInternal", [
+ createProperties,
+ ]).then(() => {
+ if (onclick) {
+ onClickedProp.setListener(createProperties.id, onclick);
+ }
+ if (callback) {
+ callback();
+ }
+ });
+ return createProperties.id;
+ },
+
+ update(id, updateProperties) {
+ let {onclick} = updateProperties;
+ delete updateProperties.onclick;
+ return context.childManager.callParentAsyncFunction("contextMenus.update", [
+ id,
+ updateProperties,
+ ]).then(() => {
+ if (onclick) {
+ onClickedProp.setListener(id, onclick);
+ } else if (onclick === null) {
+ onClickedProp.unsetListenerFromAnyContext(id);
+ }
+ // else onclick is not set so it should not be changed.
+ });
+ },
+
+ remove(id) {
+ onClickedProp.unsetListenerFromAnyContext(id);
+ return context.childManager.callParentAsyncFunction("contextMenus.remove", [
+ id,
+ ]);
+ },
+
+ removeAll() {
+ onClickedProp.deleteAllListenersFromExtension();
+
+ return context.childManager.callParentAsyncFunction("contextMenus.removeAll", []);
+ },
+ },
+ };
+});
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -5,17 +5,16 @@
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/MatchPattern.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var {
EventManager,
IconDetails,
- runSafe,
} = ExtensionUtils;
// Map[Extension -> Map[ID -> MenuItem]]
// Note: we want to enumerate all the menu items so
// this cannot be a weak map.
var gContextMenuMap = new Map();
// Map[Extension -> MenuItem]
@@ -188,19 +187,16 @@ var gMenuBuilder = {
item.checked = true;
}
item.tabManager.addActiveTabPermission();
let tab = item.tabManager.convert(contextData.tab);
let info = item.getClickInfo(contextData, wasChecked);
item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
- if (item.onclick) {
- runSafe(item.extContext, item.onclick, info, tab);
- }
});
return element;
},
handleEvent: function(event) {
if (this.xulMenu != event.target || event.type != "popuphidden") {
return;
@@ -254,19 +250,18 @@ function getContexts(contextData) {
if (contextData.onAudio) {
contexts.add("audio");
}
return contexts;
}
-function MenuItem(extension, extContext, createProperties, isRoot = false) {
+function MenuItem(extension, createProperties, isRoot = false) {
this.extension = extension;
- this.extContext = extContext;
this.children = [];
this.parent = null;
this.tabManager = TabManager.for(extension);
this.setDefaults();
this.setProps(createProperties);
if (!this.hasOwnProperty("_id")) {
this.id = gNextMenuItemID++;
@@ -370,17 +365,17 @@ MenuItem.prototype = {
}
this.children.splice(idx, 1);
child.parent = null;
},
get root() {
let extension = this.extension;
if (!gRootItems.has(extension)) {
- let root = new MenuItem(extension, this.context,
+ let root = new MenuItem(extension,
{title: extension.name},
/* isRoot = */ true);
gRootItems.set(extension, root);
}
return gRootItems.get(extension);
},
@@ -490,47 +485,43 @@ extensions.on("shutdown", (type, extensi
}
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("contextMenus", "addon_parent", context => {
let {extension} = context;
return {
contextMenus: {
- create: function(createProperties, callback) {
- let menuItem = new MenuItem(extension, context, createProperties);
+ createInternal: function(createProperties) {
+ // Note that the id is required by the schema. If the addon did not set
+ // it, the implementation of contextMenus.create in the child should
+ // have added it.
+ let menuItem = new MenuItem(extension, createProperties);
gContextMenuMap.get(extension).set(menuItem.id, menuItem);
- if (callback) {
- runSafe(context, callback);
- }
- return menuItem.id;
},
update: function(id, updateProperties) {
let menuItem = gContextMenuMap.get(extension).get(id);
if (menuItem) {
menuItem.setProps(updateProperties);
}
- return Promise.resolve();
},
remove: function(id) {
let menuItem = gContextMenuMap.get(extension).get(id);
if (menuItem) {
menuItem.remove();
}
- return Promise.resolve();
},
removeAll: function() {
let root = gRootItems.get(extension);
if (root) {
root.remove();
}
- return Promise.resolve();
},
onClicked: new EventManager(context, "contextMenus.onClicked", fire => {
let listener = (event, info, tab) => {
fire(info, tab);
};
extension.on("webext-contextmenu-menuitem-click", listener);
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -7,16 +7,17 @@ category webextension-scripts desktop-ru
category webextension-scripts history chrome://browser/content/ext-history.js
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
category webextension-scripts utils chrome://browser/content/ext-utils.js
category webextension-scripts windows chrome://browser/content/ext-windows.js
# scripts that must run in the same process as addon code.
category webextension-scripts-addon browserAction chrome://browser/content/ext-c-browserAction.js
+category webextension-scripts-addon contextMenus chrome://browser/content/ext-c-contextMenus.js
category webextension-scripts-addon pageAction chrome://browser/content/ext-c-pageAction.js
category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
# schemas
category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
category webextension-schemas commands chrome://browser/content/schemas/commands.json
category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -18,10 +18,11 @@ browser.jar:
content/browser/ext-contextMenus.js
content/browser/ext-desktop-runtime.js
content/browser/ext-history.js
content/browser/ext-pageAction.js
content/browser/ext-tabs.js
content/browser/ext-utils.js
content/browser/ext-windows.js
content/browser/ext-c-browserAction.js
+ content/browser/ext-c-contextMenus.js
content/browser/ext-c-pageAction.js
content/browser/ext-c-tabs.js
--- a/browser/components/extensions/schemas/context_menus.json
+++ b/browser/components/extensions/schemas/context_menus.json
@@ -202,16 +202,84 @@
"name": "callback",
"optional": true,
"description": "Called when the item has been created in the browser. If there were any problems creating the item, details will be available in $(ref:runtime.lastError).",
"parameters": []
}
]
},
{
+ "name": "createInternal",
+ "type": "function",
+ "allowedContexts": ["addon_parent_only"],
+ "async": "callback",
+ "description": "Identical to contextMenus.create, except: the 'id' field is required and allows an integer, 'onclick' is not allowed, and the method is async (and the return value is not a menu item ID).",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "createProperties",
+ "properties": {
+ "type": {
+ "$ref": "ItemType",
+ "optional": true
+ },
+ "id": {
+ "choices": [
+ { "type": "integer" },
+ { "type": "string" }
+ ]
+ },
+ "title": {
+ "type": "string",
+ "optional": true
+ },
+ "checked": {
+ "type": "boolean",
+ "optional": true
+ },
+ "contexts": {
+ "type": "array",
+ "items": {
+ "$ref": "ContextType"
+ },
+ "minItems": 1,
+ "optional": true
+ },
+ "parentId": {
+ "choices": [
+ { "type": "integer" },
+ { "type": "string" }
+ ],
+ "optional": true
+ },
+ "documentUrlPatterns": {
+ "type": "array",
+ "items": {"type": "string"},
+ "optional": true
+ },
+ "targetUrlPatterns": {
+ "type": "array",
+ "items": {"type": "string"},
+ "optional": true
+ },
+ "enabled": {
+ "type": "boolean",
+ "optional": true
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
"name": "update",
"type": "function",
"description": "Updates a previously created context menu item.",
"async": "callback",
"parameters": [
{
"choices": [
{ "type": "integer" },
@@ -242,17 +310,17 @@
"items": {
"$ref": "ContextType"
},
"minItems": 1,
"optional": true
},
"onclick": {
"type": "function",
- "optional": true,
+ "optional": "omit-key-if-missing",
"parameters": [
{
"name": "info",
"$ref": "contextMenusInternal.OnClickData"
},
{
"name": "tab",
"$ref": "tabs.Tab",
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -30,16 +30,17 @@ tags = webextensions
[browser_ext_commands_execute_browser_action.js]
[browser_ext_commands_execute_page_action.js]
[browser_ext_commands_getAll.js]
[browser_ext_commands_onCommand.js]
[browser_ext_contentscript_connect.js]
[browser_ext_contextMenus.js]
[browser_ext_contextMenus_checkboxes.js]
[browser_ext_contextMenus_icons.js]
+[browser_ext_contextMenus_onclick.js]
[browser_ext_contextMenus_radioGroups.js]
[browser_ext_contextMenus_uninstall.js]
[browser_ext_contextMenus_urlPatterns.js]
[browser_ext_currentWindow.js]
[browser_ext_getViews.js]
[browser_ext_incognito_popup.js]
[browser_ext_lastError.js]
[browser_ext_legacy_extension_context_contentscript.js]
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
@@ -29,16 +29,19 @@ add_task(function* () {
browser.contextMenus.remove(menuitemId);
},
});
browser.contextMenus.create({
title: "child",
});
+ browser.test.onMessage.addListener(() => {
+ browser.test.sendMessage("pong");
+ });
browser.test.notifyPass("contextmenus-icons");
},
});
let confirmContextMenuIcon = (rootElement) => {
let expectedURL = new RegExp(String.raw`^moz-extension://[^/]+/extension\.png$`);
let imageUrl = rootElement.getAttribute("image");
ok(expectedURL.test(imageUrl), "The context menu should display the extension icon next to the root element");
@@ -50,16 +53,20 @@ add_task(function* () {
let extensionMenu = yield openExtensionContextMenu();
let contextMenu = document.getElementById("contentAreaContextMenu");
let topLevelMenuItem = contextMenu.getElementsByAttribute("ext-type", "top-level-menu")[0];
confirmContextMenuIcon(topLevelMenuItem);
let childToDelete = extensionMenu.getElementsByAttribute("label", "child-to-delete")[0];
yield closeExtensionContextMenu(childToDelete);
+ // Now perform a roundtrip to the extension process to make sure that the
+ // click event has had a chance to fire.
+ extension.sendMessage("ping");
+ yield extension.awaitMessage("pong");
yield openExtensionContextMenu();
contextMenu = document.getElementById("contentAreaContextMenu");
topLevelMenuItem = contextMenu.getElementsByAttribute("label", "child")[0];
confirmContextMenuIcon(topLevelMenuItem);
yield closeContextMenu();
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_onclick.js
@@ -0,0 +1,196 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Loaded both as a background script and a tab page.
+function testScript() {
+ let page = location.pathname.includes("tab.html") ? "tab" : "background";
+ let clickCounts = {
+ old: 0,
+ new: 0,
+ };
+ browser.contextMenus.onClicked.addListener(() => {
+ // Async to give other onclick handlers a chance to fire.
+ setTimeout(() => {
+ browser.test.sendMessage("onClicked-fired", page);
+ });
+ });
+ browser.test.onMessage.addListener((toPage, msg) => {
+ if (toPage !== page) {
+ return;
+ }
+ browser.test.log(`Received ${msg} for ${toPage}`);
+ if (msg == "get-click-counts") {
+ browser.test.sendMessage("click-counts", clickCounts);
+ } else if (msg == "clear-click-counts") {
+ clickCounts.old = clickCounts.new = 0;
+ browser.test.sendMessage("next");
+ } else if (msg == "create-with-onclick") {
+ browser.contextMenus.create({
+ id: "iden",
+ title: "tifier",
+ onclick() {
+ ++clickCounts.old;
+ browser.test.log(`onclick fired for original onclick property in ${page}`);
+ },
+ }, () => browser.test.sendMessage("next"));
+ } else if (msg == "create-without-onclick") {
+ browser.contextMenus.create({
+ id: "iden",
+ title: "tifier",
+ }, () => browser.test.sendMessage("next"));
+ } else if (msg == "update-without-onclick") {
+ browser.contextMenus.update("iden", {
+ enabled: true, // Already enabled, so this does nothing.
+ }, () => browser.test.sendMessage("next"));
+ } else if (msg == "update-with-onclick") {
+ browser.contextMenus.update("iden", {
+ onclick() {
+ ++clickCounts.new;
+ browser.test.log(`onclick fired for updated onclick property in ${page}`);
+ },
+ }, () => browser.test.sendMessage("next"));
+ } else if (msg == "remove") {
+ browser.contextMenus.remove("iden", () => browser.test.sendMessage("next"));
+ } else if (msg == "removeAll") {
+ browser.contextMenus.removeAll(() => browser.test.sendMessage("next"));
+ }
+ });
+
+ if (page == "background") {
+ browser.test.log("Opening tab.html");
+ browser.tabs.create({
+ url: "tab.html",
+ active: false, // To not interfere with the context menu tests.
+ });
+ } else {
+ // Sanity check - the pages must be in the same process.
+ let pages = browser.extension.getViews();
+ browser.test.assertTrue(pages.includes(window),
+ "Expected this tab to be an extension view");
+ pages = pages.filter(w => w !== window);
+ browser.test.assertEq(pages[0], browser.extension.getBackgroundPage(),
+ "Expected the other page to be a background page");
+ browser.test.sendMessage("tab.html ready");
+ }
+}
+
+add_task(function* () {
+ let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser,
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html");
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["contextMenus"],
+ },
+ background: testScript,
+ files: {
+ "tab.html": `<!DOCTYPE html><meta charset="utf-8"><script src="tab.js"></script>`,
+ "tab.js": testScript,
+ },
+ });
+ yield extension.startup();
+ yield extension.awaitMessage("tab.html ready");
+
+ function* clickContextMenu() {
+ // Using openContextMenu instead of openExtensionContextMenu because the
+ // test extension has only one context menu item.
+ let extensionMenuRoot = yield openContextMenu();
+ let items = extensionMenuRoot.getElementsByAttribute("label", "tifier");
+ is(items.length, 1, "Expected one context menu item");
+ yield closeExtensionContextMenu(items[0]);
+ // One of them is "tab", the other is "background".
+ info(`onClicked from: ${yield extension.awaitMessage("onClicked-fired")}`);
+ info(`onClicked from: ${yield extension.awaitMessage("onClicked-fired")}`);
+ }
+
+ function* getCounts(page) {
+ extension.sendMessage(page, "get-click-counts");
+ return yield extension.awaitMessage("click-counts");
+ }
+ function* resetCounts() {
+ extension.sendMessage("tab", "clear-click-counts");
+ extension.sendMessage("background", "clear-click-counts");
+ yield extension.awaitMessage("next");
+ yield extension.awaitMessage("next");
+ }
+
+ // During this test, at most one "onclick" attribute is expected at any time.
+ for (let pageOne of ["background", "tab"]) {
+ for (let pageTwo of ["background", "tab"]) {
+ info(`Testing with menu created by ${pageOne} and updated by ${pageTwo}`);
+ extension.sendMessage(pageOne, "create-with-onclick");
+ yield extension.awaitMessage("next");
+
+ // Test that update without onclick attribute does not clear the existing
+ // onclick handler.
+ extension.sendMessage(pageTwo, "update-without-onclick");
+ yield extension.awaitMessage("next");
+ yield clickContextMenu();
+ let clickCounts = yield getCounts(pageOne);
+ is(clickCounts.old, 1, `Original onclick should still be present in ${pageOne}`);
+ is(clickCounts.new, 0, `Not expecting any new handlers in ${pageOne}`);
+ if (pageOne !== pageTwo) {
+ clickCounts = yield getCounts(pageTwo);
+ is(clickCounts.old, 0, `Not expecting any handlers in ${pageTwo}`);
+ is(clickCounts.new, 0, `Not expecting any new handlers in ${pageTwo}`);
+ }
+ yield resetCounts();
+
+ // Test that update with onclick handler in a different page clears the
+ // existing handler and activates the new onclick handler.
+ extension.sendMessage(pageTwo, "update-with-onclick");
+ yield extension.awaitMessage("next");
+ yield clickContextMenu();
+ clickCounts = yield getCounts(pageOne);
+ is(clickCounts.old, 0, `Original onclick should be gone from ${pageOne}`);
+ if (pageOne !== pageTwo) {
+ is(clickCounts.new, 0, `Still not expecting new handlers in ${pageOne}`);
+ }
+ clickCounts = yield getCounts(pageTwo);
+ if (pageOne !== pageTwo) {
+ is(clickCounts.old, 0, `Not expecting an old onclick in ${pageTwo}`);
+ }
+ is(clickCounts.new, 1, `New onclick should be triggered in ${pageTwo}`);
+ yield resetCounts();
+
+ // Test that updating the handler (different again from the last `update`
+ // call, but the same as the `create` call) clears the existing handler
+ // and activates the new onclick handler.
+ extension.sendMessage(pageOne, "update-with-onclick");
+ yield extension.awaitMessage("next");
+ yield clickContextMenu();
+ clickCounts = yield getCounts(pageOne);
+ is(clickCounts.new, 1, `onclick should be triggered in ${pageOne}`);
+ if (pageOne !== pageTwo) {
+ clickCounts = yield getCounts(pageTwo);
+ is(clickCounts.new, 0, `onclick should be gone from ${pageTwo}`);
+ }
+ yield resetCounts();
+
+ // Test that removing the context menu and recreating it with the same ID
+ // (in a different context) does not leave behind any onclick handlers.
+ extension.sendMessage(pageTwo, "remove");
+ yield extension.awaitMessage("next");
+ extension.sendMessage(pageTwo, "create-without-onclick");
+ yield extension.awaitMessage("next");
+ yield clickContextMenu();
+ clickCounts = yield getCounts(pageOne);
+ is(clickCounts.new, 0, `Did not expect any click handlers in ${pageOne}`);
+ if (pageOne !== pageTwo) {
+ clickCounts = yield getCounts(pageTwo);
+ is(clickCounts.new, 0, `Did not expect any click handlers in ${pageTwo}`);
+ }
+ yield resetCounts();
+
+ // Remove context menu for the next iteration of the test. And just to get
+ // more coverage, let's use removeAll instead of remove.
+ extension.sendMessage(pageOne, "removeAll");
+ yield extension.awaitMessage("next");
+ }
+ }
+ yield extension.unload();
+ yield BrowserTestUtils.removeTab(tab1);
+});
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -735,16 +735,20 @@ GlobalManager = {
return context.extension.hasPermission(permission);
},
shouldInject(namespace, name, allowedContexts) {
// Do not generate content script APIs, unless explicitly allowed.
if (context.envType === "content_parent" && !allowedContexts.includes("content")) {
return false;
}
+ if (context.envType !== "addon_parent" &&
+ allowedContexts.includes("addon_parent_only")) {
+ return false;
+ }
return findPathInObject(apis, namespace, false) !== null;
},
getImplementation(namespace, name) {
let pathObj = findPathInObject(apis, namespace);
return new LocalAPIImplementation(pathObj, name, context);
},
};
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1851,31 +1851,62 @@ class ChildAPIManager {
callId,
path,
args,
});
return this.context.wrapPromise(deferred.promise, callback);
}
+ /**
+ * Create a proxy for an event in the parent process. The returned event
+ * object shares its internal state with other instances. For instance, if
+ * `removeListener` is used on a listener that was added on another object
+ * through `addListener`, then the event is unregistered.
+ *
+ * @param {string} path The full name of the event, e.g. "tabs.onCreated".
+ * @returns {object} An object with the addListener, removeListener and
+ * hasListener methods. See SchemaAPIInterface for documentation.
+ */
+ getParentEvent(path) {
+ let parsed = /^(.+)\.(on[A-Z][^.]+)$/.exec(path);
+ if (!parsed) {
+ throw new Error("getParentEvent: Invalid event name: " + path);
+ }
+ let [, namespace, name] = parsed;
+ let impl = new ProxyAPIImplementation(namespace, name, this);
+ return {
+ addListener: (listener, ...args) => impl.addListener(listener, args),
+ removeListener: (listener) => impl.removeListener(listener),
+ hasListener: (listener) => impl.hasListener(listener),
+ };
+ }
+
close() {
this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
}
get cloneScope() {
return this.context.cloneScope;
}
get principal() {
return this.context.principal;
}
shouldInject(namespace, name, allowedContexts) {
// Do not generate content script APIs, unless explicitly allowed.
- return this.context.envType !== "content_child" || allowedContexts.includes("content");
+ if (this.context.envType === "content_child" &&
+ !allowedContexts.includes("content")) {
+ return false;
+ }
+ if (allowedContexts.includes("addon_parent_only")) {
+ return false;
+ }
+ return true;
}
getImplementation(namespace, name) {
let pathObj = this.localApis;
if (pathObj) {
for (let part of namespace.split(".")) {
pathObj = pathObj[part];
if (!pathObj) {
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -1030,17 +1030,17 @@ class ObjectType extends Type {
result[prop] = r.value;
properties[prop] = r.value;
}
}
remainingProps.delete(prop);
} else if (!optional) {
error = context.error(`Property "${prop}" is required`,
`contain the required "${prop}" property`);
- } else {
+ } else if (optional !== "omit-key-if-missing") {
result[prop] = null;
}
if (error) {
if (onError == "warn") {
context.logError(error.error);
} else if (onError != "ignore") {
throw error;