summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/child
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/child')
-rw-r--r--browser/components/extensions/child/.eslintrc.js9
-rw-r--r--browser/components/extensions/child/ext-browser-content-only.js13
-rw-r--r--browser/components/extensions/child/ext-browser.js49
-rw-r--r--browser/components/extensions/child/ext-devtools-inspectedWindow.js29
-rw-r--r--browser/components/extensions/child/ext-devtools-network.js70
-rw-r--r--browser/components/extensions/child/ext-devtools-panels.js324
-rw-r--r--browser/components/extensions/child/ext-devtools.js15
-rw-r--r--browser/components/extensions/child/ext-menus-child.js38
-rw-r--r--browser/components/extensions/child/ext-menus.js305
-rw-r--r--browser/components/extensions/child/ext-omnibox.js38
-rw-r--r--browser/components/extensions/child/ext-tabs.js22
11 files changed, 912 insertions, 0 deletions
diff --git a/browser/components/extensions/child/.eslintrc.js b/browser/components/extensions/child/.eslintrc.js
new file mode 100644
index 0000000000..3073b22caf
--- /dev/null
+++ b/browser/components/extensions/child/.eslintrc.js
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ extends: "../../../../toolkit/components/extensions/child/.eslintrc.js",
+};
diff --git a/browser/components/extensions/child/ext-browser-content-only.js b/browser/components/extensions/child/ext-browser-content-only.js
new file mode 100644
index 0000000000..af5b8accf9
--- /dev/null
+++ b/browser/components/extensions/child/ext-browser-content-only.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+extensions.registerModules({
+ menusChild: {
+ url: "chrome://browser/content/child/ext-menus-child.js",
+ scopes: ["content_child"],
+ paths: [["menus"]],
+ },
+});
diff --git a/browser/components/extensions/child/ext-browser.js b/browser/components/extensions/child/ext-browser.js
new file mode 100644
index 0000000000..790b2d4bd0
--- /dev/null
+++ b/browser/components/extensions/child/ext-browser.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+extensions.registerModules({
+ devtools: {
+ url: "chrome://browser/content/child/ext-devtools.js",
+ scopes: ["devtools_child"],
+ paths: [["devtools"]],
+ },
+ devtools_inspectedWindow: {
+ url: "chrome://browser/content/child/ext-devtools-inspectedWindow.js",
+ scopes: ["devtools_child"],
+ paths: [["devtools", "inspectedWindow"]],
+ },
+ devtools_panels: {
+ url: "chrome://browser/content/child/ext-devtools-panels.js",
+ scopes: ["devtools_child"],
+ paths: [["devtools", "panels"]],
+ },
+ devtools_network: {
+ url: "chrome://browser/content/child/ext-devtools-network.js",
+ scopes: ["devtools_child"],
+ paths: [["devtools", "network"]],
+ },
+ // Because of permissions, the module name must differ from both namespaces.
+ menusInternal: {
+ url: "chrome://browser/content/child/ext-menus.js",
+ scopes: ["addon_child"],
+ paths: [["contextMenus"], ["menus"]],
+ },
+ menusChild: {
+ url: "chrome://browser/content/child/ext-menus-child.js",
+ scopes: ["addon_child", "devtools_child"],
+ paths: [["menus"]],
+ },
+ omnibox: {
+ url: "chrome://browser/content/child/ext-omnibox.js",
+ scopes: ["addon_child"],
+ paths: [["omnibox"]],
+ },
+ tabs: {
+ url: "chrome://browser/content/child/ext-tabs.js",
+ scopes: ["addon_child"],
+ paths: [["tabs"]],
+ },
+});
diff --git a/browser/components/extensions/child/ext-devtools-inspectedWindow.js b/browser/components/extensions/child/ext-devtools-inspectedWindow.js
new file mode 100644
index 0000000000..fdd8b97c17
--- /dev/null
+++ b/browser/components/extensions/child/ext-devtools-inspectedWindow.js
@@ -0,0 +1,29 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.devtools_inspectedWindow = class extends ExtensionAPI {
+ getAPI(context) {
+ // `devtoolsToolboxInfo` is received from the child process when the root devtools view
+ // has been created, and every sub-frame of that top level devtools frame will
+ // receive the same information when the context has been created from the
+ // `ExtensionChild.createExtensionContext` method.
+ let tabId =
+ context.devtoolsToolboxInfo &&
+ context.devtoolsToolboxInfo.inspectedWindowTabId;
+
+ return {
+ devtools: {
+ inspectedWindow: {
+ get tabId() {
+ return tabId;
+ },
+ },
+ },
+ };
+ }
+};
diff --git a/browser/components/extensions/child/ext-devtools-network.js b/browser/components/extensions/child/ext-devtools-network.js
new file mode 100644
index 0000000000..e36043f491
--- /dev/null
+++ b/browser/components/extensions/child/ext-devtools-network.js
@@ -0,0 +1,70 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Responsible for fetching HTTP response content from the backend.
+ *
+ * @param {DevtoolsExtensionContext}
+ * A devtools extension context running in a child process.
+ * @param {object} options
+ */
+class ChildNetworkResponseLoader {
+ constructor(context, requestId) {
+ this.context = context;
+ this.requestId = requestId;
+ }
+
+ api() {
+ const { context, requestId } = this;
+ return {
+ getContent(callback) {
+ return context.childManager.callParentAsyncFunction(
+ "devtools.network.Request.getContent",
+ [requestId],
+ callback
+ );
+ },
+ };
+ }
+}
+
+this.devtools_network = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ devtools: {
+ network: {
+ onRequestFinished: new EventManager({
+ context,
+ name: "devtools.network.onRequestFinished",
+ register: fire => {
+ let onFinished = data => {
+ const loader = new ChildNetworkResponseLoader(
+ context,
+ data.requestId
+ );
+ const harEntry = { ...data.harEntry, ...loader.api() };
+ const result = Cu.cloneInto(harEntry, context.cloneScope, {
+ cloneFunctions: true,
+ });
+ fire.asyncWithoutClone(result);
+ };
+
+ let parent = context.childManager.getParentEvent(
+ "devtools.network.onRequestFinished"
+ );
+ parent.addListener(onFinished);
+ return () => {
+ parent.removeListener(onFinished);
+ };
+ },
+ }).api(),
+ },
+ },
+ };
+ }
+};
diff --git a/browser/components/extensions/child/ext-devtools-panels.js b/browser/components/extensions/child/ext-devtools-panels.js
new file mode 100644
index 0000000000..43d650995d
--- /dev/null
+++ b/browser/components/extensions/child/ext-devtools-panels.js
@@ -0,0 +1,324 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionChildDevToolsUtils",
+ "resource://gre/modules/ExtensionChildDevToolsUtils.jsm"
+);
+
+var { promiseDocumentLoaded } = ExtensionUtils;
+
+/**
+ * Represents an addon devtools panel in the child process.
+ *
+ * @param {DevtoolsExtensionContext}
+ * A devtools extension context running in a child process.
+ * @param {object} panelOptions
+ * @param {string} panelOptions.id
+ * The id of the addon devtools panel registered in the main process.
+ */
+class ChildDevToolsPanel extends ExtensionCommon.EventEmitter {
+ constructor(context, { id }) {
+ super();
+
+ this.context = context;
+ this.context.callOnClose(this);
+
+ this.id = id;
+ this._panelContext = null;
+
+ this.conduit = context.openConduit(this, {
+ recv: ["PanelHidden", "PanelShown"],
+ });
+ }
+
+ get panelContext() {
+ if (this._panelContext) {
+ return this._panelContext;
+ }
+
+ for (let view of this.context.extension.devtoolsViews) {
+ if (
+ view.viewType === "devtools_panel" &&
+ view.devtoolsToolboxInfo.toolboxPanelId === this.id
+ ) {
+ this._panelContext = view;
+
+ // Reset the cached _panelContext property when the view is closed.
+ view.callOnClose({
+ close: () => {
+ this._panelContext = null;
+ },
+ });
+ return view;
+ }
+ }
+
+ return null;
+ }
+
+ recvPanelShown() {
+ // Ignore received call before the panel context exist.
+ if (!this.panelContext || !this.panelContext.contentWindow) {
+ return;
+ }
+ const { document } = this.panelContext.contentWindow;
+
+ // Ensure that the onShown event is fired when the panel document has
+ // been fully loaded.
+ promiseDocumentLoaded(document).then(() => {
+ this.emit("shown", this.panelContext.contentWindow);
+ });
+ }
+
+ recvPanelHidden() {
+ this.emit("hidden");
+ }
+
+ api() {
+ return {
+ onShown: new EventManager({
+ context: this.context,
+ name: "devtoolsPanel.onShown",
+ register: fire => {
+ const listener = (eventName, panelContentWindow) => {
+ fire.asyncWithoutClone(panelContentWindow);
+ };
+ this.on("shown", listener);
+ return () => {
+ this.off("shown", listener);
+ };
+ },
+ }).api(),
+
+ onHidden: new EventManager({
+ context: this.context,
+ name: "devtoolsPanel.onHidden",
+ register: fire => {
+ const listener = () => {
+ fire.async();
+ };
+ this.on("hidden", listener);
+ return () => {
+ this.off("hidden", listener);
+ };
+ },
+ }).api(),
+
+ // TODO(rpl): onSearch event and createStatusBarButton method
+ };
+ }
+
+ close() {
+ this._panelContext = null;
+ this.context = null;
+ }
+}
+
+/**
+ * Represents an addon devtools inspector sidebar in the child process.
+ *
+ * @param {DevtoolsExtensionContext}
+ * A devtools extension context running in a child process.
+ * @param {object} sidebarOptions
+ * @param {string} sidebarOptions.id
+ * The id of the addon devtools sidebar registered in the main process.
+ */
+class ChildDevToolsInspectorSidebar extends ExtensionCommon.EventEmitter {
+ constructor(context, { id }) {
+ super();
+
+ this.context = context;
+ this.context.callOnClose(this);
+
+ this.id = id;
+
+ this.conduit = context.openConduit(this, {
+ recv: ["InspectorSidebarHidden", "InspectorSidebarShown"],
+ });
+ }
+
+ close() {
+ this.context = null;
+ }
+
+ recvInspectorSidebarShown() {
+ // TODO: wait and emit sidebar contentWindow once sidebar.setPage is supported.
+ this.emit("shown");
+ }
+
+ recvInspectorSidebarHidden() {
+ this.emit("hidden");
+ }
+
+ api() {
+ const { context, id } = this;
+
+ let extensionURL = new URL("/", context.uri.spec);
+
+ // This is currently needed by sidebar.setPage because API objects are not automatically wrapped
+ // by the API Schema validations and so the ExtensionURL type used in the JSON schema
+ // doesn't have any effect on the parameter received by the setPage API method.
+ function resolveExtensionURL(url) {
+ let sidebarPageURL = new URL(url, context.uri.spec);
+
+ if (
+ extensionURL.protocol !== sidebarPageURL.protocol ||
+ extensionURL.host !== sidebarPageURL.host
+ ) {
+ throw new context.cloneScope.Error(
+ `Invalid sidebar URL: ${sidebarPageURL.href} is not a valid extension URL`
+ );
+ }
+
+ return sidebarPageURL.href;
+ }
+
+ return {
+ onShown: new EventManager({
+ context,
+ name: "devtoolsInspectorSidebar.onShown",
+ register: fire => {
+ const listener = (eventName, panelContentWindow) => {
+ fire.asyncWithoutClone(panelContentWindow);
+ };
+ this.on("shown", listener);
+ return () => {
+ this.off("shown", listener);
+ };
+ },
+ }).api(),
+
+ onHidden: new EventManager({
+ context,
+ name: "devtoolsInspectorSidebar.onHidden",
+ register: fire => {
+ const listener = () => {
+ fire.async();
+ };
+ this.on("hidden", listener);
+ return () => {
+ this.off("hidden", listener);
+ };
+ },
+ }).api(),
+
+ setPage(extensionPageURL) {
+ let resolvedSidebarURL = resolveExtensionURL(extensionPageURL);
+
+ return context.childManager.callParentAsyncFunction(
+ "devtools.panels.elements.Sidebar.setPage",
+ [id, resolvedSidebarURL]
+ );
+ },
+
+ setObject(jsonObject, rootTitle) {
+ return context.cloneScope.Promise.resolve().then(() => {
+ return context.childManager.callParentAsyncFunction(
+ "devtools.panels.elements.Sidebar.setObject",
+ [id, jsonObject, rootTitle]
+ );
+ });
+ },
+
+ setExpression(evalExpression, rootTitle) {
+ return context.cloneScope.Promise.resolve().then(() => {
+ return context.childManager.callParentAsyncFunction(
+ "devtools.panels.elements.Sidebar.setExpression",
+ [id, evalExpression, rootTitle]
+ );
+ });
+ },
+ };
+ }
+}
+
+this.devtools_panels = class extends ExtensionAPI {
+ getAPI(context) {
+ const themeChangeObserver = ExtensionChildDevToolsUtils.getThemeChangeObserver();
+
+ return {
+ devtools: {
+ panels: {
+ elements: {
+ createSidebarPane(title) {
+ // NOTE: this is needed to be able to return to the caller (the extension)
+ // a promise object that it had the privileges to use (e.g. by marking this
+ // method async we will return a promise object which can only be used by
+ // chrome privileged code).
+ return context.cloneScope.Promise.resolve().then(async () => {
+ const sidebarId = await context.childManager.callParentAsyncFunction(
+ "devtools.panels.elements.createSidebarPane",
+ [title]
+ );
+
+ const sidebar = new ChildDevToolsInspectorSidebar(context, {
+ id: sidebarId,
+ });
+
+ const sidebarAPI = Cu.cloneInto(
+ sidebar.api(),
+ context.cloneScope,
+ { cloneFunctions: true }
+ );
+
+ return sidebarAPI;
+ });
+ },
+ },
+ create(title, icon, url) {
+ // NOTE: this is needed to be able to return to the caller (the extension)
+ // a promise object that it had the privileges to use (e.g. by marking this
+ // method async we will return a promise object which can only be used by
+ // chrome privileged code).
+ return context.cloneScope.Promise.resolve().then(async () => {
+ const panelId = await context.childManager.callParentAsyncFunction(
+ "devtools.panels.create",
+ [title, icon, url]
+ );
+
+ const devtoolsPanel = new ChildDevToolsPanel(context, {
+ id: panelId,
+ });
+
+ const devtoolsPanelAPI = Cu.cloneInto(
+ devtoolsPanel.api(),
+ context.cloneScope,
+ { cloneFunctions: true }
+ );
+ return devtoolsPanelAPI;
+ });
+ },
+ get themeName() {
+ return themeChangeObserver.themeName;
+ },
+ onThemeChanged: new EventManager({
+ context,
+ name: "devtools.panels.onThemeChanged",
+ register: fire => {
+ const listener = (eventName, themeName) => {
+ fire.async(themeName);
+ };
+ themeChangeObserver.on("themeChanged", listener);
+ return () => {
+ themeChangeObserver.off("themeChanged", listener);
+ };
+ },
+ }).api(),
+ },
+ },
+ };
+ }
+};
diff --git a/browser/components/extensions/child/ext-devtools.js b/browser/components/extensions/child/ext-devtools.js
new file mode 100644
index 0000000000..219df7cb07
--- /dev/null
+++ b/browser/components/extensions/child/ext-devtools.js
@@ -0,0 +1,15 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.devtools = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ devtools: {},
+ };
+ }
+};
diff --git a/browser/components/extensions/child/ext-menus-child.js b/browser/components/extensions/child/ext-menus-child.js
new file mode 100644
index 0000000000..2819ec219e
--- /dev/null
+++ b/browser/components/extensions/child/ext-menus-child.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ContextMenuChild: "resource:///actors/ContextMenuChild.sys.mjs",
+});
+
+this.menusChild = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ menus: {
+ getTargetElement(targetElementId) {
+ let element;
+ let lastMenuTarget = ContextMenuChild.getLastTarget(
+ context.contentWindow.docShell.browsingContext
+ );
+ if (
+ lastMenuTarget &&
+ Math.floor(lastMenuTarget.timeStamp) === targetElementId
+ ) {
+ element = lastMenuTarget.targetRef.get();
+ }
+ if (
+ element &&
+ element.getRootNode({ composed: true }) ===
+ context.contentWindow.document
+ ) {
+ return element;
+ }
+ return null;
+ },
+ },
+ };
+ }
+};
diff --git a/browser/components/extensions/child/ext-menus.js b/browser/components/extensions/child/ext-menus.js
new file mode 100644
index 0000000000..6c3b7ae492
--- /dev/null
+++ b/browser/components/extensions/child/ext-menus.js
@@ -0,0 +1,305 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { withHandlingUserInput } = ExtensionCommon;
+
+var { ExtensionError } = ExtensionUtils;
+
+// 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.
+ withHandlingUserInput(this.context.contentWindow, () =>
+ 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("menusInternal.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("menusInternal.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);
+ }
+ }
+}
+
+this.menusInternal = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+ let onClickedProp = new ContextMenusClickPropHandler(context);
+ let pendingMenuEvent;
+
+ let api = {
+ menus: {
+ create(createProperties, callback) {
+ let caller = context.getCaller();
+
+ if (extension.persistentBackground && createProperties.id === null) {
+ createProperties.id = ++gNextMenuItemID;
+ }
+ let { onclick } = createProperties;
+ if (onclick && !context.extension.persistentBackground) {
+ throw new ExtensionError(
+ `Property "onclick" cannot be used in menus.create, replace with an "onClicked" event listener.`
+ );
+ }
+ delete createProperties.onclick;
+ context.childManager
+ .callParentAsyncFunction("menusInternal.create", [createProperties])
+ .then(() => {
+ if (onclick) {
+ onClickedProp.setListener(createProperties.id, onclick);
+ }
+ if (callback) {
+ context.runSafeWithoutClone(callback);
+ }
+ })
+ .catch(error => {
+ context.withLastError(error, caller, () => {
+ if (callback) {
+ context.runSafeWithoutClone(callback);
+ }
+ });
+ });
+ return createProperties.id;
+ },
+
+ update(id, updateProperties) {
+ let { onclick } = updateProperties;
+ if (onclick && !context.extension.persistentBackground) {
+ throw new ExtensionError(
+ `Property "onclick" cannot be used in menus.update, replace with an "onClicked" event listener.`
+ );
+ }
+ delete updateProperties.onclick;
+ return context.childManager
+ .callParentAsyncFunction("menusInternal.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(
+ "menusInternal.remove",
+ [id]
+ );
+ },
+
+ removeAll() {
+ onClickedProp.deleteAllListenersFromExtension();
+
+ return context.childManager.callParentAsyncFunction(
+ "menusInternal.removeAll",
+ []
+ );
+ },
+
+ overrideContext(contextOptions) {
+ let checkValidArg = (contextType, propKey) => {
+ if (contextOptions.context !== contextType) {
+ if (contextOptions[propKey]) {
+ throw new ExtensionError(
+ `Property "${propKey}" can only be used with context "${contextType}"`
+ );
+ }
+ return false;
+ }
+ if (contextOptions.showDefaults) {
+ throw new ExtensionError(
+ `Property "showDefaults" cannot be used with context "${contextType}"`
+ );
+ }
+ if (!contextOptions[propKey]) {
+ throw new ExtensionError(
+ `Property "${propKey}" is required for context "${contextType}"`
+ );
+ }
+ return true;
+ };
+ if (checkValidArg("tab", "tabId")) {
+ if (!context.extension.hasPermission("tabs")) {
+ throw new ExtensionError(
+ `The "tab" context requires the "tabs" permission.`
+ );
+ }
+ }
+ if (checkValidArg("bookmark", "bookmarkId")) {
+ if (!context.extension.hasPermission("bookmarks")) {
+ throw new ExtensionError(
+ `The "bookmark" context requires the "bookmarks" permission.`
+ );
+ }
+ }
+
+ let webExtContextData = {
+ extensionId: context.extension.id,
+ showDefaults: contextOptions.showDefaults,
+ overrideContext: contextOptions.context,
+ bookmarkId: contextOptions.bookmarkId,
+ tabId: contextOptions.tabId,
+ };
+
+ if (pendingMenuEvent) {
+ // overrideContext is called more than once during the same event.
+ pendingMenuEvent.webExtContextData = webExtContextData;
+ return;
+ }
+ pendingMenuEvent = {
+ webExtContextData,
+ observe(subject, topic, data) {
+ pendingMenuEvent = null;
+ Services.obs.removeObserver(this, "on-prepare-contextmenu");
+ subject = subject.wrappedJSObject;
+ if (context.principal.subsumes(subject.principal)) {
+ subject.setWebExtContextData(this.webExtContextData);
+ }
+ },
+ run() {
+ // "on-prepare-contextmenu" is expected to be observed before the
+ // end of the "contextmenu" event dispatch. This task is queued
+ // in case that does not happen, e.g. when the menu is not shown.
+ // ... or if the method was not called during a contextmenu event.
+ if (pendingMenuEvent === this) {
+ pendingMenuEvent = null;
+ Services.obs.removeObserver(this, "on-prepare-contextmenu");
+ }
+ },
+ };
+ Services.obs.addObserver(pendingMenuEvent, "on-prepare-contextmenu");
+ Services.tm.dispatchToMainThread(pendingMenuEvent);
+ },
+
+ onClicked: new EventManager({
+ context,
+ name: "menus.onClicked",
+ register: fire => {
+ let listener = (info, tab) => {
+ withHandlingUserInput(context.contentWindow, () =>
+ fire.sync(info, tab)
+ );
+ };
+
+ let event = context.childManager.getParentEvent(
+ "menusInternal.onClicked"
+ );
+ event.addListener(listener);
+ return () => {
+ event.removeListener(listener);
+ };
+ },
+ }).api(),
+ },
+ };
+
+ const result = {};
+ if (context.extension.hasPermission("menus")) {
+ result.menus = api.menus;
+ }
+ if (context.extension.hasPermission("contextMenus")) {
+ result.contextMenus = api.menus;
+ }
+ return result;
+ }
+};
diff --git a/browser/components/extensions/child/ext-omnibox.js b/browser/components/extensions/child/ext-omnibox.js
new file mode 100644
index 0000000000..1121b11390
--- /dev/null
+++ b/browser/components/extensions/child/ext-omnibox.js
@@ -0,0 +1,38 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.omnibox = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ omnibox: {
+ onInputChanged: new EventManager({
+ context,
+ name: "omnibox.onInputChanged",
+ register: fire => {
+ let listener = (text, id) => {
+ fire.asyncWithoutClone(text, suggestions => {
+ context.childManager.callParentFunctionNoReturn(
+ "omnibox.addSuggestions",
+ [id, suggestions]
+ );
+ });
+ };
+ context.childManager
+ .getParentEvent("omnibox.onInputChanged")
+ .addListener(listener);
+ return () => {
+ context.childManager
+ .getParentEvent("omnibox.onInputChanged")
+ .removeListener(listener);
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/browser/components/extensions/child/ext-tabs.js b/browser/components/extensions/child/ext-tabs.js
new file mode 100644
index 0000000000..ae3ef1cb75
--- /dev/null
+++ b/browser/components/extensions/child/ext-tabs.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+this.tabs = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ tabs: {
+ connect(tabId, options) {
+ let { frameId = null, name = "" } = options || {};
+ return context.messenger.connect({ name, tabId, frameId });
+ },
+
+ sendMessage(tabId, message, options, callback) {
+ let arg = { tabId, frameId: options?.frameId, message, callback };
+ return context.messenger.sendRuntimeMessage(arg);
+ },
+ },
+ };
+ }
+};