Bug 1368289: Simplify frameloader global metadata tagging. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Sat, 27 May 2017 17:30:53 -0700
changeset 585673 97c36b5679052261352af14a0147fdd7c9d9cd16
parent 585672 505912bd4ef3cbf322f67b6c1ea923c5d4160357
child 630777 795ab1b8b05d19f46e298366735cdef94b2feda4
push id61171
push usermaglione.k@gmail.com
push dateSun, 28 May 2017 00:33:11 +0000
reviewersmixedpuppy
bugs1368289
milestone55.0a1
Bug 1368289: Simplify frameloader global metadata tagging. r?mixedpuppy MozReview-Commit-ID: 1hgTtWysmya
browser/components/extensions/ext-utils.js
toolkit/components/extensions/ExtensionPageChild.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/extension-process-script.js
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -227,17 +227,17 @@ class TabTracker extends TabTrackerBase 
         let {adoptedTab} = event.detail;
         if (adoptedTab) {
           this.adoptedTabs.set(adoptedTab, event.target);
 
           // This tab is being created to adopt a tab from a different window.
           // Copy the ID from the old tab to the new.
           this.setId(nativeTab, this.getId(adoptedTab));
 
-          adoptedTab.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", {
+          adoptedTab.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetFrameData", {
             windowId: windowTracker.getId(nativeTab.ownerGlobal),
           });
         }
 
         // Save the current tab, since the newly-created tab will likely be
         // active by the time the promise below resolves and the event is
         // dispatched.
         let currentTab = nativeTab.ownerGlobal.gBrowser.selectedTab;
--- a/toolkit/components/extensions/ExtensionPageChild.jsm
+++ b/toolkit/components/extensions/ExtensionPageChild.jsm
@@ -21,16 +21,21 @@ const Cr = Components.results;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
+XPCOMUtils.defineLazyGetter(
+  this, "processScript",
+  () => Cc["@mozilla.org/webextensions/extension-process-script;1"]
+          .getService().wrappedJSObject);
+
 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
 const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionChild.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
@@ -47,16 +52,20 @@ const {
 
 const {
   ChildAPIManager,
   Messenger,
 } = ExtensionChild;
 
 var ExtensionPageChild;
 
+function getFrameData(global) {
+  return processScript.getFrameData(global, true);
+}
+
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("addon");
     this.initialized = false;
   }
 
   lazyInit() {
     if (!this.initialized) {
@@ -143,18 +152,18 @@ class ExtensionBaseContextChild extends 
   }
 
   get principal() {
     return this.contentWindow.document.nodePrincipal;
   }
 
   get windowId() {
     if (["tab", "popup", "sidebar"].includes(this.viewType)) {
-      let globalView = ExtensionPageChild.contentGlobals.get(this.messageManager);
-      return globalView ? globalView.windowId : -1;
+      let frameData = getFrameData(this.messageManager);
+      return frameData ? frameData.windowId : -1;
     }
     return -1;
   }
 
   get tabId() {
     // Will be overwritten in the constructor if necessary.
     return -1;
   }
@@ -285,150 +294,53 @@ defineLazyGetter(DevToolsContextChild.pr
     incognito: this.incognito,
   });
 
   this.callOnClose(childManager);
 
   return childManager;
 });
 
-// All subframes in a tab, background page, popup, etc. have the same view type.
-// This class keeps track of such global state.
-// Note that this is created even for non-extension tabs because at present we
-// do not have a way to distinguish regular tabs from extension tabs at the
-// initialization of a frame script.
-class ContentGlobal {
-  /**
-   * @param {nsIContentFrameMessageManager} global The frame script's global.
-   */
-  constructor(global) {
-    this.global = global;
-    // Unless specified otherwise assume that the extension page is in a tab,
-    // because the majority of all class instances are going to be a tab. Any
-    // special views (background page, extension popup) will immediately send an
-    // Extension:InitExtensionView message to change the viewType.
-    this.viewType = "tab";
-    this.tabId = -1;
-    this.windowId = -1;
-    this.initialized = false;
-
-    this.global.addMessageListener("Extension:InitExtensionView", this);
-    this.global.addMessageListener("Extension:SetTabAndWindowId", this);
-  }
-
-  uninit() {
-    this.global.removeMessageListener("Extension:InitExtensionView", this);
-    this.global.removeMessageListener("Extension:SetTabAndWindowId", this);
-  }
-
-  ensureInitialized() {
-    if (!this.initialized) {
-      // Request tab and window ID in case "Extension:InitExtensionView" is not
-      // sent (e.g. when `viewType` is "tab").
-      let reply = this.global.sendSyncMessage("Extension:GetTabAndWindowId");
-      this.handleSetTabAndWindowId(reply[0] || {});
-    }
-    return this;
-  }
-
-  receiveMessage({name, data}) {
-    switch (name) {
-      case "Extension:InitExtensionView":
-        // The view type is initialized once and then fixed.
-        this.global.removeMessageListener("Extension:InitExtensionView", this);
-        this.viewType = data.viewType;
-
-        // Force external links to open in tabs.
-        if (["popup", "sidebar"].includes(this.viewType)) {
-          this.global.docShell.isAppTab = true;
-        }
-
-        if (data.devtoolsToolboxInfo) {
-          this.devtoolsToolboxInfo = data.devtoolsToolboxInfo;
-        }
-
-        promiseEvent(this.global, "DOMContentLoaded", true).then(() => {
-          let windowId = getInnerWindowID(this.global.content);
-          let context = ExtensionPageChild.extensionContexts.get(windowId);
-
-          this.global.sendAsyncMessage("Extension:ExtensionViewLoaded",
-                                       {childId: context && context.childManager.id});
-        });
-
-        /* FALLTHROUGH */
-      case "Extension:SetTabAndWindowId":
-        this.handleSetTabAndWindowId(data);
-        break;
-    }
-  }
-
-  handleSetTabAndWindowId(data) {
-    let {tabId, windowId} = data;
-
-    if (tabId) {
-      // Tab IDs are not expected to change.
-      if (this.tabId !== -1 && tabId !== this.tabId) {
-        throw new Error("Attempted to change a tabId after it was set");
-      }
-      this.tabId = tabId;
-    }
-
-    if (windowId !== undefined) {
-      // Window IDs may change if a tab is moved to a different location.
-      // Note: This is the ID of the browser window for the extension API.
-      // Do not confuse it with the innerWindowID of DOMWindows!
-      this.windowId = windowId;
-    }
-    this.initialized = true;
-  }
-}
-
 ExtensionPageChild = {
-  // Map<nsIContentFrameMessageManager, ContentGlobal>
-  contentGlobals: new Map(),
-
   // Map<innerWindowId, ExtensionPageContextChild>
   extensionContexts: new Map(),
 
   initialized: false,
 
   _init() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
 
     Services.obs.addObserver(this, "inner-window-destroyed"); // eslint-ignore-line mozilla/balanced-listeners
   },
 
-  init(global) {
-    if (!WebExtensionPolicy.isExtensionProcess) {
-      throw new Error("Cannot init extension page global in current process");
-    }
-
-    if (!this.contentGlobals.has(global)) {
-      this.contentGlobals.set(global, new ContentGlobal(global));
-    }
-  },
-
-  uninit(global) {
-    if (this.contentGlobals.has(global)) {
-      this.contentGlobals.get(global).uninit();
-      this.contentGlobals.delete(global);
-    }
-  },
-
   observe(subject, topic, data) {
     if (topic === "inner-window-destroyed") {
       let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
 
       this.destroyExtensionContext(windowId);
     }
   },
 
+  expectViewLoad(global, viewType) {
+    if (["popup", "sidebar"].includes(viewType)) {
+      global.docShell.isAppTab = true;
+    }
+
+    promiseEvent(global, "DOMContentLoaded", true).then(() => {
+      let windowId = getInnerWindowID(global.content);
+      let context = this.extensionContexts.get(windowId);
+
+      global.sendAsyncMessage("Extension:ExtensionViewLoaded",
+                              {childId: context && context.childManager.id});
+    });
+  },
+
   /**
    * Create a privileged context at document-element-inserted.
    *
    * @param {BrowserExtensionContent} extension
    *     The extension for which the context should be created.
    * @param {nsIDOMWindow} contentWindow The global of the page.
    */
   initExtensionContext(extension, contentWindow) {
@@ -447,17 +359,17 @@ ExtensionPageChild = {
       throw new Error("An extension context was already initialized for this frame");
     }
 
     let mm = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIDocShell)
                           .QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIContentFrameMessageManager);
 
-    let {viewType, tabId, devtoolsToolboxInfo} = this.contentGlobals.get(mm).ensureInitialized();
+    let {viewType, tabId, devtoolsToolboxInfo} = getFrameData(mm) || {};
 
     let uri = contentWindow.document.documentURIObject;
 
     if (devtoolsToolboxInfo) {
       context = new DevToolsContextChild(extension, {
         viewType, contentWindow, uri, tabId, devtoolsToolboxInfo,
       });
     } else {
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -138,17 +138,17 @@ let apiManager = new class extends Schem
   receiveMessage({name, target, sync}) {
     if (name === "Extension:GetTabAndWindowId") {
       let result = this.global.tabTracker.getBrowserData(target);
 
       if (result.tabId) {
         if (sync) {
           return result;
         }
-        target.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", result);
+        target.messageManager.sendAsyncMessage("Extension:SetFrameData", result);
       }
     }
   }
 }();
 
 // Subscribes to messages related to the extension messaging API and forwards it
 // to the relevant message manager. The "sender" field for the `onMessage` and
 // `onConnect` events are updated if needed.
@@ -276,17 +276,17 @@ GlobalManager = {
 
     let viewType = browser.getAttribute("webextension-view-type");
     if (viewType) {
       let data = {viewType};
 
       let {tabTracker} = apiManager.global;
       Object.assign(data, tabTracker.getBrowserData(browser), additionalData);
 
-      browser.messageManager.sendAsyncMessage("Extension:InitExtensionView",
+      browser.messageManager.sendAsyncMessage("Extension:SetFrameData",
                                               data);
     }
   },
 
   getExtension(extensionId) {
     return this.extensionMap.get(extensionId);
   },
 
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -78,31 +78,54 @@ function getMessageManager(window) {
 }
 
 var DocumentManager;
 var ExtensionManager;
 
 class ExtensionGlobal {
   constructor(global) {
     this.global = global;
+    this.global.addMessageListener("Extension:SetFrameData", this);
+
+    this.frameData = null;
 
     MessageChannel.addListener(global, "Extension:Capture", this);
     MessageChannel.addListener(global, "Extension:DetectLanguage", this);
     MessageChannel.addListener(global, "Extension:Execute", this);
     MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
     MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
   }
 
   get messageFilterStrict() {
     return {
       innerWindowID: getInnerWindowID(this.global.content),
     };
   }
 
-  receiveMessage({target, messageName, recipient, data}) {
+  getFrameData(force = false) {
+    if (!this.frameData && force) {
+      this.frameData = this.global.sendSyncMessage("Extension:GetTabAndWindowId")[0];
+    }
+    return this.frameData;
+  }
+
+  receiveMessage({target, messageName, recipient, data, name}) {
+    switch (name) {
+      case "Extension:SetFrameData":
+        if (this.frameData) {
+          Object.assign(this.frameData, data);
+        } else {
+          this.frameData = data;
+        }
+        if (data.viewType && WebExtensionPolicy.isExtensionProcess) {
+          ExtensionPageChild.expectViewLoad(this.global, data.viewType);
+        }
+        return;
+    }
+
     switch (messageName) {
       case "Extension:Capture":
         return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options);
       case "Extension:DetectLanguage":
         return ExtensionContent.handleDetectLanguage(this.global, target);
       case "Extension:Execute":
         let policy = WebExtensionPolicy.getByID(recipient.extensionId);
 
@@ -125,59 +148,31 @@ class ExtensionGlobal {
         return ExtensionContent.handleWebNavigationGetAllFrames(this.global);
     }
   }
 }
 
 // Responsible for creating ExtensionContexts and injecting content
 // scripts into them when new documents are created.
 DocumentManager = {
-  globals: new Map(),
+  globals: new WeakMap(),
 
   // Initialize listeners that we need regardless of whether extensions are
   // enabled.
   earlyInit() {
     Services.obs.addObserver(this, "tab-content-frameloader-created"); // eslint-disable-line mozilla/balanced-listeners
   },
 
-  extensionProcessInitialized: false,
-  initExtensionProcess() {
-    if (this.extensionProcessInitialized || !WebExtensionPolicy.isExtensionProcess) {
-      return;
-    }
-    this.extensionProcessInitialized = true;
-
-    for (let global of this.globals.keys()) {
-      ExtensionPageChild.init(global);
-    }
-  },
-
   // Initialize a frame script global which extension contexts may be loaded
   // into.
   initGlobal(global) {
-    // Note: {once: true} does not work as expected here.
-    global.addEventListener("unload", event => { // eslint-disable-line mozilla/balanced-listeners
-      this.uninitGlobal(global);
-    });
-
     this.globals.set(global, new ExtensionGlobal(global));
-    if (this.extensionProcessInitialized && WebExtensionPolicy.isExtensionProcess) {
-      ExtensionPageChild.init(global);
-    }
-  },
-  uninitGlobal(global) {
-    if (this.extensionProcessInitialized) {
-      ExtensionPageChild.uninit(global);
-    }
-    this.globals.delete(global);
   },
 
   initExtension(extension) {
-    this.initExtensionProcess();
-
     this.injectExtensionScripts(extension);
   },
 
   // Listeners
 
   observe(subject, topic, data) {
     if (topic == "tab-content-frameloader-created") {
       this.initGlobal(subject);
@@ -378,16 +373,21 @@ function ExtensionProcessScript() {
 ExtensionProcessScript.singleton = null;
 
 ExtensionProcessScript.prototype = {
   classID: Components.ID("{21f9819e-4cdf-49f9-85a0-850af91a5058}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.mozIExtensionProcessScript]),
 
   get wrappedJSObject() { return this; },
 
+  getFrameData(global, force) {
+    let extGlobal = DocumentManager.globals.get(global);
+    return extGlobal && extGlobal.getFrameData(force);
+  },
+
   initExtension(data) {
     return ExtensionManager.initExtensionPolicy(data);
   },
 
   initExtensionDocument(policy, doc) {
     if (DocumentManager.globals.has(getMessageManager(doc.defaultView))) {
       DocumentManager.loadInto(policy, doc.defaultView);
     }