Bug 1291737 - Added a new helper to create windowless extension pages. r=kmag
authorLuca Greco <lgreco@mozilla.com>
Wed, 18 Jan 2017 15:14:53 +0100
changeset 375204 52c940b9aff64f229fb062182907cb265156f76a
parent 375203 e393454e52860f106d5a28d1877382393d3213d1
child 375205 dc42f50d06e8de0945f1bc8e8efc03d597ddc715
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1291737
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1291737 - Added a new helper to create windowless extension pages. r=kmag MozReview-Commit-ID: CqpWgFGmJAt
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/ext-backgroundPage.js
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -11,52 +11,62 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 /* exported ExtensionParent */
 
 this.EXPORTED_SYMBOLS = ["ExtensionParent"];
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
                                   "resource://gre/modules/NativeMessaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   BaseContext,
   SchemaAPIManager,
 } = ExtensionCommon;
 
 var {
   MessageManagerProxy,
   SpreadArgs,
   defineLazyGetter,
   findPathInObject,
+  promiseDocumentLoaded,
+  promiseEvent,
+  promiseObserved,
 } = ExtensionUtils;
 
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
 
+const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI(
+  `<?xml version="1.0"?>
+  <window id="documentElement"/>`);
+
 let schemaURLs = new Set();
 
 if (!AppConstants.RELEASE_OR_BETA) {
   schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
 }
 
 let GlobalManager;
 let ParentAPIManager;
@@ -572,14 +582,216 @@ ParentAPIManager = {
       throw new Error("WebExtension context not found!");
     }
     return context;
   },
 };
 
 ParentAPIManager.init();
 
+/**
+ * This is a base class used by the ext-backgroundPage and ext-devtools API implementations
+ * to inherits the shared boilerplate code needed to create a parent document for the hidden
+ * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
+ * DevToolsPage classes.
+ *
+ * @param {Extension} extension
+ *   the Extension which owns the hidden extension page created (used to decide
+ *   if the hidden extension page parent doc is going to be a windowlessBrowser or
+ *   a visible XUL window)
+ * @param {string} viewType
+ *  the viewType of the WebExtension page that is going to be loaded
+ *  in the created browser element (e.g. "background" or "devtools_page").
+ *
+ */
+class HiddenExtensionPage {
+  constructor(extension, viewType) {
+    if (!extension || !viewType) {
+      throw new Error("extension and viewType parameters are mandatory");
+    }
+    this.extension = extension;
+    this.viewType = viewType;
+    this.parentWindow = null;
+    this.windowlessBrowser = null;
+    this.browser = null;
+  }
+
+  /**
+   * Destroy the created parent document.
+   */
+  shutdown() {
+    if (this.unloaded) {
+      throw new Error("Unable to shutdown an unloaded HiddenExtensionPage instance");
+    }
+
+    this.unloaded = true;
+
+    if (this.browser) {
+      this.browser.remove();
+      this.browser = null;
+    }
+
+    // Navigate away from the background page to invalidate any
+    // setTimeouts or other callbacks.
+    if (this.webNav) {
+      this.webNav.loadURI("about:blank", 0, null, null, null);
+      this.webNav = null;
+    }
+
+    if (this.parentWindow) {
+      this.parentWindow.close();
+      this.parentWindow = null;
+    }
+
+    if (this.windowlessBrowser) {
+      this.windowlessBrowser.loadURI("about:blank", 0, null, null, null);
+      this.windowlessBrowser.close();
+      this.windowlessBrowser = null;
+    }
+  }
+
+  /**
+   * Creates the browser XUL element that will contain the WebExtension Page.
+   *
+   * @returns {Promise<XULElement>}
+   *   a Promise which resolves to the newly created browser XUL element.
+   */
+  createBrowserElement() {
+    if (this.browser) {
+      throw new Error("createBrowserElement called twice");
+    }
+
+    let waitForParentDocument;
+    if (this.extension.remote) {
+      waitForParentDocument = this.createWindowedBrowser();
+    } else {
+      waitForParentDocument = this.createWindowlessBrowser();
+    }
+
+    return waitForParentDocument.then(chromeDoc => {
+      const browser = this.browser = chromeDoc.createElement("browser");
+      browser.setAttribute("type", "content");
+      browser.setAttribute("disableglobalhistory", "true");
+      browser.setAttribute("webextension-view-type", this.viewType);
+
+      let awaitFrameLoader = Promise.resolve();
+
+      if (this.extension.remote) {
+        browser.setAttribute("remote", "true");
+        browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
+        awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
+      }
+
+      chromeDoc.documentElement.appendChild(browser);
+      return awaitFrameLoader.then(() => browser);
+    });
+  }
+
+  /**
+   * Private helper that create a XULDocument in a windowless browser.
+   *
+   * An hidden extension page (e.g. a background page or devtools page) is usually
+   * loaded into a windowless browser, with no on-screen representation or graphical
+   * display abilities.
+   *
+   * This currently does not support remote browsers, and therefore cannot
+   * be used with out-of-process extensions.
+   *
+   * @returns {Promise<XULDocument>}
+   *   a promise which resolves to the newly created XULDocument.
+   */
+  createWindowlessBrowser() {
+    return Task.spawn(function* () {
+      // 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).
+      let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
+      this.windowlessBrowser = windowlessBrowser;
+
+      // 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.
+      let chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
+                                         .getInterface(Ci.nsIDocShell)
+                                         .QueryInterface(Ci.nsIWebNavigation);
+
+      yield this.initParentWindow(chromeShell);
+
+      return promiseDocumentLoaded(windowlessBrowser.document);
+    }.bind(this));
+  }
+
+  /**
+   * Private helper that create a XULDocument in a visible dialog window.
+   *
+   * Using this helper, the extension page is loaded into a visible dialog window.
+   * Only to be used for debugging, and in temporary, test-only use for
+   * out-of-process extensions.
+   *
+   * @returns {Promise<XULDocument>}
+   *   a promise which resolves to the newly created XULDocument.
+   */
+  createWindowedBrowser() {
+    return Task.spawn(function* () {
+      let window = Services.ww.openWindow(null, "about:blank", "_blank",
+                                          "chrome,alwaysLowered,dialog", null);
+
+      this.parentWindow = window;
+
+      let chromeShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIDocShell)
+                              .QueryInterface(Ci.nsIWebNavigation);
+
+
+      yield this.initParentWindow(chromeShell);
+
+      window.minimize();
+
+      return promiseDocumentLoaded(window.document);
+    }.bind(this));
+  }
+
+  /**
+   * Private helper that initialize the created parent document.
+   *
+   * @param {nsIDocShell} chromeShell
+   *   the docShell related to initialize.
+   *
+   * @returns {Promise<nsIXULDocument>}
+   *   the initialized parent chrome document.
+   */
+  initParentWindow(chromeShell) {
+    if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+      let attrs = chromeShell.getOriginAttributes();
+      attrs.privateBrowsingId = 1;
+      chromeShell.setOriginAttributes(attrs);
+    }
+
+    let system = Services.scriptSecurityManager.getSystemPrincipal();
+    chromeShell.createAboutBlankContentViewer(system);
+    chromeShell.useGlobalHistory = false;
+    chromeShell.loadURI(XUL_URL, 0, null, null, null);
+
+    return promiseObserved("chrome-document-global-created",
+                           win => win.document == chromeShell.document);
+  }
+}
+
+function promiseExtensionViewLoaded(browser) {
+  return new Promise(resolve => {
+    browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad() {
+      browser.messageManager.removeMessageListener("Extension:ExtensionViewLoaded", onLoad);
+      resolve();
+    });
+  });
+}
 
 const ExtensionParent = {
   GlobalManager,
+  HiddenExtensionPage,
   ParentAPIManager,
   apiManager,
+  promiseExtensionViewLoaded,
 };
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -2,234 +2,86 @@
 
 var {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
-                                  "resource:///modules/E10SUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
 const {
-  promiseDocumentLoaded,
-  promiseEvent,
-  promiseObserved,
-} = ExtensionUtils;
-
-const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI(
-  `<?xml version="1.0"?>
-  <window id="documentElement"/>`);
+  HiddenExtensionPage,
+  promiseExtensionViewLoaded,
+} = ExtensionParent;
 
 // WeakMap[Extension -> BackgroundPage]
 var backgroundPagesMap = new WeakMap();
 
 // Responsible for the background_page section of the manifest.
-class BackgroundPageBase {
-  constructor(options, extension) {
-    this.extension = extension;
+class BackgroundPage extends HiddenExtensionPage {
+  constructor(extension, options) {
+    super(extension, "background");
+
     this.page = options.page || null;
     this.isGenerated = !!options.scripts;
     this.webNav = null;
+
+    if (this.page) {
+      this.url = this.extension.baseURI.resolve(this.page);
+    } else if (this.isGenerated) {
+      this.url = this.extension.baseURI.resolve("_generated_background_page.html");
+    }
+
+    if (!this.extension.isExtensionURL(this.url)) {
+      this.extension.manifestError("Background page must be a file within the extension");
+      this.url = this.extension.baseURI.resolve("_blank.html");
+    }
   }
 
   build() {
     return Task.spawn(function* () {
-      let url;
-      if (this.page) {
-        url = this.extension.baseURI.resolve(this.page);
-      } else if (this.isGenerated) {
-        url = this.extension.baseURI.resolve("_generated_background_page.html");
-      }
+      yield this.createBrowserElement();
 
-      if (!this.extension.isExtensionURL(url)) {
-        this.extension.manifestError("Background page must be a file within the extension");
-        url = this.extension.baseURI.resolve("_blank.html");
-      }
+      extensions.emit("extension-browser-inserted", this.browser);
 
-      let chromeDoc = yield this.getParentDocument();
-
-      let browser = chromeDoc.createElement("browser");
-      browser.setAttribute("type", "content");
-      browser.setAttribute("disableglobalhistory", "true");
-      browser.setAttribute("webextension-view-type", "background");
+      this.browser.loadURI(this.url);
 
-      let awaitFrameLoader;
-      if (this.extension.remote) {
-        browser.setAttribute("remote", "true");
-        browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
-        awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
-      }
-
-      chromeDoc.documentElement.appendChild(browser);
-      yield awaitFrameLoader;
-
-      this.browser = browser;
-
-      extensions.emit("extension-browser-inserted", browser);
+      yield promiseExtensionViewLoaded(this.browser);
 
-      browser.loadURI(url);
-
-      yield new Promise(resolve => {
-        browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad() {
-          browser.messageManager.removeMessageListener("Extension:ExtensionViewLoaded", onLoad);
-          resolve();
-        });
-      });
-
-      if (browser.docShell) {
-        this.webNav = browser.docShell.QueryInterface(Ci.nsIWebNavigation);
+      if (this.browser.docShell) {
+        this.webNav = this.browser.docShell.QueryInterface(Ci.nsIWebNavigation);
         let window = this.webNav.document.defaultView;
 
-
         // Set the add-on's main debugger global, for use in the debugger
         // console.
         if (this.extension.addonData.instanceID) {
           AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
                       .then(addon => addon.setDebugGlobal(window));
         }
       }
 
       this.extension.emit("startup");
     }.bind(this));
   }
 
-  initParentWindow(chromeShell) {
-    if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
-      let attrs = chromeShell.getOriginAttributes();
-      attrs.privateBrowsingId = 1;
-      chromeShell.setOriginAttributes(attrs);
-    }
-
-    let system = Services.scriptSecurityManager.getSystemPrincipal();
-    chromeShell.createAboutBlankContentViewer(system);
-    chromeShell.useGlobalHistory = false;
-    chromeShell.loadURI(XUL_URL, 0, null, null, null);
-
-    return promiseObserved("chrome-document-global-created",
-                           win => win.document == chromeShell.document);
-  }
-
   shutdown() {
     if (this.extension.addonData.instanceID) {
       AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
                   .then(addon => addon.setDebugGlobal(null));
     }
 
-    if (this.browser) {
-      this.browser.remove();
-      this.browser = null;
-    }
-
-    // Navigate away from the background page to invalidate any
-    // setTimeouts or other callbacks.
-    if (this.webNav) {
-      this.webNav.loadURI("about:blank", 0, null, null, null);
-      this.webNav = null;
-    }
-  }
-}
-
-/**
- * A background page loaded into a windowless browser, with no on-screen
- * representation or graphical display abilities.
- *
- * This currently does not support remote browsers, and therefore cannot
- * be used with out-of-process extensions.
- */
-class WindowlessBackgroundPage extends BackgroundPageBase {
-  constructor(options, extension) {
-    super(options, extension);
-    this.windowlessBrowser = null;
-  }
-
-  getParentDocument() {
-    return Task.spawn(function* () {
-      let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
-      this.windowlessBrowser = windowlessBrowser;
-
-      // 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.
-      let chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
-                                         .getInterface(Ci.nsIDocShell)
-                                         .QueryInterface(Ci.nsIWebNavigation);
-
-      yield this.initParentWindow(chromeShell);
-
-      return promiseDocumentLoaded(windowlessBrowser.document);
-    }.bind(this));
-  }
-
-  shutdown() {
     super.shutdown();
-
-    this.windowlessBrowser.loadURI("about:blank", 0, null, null, null);
-    this.windowlessBrowser.close();
-    this.windowlessBrowser = null;
-  }
-}
-
-/**
- * A background page loaded into a visible dialog window. Only to be
- * used for debugging, and in temporary, test-only use for
- * out-of-process extensions.
- */
-class WindowedBackgroundPage extends BackgroundPageBase {
-  constructor(options, extension) {
-    super(options, extension);
-    this.parentWindow = null;
-  }
-
-  getParentDocument() {
-    return Task.spawn(function* () {
-      let window = Services.ww.openWindow(null, "about:blank", "_blank",
-                                          "chrome,alwaysLowered,dialog", null);
-
-      this.parentWindow = window;
-
-      let chromeShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                              .getInterface(Ci.nsIDocShell)
-                              .QueryInterface(Ci.nsIWebNavigation);
-
-      yield this.initParentWindow(chromeShell);
-
-      window.minimize();
-
-      return promiseDocumentLoaded(window.document);
-    }.bind(this));
-  }
-
-  shutdown() {
-    super.shutdown();
-
-    if (this.parentWindow) {
-      this.parentWindow.close();
-      this.parentWindow = null;
-    }
   }
 }
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_background", (type, directive, extension, manifest) => {
-  let bgPage;
-  if (extension.remote) {
-    bgPage = new WindowedBackgroundPage(manifest.background, extension);
-  } else {
-    bgPage = new WindowlessBackgroundPage(manifest.background, extension);
-  }
+  let bgPage = new BackgroundPage(extension, manifest.background);
 
   backgroundPagesMap.set(extension, bgPage);
   return bgPage.build();
 });
 
 extensions.on("shutdown", (type, extension) => {
   if (backgroundPagesMap.has(extension)) {
     backgroundPagesMap.get(extension).shutdown();