diff options
Diffstat (limited to 'browser/components/extensions/child')
11 files changed, 914 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..e055976aec --- /dev/null +++ b/browser/components/extensions/child/ext-devtools-panels.js @@ -0,0 +1,326 @@ +/* -*- 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.defineESModuleGetters(this, { + ExtensionChildDevToolsUtils: + "resource://gre/modules/ExtensionChildDevToolsUtils.sys.mjs", +}); + +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); + }, + }, + }; + } +}; |