Created
October 7, 2016 14:03
-
-
Save rpl/41495946e857701c82b1e1317e468970 to your computer and use it in GitHub Desktop.
WebExtension devtools API (in the new webext-oop style)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ | |
| /* vim: set sts=2 sw=2 et tw=80: */ | |
| "use strict"; | |
| /* global getTabIdForToolbox */ | |
| /** | |
| * This module provides helpers used by the other specialized `ext-devtools-*.js` modules | |
| * and the implementation of the `devtools_page`. | |
| */ | |
| XPCOMUtils.defineLazyModuleGetter(this, "Task", | |
| "resource://gre/modules/Task.jsm"); | |
| XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", | |
| "resource://devtools/client/framework/gDevTools.jsm"); | |
| XPCOMUtils.defineLazyModuleGetter(this, "Task", | |
| "resource://gre/modules/Task.jsm"); | |
| Cu.import("resource://gre/modules/ExtensionUtils.jsm"); | |
| const { | |
| promiseDocumentLoaded, | |
| promiseObserved, | |
| } = ExtensionUtils; | |
| // Map[extension -> DevtoolsPage] | |
| let devtoolsPageMap = new Map(); | |
| const DevtoolsContextsManager = { | |
| contextDataMap: new Map(), | |
| addContext(context, toolbox) { | |
| if (this.contextDataMap.has(context)) { | |
| throw new Error("Extension Context cannot be assigned to a developer toolbox twice"); | |
| } | |
| this.contextDataMap.set(context, { | |
| toolbox, target: toolbox.target, | |
| }); | |
| // Remove any reference to the toolbox when the context is closed. | |
| context.callOnClose({ | |
| close: () => this.cleanupContext(context), | |
| }); | |
| }, | |
| cleanupContext(context) { | |
| const contextData = this.contextDataMap.get(context); | |
| if (contextData.contextToolboxTarget) { | |
| contextData.contextToolboxTarget.close(); | |
| } | |
| this.contextDataMap.delete(context); | |
| }, | |
| getToolboxForContext(context) { | |
| if (!this.contextDataMap.has(context)) { | |
| return null; | |
| } | |
| return this.contextDataMap.get(context).toolbox; | |
| }, | |
| getLocalTabTargetForContext(context) { | |
| const {TabTarget} = require("devtools/client/framework/target"); | |
| const contextDataMap = this.contextDataMap; | |
| const contextData = contextDataMap.get(context) || {}; | |
| if (contextData.contextToolboxTarget) { | |
| return Promise.resolve(contextData.contextToolboxTarget); | |
| } | |
| return Task.spawn(function* asyncConnectLocalTabTartget() { | |
| contextData.contextToolboxTarget = new TabTarget(contextData.target.tab); | |
| contextDataMap.set(context, contextData); | |
| yield contextData.contextToolboxTarget.makeRemote(); | |
| return contextData.contextToolboxTarget; | |
| }); | |
| }, | |
| }; | |
| global.DevtoolsContextsManager = DevtoolsContextsManager; | |
| global.getTabIdForToolbox = (toolbox) => { | |
| let {target} = toolbox; | |
| let parentWindow = target.tab.linkedBrowser.ownerDocument.defaultView; | |
| let tab = parentWindow.gBrowser.getTabForBrowser(target.tab.linkedBrowser); | |
| return TabManager.getId(tab); | |
| }; | |
| /** | |
| * The DevtoolsPage class manages the `devtools_page` that an WebExtensions add-on can | |
| * declare in its manifest. | |
| * | |
| * The devtools_page contexts are invisible WebExtensions contexts, similar to the | |
| * background page, associated to a single developer toolbox (e.g. If an add-on | |
| * registers a devtools_page and the user opens 3 developer toolbox in 3 webpages, | |
| * 3 devtools_page contexts will be created for that add-on). | |
| * | |
| * @param {Extension} extension The extension that owns the devtools_page. | |
| * @param {string} url The relative path to the devtools page html page. | |
| */ | |
| class DevtoolsPage { | |
| constructor(extension, url) { | |
| this.url = url; | |
| this.extension = extension; | |
| // Map[Devtools target -> {webNav, windowlessBrowser}] | |
| this.devtoolsPageChromeForTarget = new Map(); | |
| } | |
| createWindowlessExtensionPage(contextType) { | |
| function* asyncCreateWindowlessExtensionPage() { | |
| // The invisible page is currently wrapped in a XUL window to fix an issue | |
| // with using the canvas API from a background page (See Bug 1274775). | |
| const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + | |
| encodeURI(`<?xml version="1.0"?><window id="documentElement"/>`); | |
| const windowlessBrowser = Services.appShell.createWindowlessBrowser(true); | |
| const system = Services.scriptSecurityManager.getSystemPrincipal(); | |
| // The windowless browser is a thin wrapper around a docShell that keeps | |
| // its related resources alive. It implements nsIWebNavigation and | |
| // forwards its methods to the underlying docShell, but cannot act as a | |
| // docShell itself. Calling `getInterface(nsIDocShell)` gives us the | |
| // underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us | |
| // access to the webNav methods that are already available on the | |
| // windowless browser, but contrary to appearances, they are not the same | |
| // object. | |
| const chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor) | |
| .getInterface(Ci.nsIDocShell) | |
| .QueryInterface(Ci.nsIWebNavigation); | |
| chromeShell.useGlobalHistory = false; | |
| chromeShell.createAboutBlankContentViewer(system); | |
| chromeShell.loadURI(XUL_URL, 0, null, null, null); | |
| yield promiseObserved("chrome-document-global-created", | |
| win => win.document == chromeShell.document); | |
| const chromeDoc = yield promiseDocumentLoaded(chromeShell.document); | |
| const browser = chromeDoc.createElement("browser"); | |
| browser.setAttribute("type", "content"); | |
| browser.setAttribute("disableglobalhistory", "true"); | |
| browser.setAttribute("webextension-view-type", contextType); | |
| chromeDoc.documentElement.appendChild(browser); | |
| return {windowlessBrowser, browser}; | |
| } | |
| return Task.spawn(asyncCreateWindowlessExtensionPage.bind(this)); | |
| } | |
| buildForToolbox(toolbox) { | |
| function* asyncBuildForToolbox() { | |
| if (this.devtoolsPageChromeForTarget.has(toolbox.target)) { | |
| return; | |
| } | |
| let url = this.extension.baseURI.resolve(this.url); | |
| if (!this.extension.isExtensionURL(url)) { | |
| this.extension.manifestError("DevTools page must be a file within the extension"); | |
| url = this.extension.baseURI.resolve("_blank.html"); | |
| } | |
| const { | |
| windowlessBrowser, | |
| browser, | |
| } = yield this.createWindowlessExtensionPage("devtools_page"); | |
| let topLevelContextInitialized = false; | |
| const waitForContext = new Promise(resolve => { | |
| let listener = (event, context) => { | |
| if (context.viewType == "devtools_page" && context.xulBrowser == browser) { | |
| // Keep track of the toolbox and target associated to the context, which is | |
| // needed by the API methods implementation. | |
| DevtoolsContextsManager.addContext(context, toolbox); | |
| if (!topLevelContextInitialized) { | |
| topLevelContextInitialized = true; | |
| // Remove listeners when the top level context has been destroyed. | |
| context.callOnClose({ | |
| close: () => { | |
| this.extension.off("extension-proxy-context-load", listener); | |
| }, | |
| }); | |
| } | |
| // Resolve the promise when the root devtools_page context has been created. | |
| resolve(context); | |
| } | |
| }; | |
| this.extension.on("extension-proxy-context-load", listener); | |
| }); | |
| extensions.emit("extension-browser-inserted", browser); | |
| browser.messageManager.sendAsyncMessage("Extension:InitExtensionView", { | |
| viewType: "devtools_page", | |
| url, | |
| devtoolsToolboxInfo: { | |
| inspectedWindowTabId: getTabIdForToolbox(toolbox), | |
| }, | |
| }); | |
| yield waitForContext; | |
| const webNav = browser.docShell.QueryInterface(Ci.nsIWebNavigation); | |
| // Keep track of the webNav and windowlessBrowser created, so that we can | |
| // close them once the context is closed (e.g. the toolbox has been closed) | |
| // or the extension has been uninstalled. | |
| this.devtoolsPageChromeForTarget.set(toolbox.target, { | |
| webNav, | |
| windowlessBrowser, | |
| }); | |
| } | |
| return Task.spawn(asyncBuildForToolbox.bind(this)); | |
| } | |
| shutdownForTarget(target) { | |
| if (this.devtoolsPageChromeForTarget.has(target)) { | |
| const { | |
| webNav, | |
| windowlessBrowser, | |
| } = this.devtoolsPageChromeForTarget.get(target); | |
| // Navigate away from the devtools page to invalidate any | |
| // setTimeouts or other callbacks. | |
| if (webNav) { | |
| webNav.loadURI("about:blank", 0, null, null, null); | |
| } | |
| if (windowlessBrowser) { | |
| windowlessBrowser.loadURI("about:blank", 0, null, null, null); | |
| windowlessBrowser.close(); | |
| } | |
| this.devtoolsPageChromeForTarget.delete(target); | |
| } | |
| } | |
| shutdown() { | |
| for (let target of this.devtoolsPageChromeForTarget.keys()) { | |
| this.shutdownForTarget(target); | |
| } | |
| this.devtoolsPageChromeForTarget.clear(); | |
| } | |
| } | |
| /* eslint-disable mozilla/balanced-listeners */ | |
| // Create a devtools page context for a new opened toolbox. | |
| gDevTools.on("toolbox-created", (evt, toolbox) => { | |
| if (!toolbox.target.isLocalTab) { | |
| // Only local tabs are currently supported (See Bug 1304378 for additional details | |
| // related to remote targets support). | |
| return; | |
| } | |
| for (let devtoolsPage of devtoolsPageMap.values()) { | |
| devtoolsPage.buildForToolbox(toolbox); | |
| } | |
| }); | |
| // Destroy a devtools page context for a destroyed toolbox. | |
| gDevTools.on("toolbox-destroy", (evt, target) => { | |
| if (!target.isLocalTab) { | |
| // Only local tabs are currently supported (See Bug 1304378 for additional details | |
| // related to remote targets support). | |
| return; | |
| } | |
| for (let devtoolsPage of devtoolsPageMap.values()) { | |
| devtoolsPage.shutdownForTarget(target); | |
| } | |
| }); | |
| // Define a new DevTools API defined in the manifest. | |
| extensions.on("manifest_devtools_page", (type, directive, extension, manifest) => { | |
| let devtoolsPage = new DevtoolsPage(extension, manifest[directive]); | |
| devtoolsPageMap.set(extension, devtoolsPage); | |
| }); | |
| // Destroy the registered devtools_page on extension shutdown. | |
| extensions.on("shutdown", (type, extension) => { | |
| if (devtoolsPageMap.has(extension)) { | |
| devtoolsPageMap.get(extension).shutdown(); | |
| devtoolsPageMap.delete(extension); | |
| } | |
| }); | |
| /* eslint-enable mozilla/balanced-listeners */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment