Bug 1387907: Merge ext-browser.js and ext-utils.js. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 06 Aug 2017 18:20:25 -0700
changeset 641430 02cbf8020c6d333c71cc4b0ea351e20cf66754ce
parent 641429 36e0186dbae57187eae72c844e21ddf4ea8758e5
child 724778 e0ba7f13a0fa1ab84601844ef26ed0a97ce9023f
push id72513
push usermaglione.k@gmail.com
push dateMon, 07 Aug 2017 01:24:48 +0000
reviewersmixedpuppy
bugs1387907
milestone57.0a1
Bug 1387907: Merge ext-browser.js and ext-utils.js. r?mixedpuppy MozReview-Commit-ID: CmOax5b5Gki
browser/components/extensions/ext-browser.js
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-commands.js
browser/components/extensions/ext-devtools-panels.js
browser/components/extensions/ext-devtools.js
browser/components/extensions/ext-menus.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-sessions.js
browser/components/extensions/ext-sidebarAction.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
browser/components/extensions/ext-windows.js
browser/components/extensions/extensions-browser.manifest
browser/components/extensions/jar.mn
toolkit/components/extensions/ext-toolkit.js
toolkit/components/extensions/extensions-toolkit.manifest
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -1,14 +1,32 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-// The ext-* files are imported into the same scopes.
-/* import-globals-from ext-utils.js */
+// This file provides some useful code for the |tabs| and |windows|
+// modules. All of the code is installed on |global|, which is a scope
+// shared among the different ext-*.js scripts.
+
+/* global EventEmitter:false, TabContext:false, WindowEventManager:false,
+          makeWidgetId:false, tabTracker:true, windowTracker:true */
+/* import-globals-from ../../../toolkit/components/extensions/ext-toolkit.js */
+
+/* globals TabBase, WindowBase, TabTrackerBase, WindowTrackerBase, TabManagerBase, WindowManagerBase */
 
-global.EventEmitter = ExtensionUtils.EventEmitter;
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+var {
+  ExtensionError,
+  defineLazyGetter,
+} = ExtensionUtils;
+
+let tabTracker;
+let windowTracker;
 
 // This function is pretty tightly tied to Extension.jsm.
 // Its job is to fill in the |tab| property of the sender.
 const getSender = (extension, target, sender) => {
   let tabId;
   if ("tabId" in sender) {
     // The message came from a privileged extension page running in a tab. In
     // that case, it should include a tabId property (which is filled in by the
@@ -69,8 +87,824 @@ global.openOptionsPage = (extension) => 
     });
     return Promise.resolve();
   }
 
   let viewId = `addons://detail/${encodeURIComponent(extension.id)}/preferences`;
 
   return window.BrowserOpenAddonsMgr(viewId);
 };
+
+global.makeWidgetId = id => {
+  id = id.toLowerCase();
+  // FIXME: This allows for collisions.
+  return id.replace(/[^a-z0-9_-]/g, "_");
+};
+
+// Manages tab-specific context data, and dispatching tab select events
+// across all windows.
+global.TabContext = class extends EventEmitter {
+  constructor(getDefaults, extension) {
+    super();
+
+    this.extension = extension;
+    this.getDefaults = getDefaults;
+
+    this.tabData = new WeakMap();
+    this.lastLocation = new WeakMap();
+
+    windowTracker.addListener("progress", this);
+    windowTracker.addListener("TabSelect", this);
+  }
+
+  get(nativeTab) {
+    if (!this.tabData.has(nativeTab)) {
+      this.tabData.set(nativeTab, this.getDefaults(nativeTab));
+    }
+
+    return this.tabData.get(nativeTab);
+  }
+
+  clear(nativeTab) {
+    this.tabData.delete(nativeTab);
+  }
+
+  handleEvent(event) {
+    if (event.type == "TabSelect") {
+      let nativeTab = event.target;
+      this.emit("tab-select", nativeTab);
+      this.emit("location-change", nativeTab);
+    }
+  }
+
+  onStateChange(browser, webProgress, request, stateFlags, statusCode) {
+    let flags = Ci.nsIWebProgressListener;
+
+    if (!(~stateFlags & (flags.STATE_IS_WINDOW | flags.STATE_START) ||
+          this.lastLocation.has(browser))) {
+      this.lastLocation.set(browser, request.URI);
+    }
+  }
+
+  onLocationChange(browser, webProgress, request, locationURI, flags) {
+    let gBrowser = browser.ownerGlobal.gBrowser;
+    let lastLocation = this.lastLocation.get(browser);
+    if (browser === gBrowser.selectedBrowser &&
+        !(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) {
+      let nativeTab = gBrowser.getTabForBrowser(browser);
+      this.emit("location-change", nativeTab, true);
+    }
+    this.lastLocation.set(browser, browser.currentURI);
+  }
+
+  shutdown() {
+    windowTracker.removeListener("progress", this);
+    windowTracker.removeListener("TabSelect", this);
+  }
+};
+
+
+class WindowTracker extends WindowTrackerBase {
+  addProgressListener(window, listener) {
+    window.gBrowser.addTabsProgressListener(listener);
+  }
+
+  removeProgressListener(window, listener) {
+    window.gBrowser.removeTabsProgressListener(listener);
+  }
+}
+
+/**
+ * An event manager API provider which listens for a DOM event in any browser
+ * window, and calls the given listener function whenever an event is received.
+ * That listener function receives a `fire` object, which it can use to dispatch
+ * events to the extension, and a DOM event object.
+ *
+ * @param {BaseContext} context
+ *        The extension context which the event manager belongs to.
+ * @param {string} name
+ *        The API name of the event manager, e.g.,"runtime.onMessage".
+ * @param {string} event
+ *        The name of the DOM event to listen for.
+ * @param {function} listener
+ *        The listener function to call when a DOM event is received.
+ */
+global.WindowEventManager = class extends EventManager {
+  constructor(context, name, event, listener) {
+    super(context, name, fire => {
+      let listener2 = listener.bind(null, fire);
+
+      windowTracker.addListener(event, listener2);
+      return () => {
+        windowTracker.removeListener(event, listener2);
+      };
+    });
+  }
+};
+
+class TabTracker extends TabTrackerBase {
+  constructor() {
+    super();
+
+    this._tabs = new WeakMap();
+    this._tabIds = new Map();
+    this._nextId = 1;
+
+    this._handleTabDestroyed = this._handleTabDestroyed.bind(this);
+  }
+
+  init() {
+    if (this.initialized) {
+      return;
+    }
+    this.initialized = true;
+
+    this.adoptedTabs = new WeakMap();
+
+    this._handleWindowOpen = this._handleWindowOpen.bind(this);
+    this._handleWindowClose = this._handleWindowClose.bind(this);
+
+    windowTracker.addListener("TabClose", this);
+    windowTracker.addListener("TabOpen", this);
+    windowTracker.addListener("TabSelect", this);
+    windowTracker.addOpenListener(this._handleWindowOpen);
+    windowTracker.addCloseListener(this._handleWindowClose);
+
+    /* eslint-disable mozilla/balanced-listeners */
+    this.on("tab-detached", this._handleTabDestroyed);
+    this.on("tab-removed", this._handleTabDestroyed);
+    /* eslint-enable mozilla/balanced-listeners */
+  }
+
+  getId(nativeTab) {
+    if (this._tabs.has(nativeTab)) {
+      return this._tabs.get(nativeTab);
+    }
+
+    this.init();
+
+    let id = this._nextId++;
+    this.setId(nativeTab, id);
+    return id;
+  }
+
+  setId(nativeTab, id) {
+    this._tabs.set(nativeTab, id);
+    this._tabIds.set(id, nativeTab);
+  }
+
+  _handleTabDestroyed(event, {nativeTab}) {
+    let id = this._tabs.get(nativeTab);
+    if (id) {
+      this._tabs.delete(nativeTab);
+      if (this._tabIds.get(id) === nativeTab) {
+        this._tabIds.delete(id);
+      }
+    }
+  }
+
+  /**
+   * Returns the XUL <tab> element associated with the given tab ID. If no tab
+   * with the given ID exists, and no default value is provided, an error is
+   * raised, belonging to the scope of the given context.
+   *
+   * @param {integer} tabId
+   *        The ID of the tab to retrieve.
+   * @param {*} default_
+   *        The value to return if no tab exists with the given ID.
+   * @returns {Element<tab>}
+   *        A XUL <tab> element.
+   */
+  getTab(tabId, default_ = undefined) {
+    let nativeTab = this._tabIds.get(tabId);
+    if (nativeTab) {
+      return nativeTab;
+    }
+    if (default_ !== undefined) {
+      return default_;
+    }
+    throw new ExtensionError(`Invalid tab ID: ${tabId}`);
+  }
+
+  /**
+   * Sets the opener of `tab` to the ID `openerTab`. Both tabs must be in the
+   * same window, or this function will throw a type error.
+   *
+   * @param {Element} tab The tab for which to set the owner.
+   * @param {Element} openerTab The opener of <tab>.
+   */
+  setOpener(tab, openerTab) {
+    if (tab.ownerDocument !== openerTab.ownerDocument) {
+      throw new Error("Tab must be in the same window as its opener");
+    }
+    tab.openerTab = openerTab;
+  }
+
+  /**
+   * @param {Event} event
+   *        The DOM Event to handle.
+   * @private
+   */
+  handleEvent(event) {
+    let nativeTab = event.target;
+
+    switch (event.type) {
+      case "TabOpen":
+        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: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;
+
+        // We need to delay sending this event until the next tick, since the
+        // tab does not have its final index when the TabOpen event is dispatched.
+        Promise.resolve().then(() => {
+          if (event.detail.adoptedTab) {
+            this.emitAttached(event.originalTarget);
+          } else {
+            this.emitCreated(event.originalTarget, currentTab);
+          }
+        });
+        break;
+
+      case "TabClose":
+        let {adoptedBy} = event.detail;
+        if (adoptedBy) {
+          // This tab is being closed because it was adopted by a new window.
+          // Copy its ID to the new tab, in case it was created as the first tab
+          // of a new window, and did not have an `adoptedTab` detail when it was
+          // opened.
+          this.setId(adoptedBy, this.getId(nativeTab));
+
+          this.emitDetached(nativeTab, adoptedBy);
+        } else {
+          this.emitRemoved(nativeTab, false);
+        }
+        break;
+
+      case "TabSelect":
+        // Because we are delaying calling emitCreated above, we also need to
+        // delay sending this event because it shouldn't fire before onCreated.
+        Promise.resolve().then(() => {
+          this.emitActivated(nativeTab);
+        });
+        break;
+    }
+  }
+
+  /**
+   * A private method which is called whenever a new browser window is opened,
+   * and dispatches the necessary events for it.
+   *
+   * @param {DOMWindow} window
+   *        The window being opened.
+   * @private
+   */
+  _handleWindowOpen(window) {
+    if (window.arguments && window.arguments[0] instanceof window.XULElement) {
+      // If the first window argument is a XUL element, it means the
+      // window is about to adopt a tab from another window to replace its
+      // initial tab.
+      //
+      // Note that this event handler depends on running before the
+      // delayed startup code in browser.js, which is currently triggered
+      // by the first MozAfterPaint event. That code handles finally
+      // adopting the tab, and clears it from the arguments list in the
+      // process, so if we run later than it, we're too late.
+      let nativeTab = window.arguments[0];
+      let adoptedBy = window.gBrowser.tabs[0];
+
+      this.adoptedTabs.set(nativeTab, adoptedBy);
+      this.setId(adoptedBy, this.getId(nativeTab));
+
+      // We need to be sure to fire this event after the onDetached event
+      // for the original tab.
+      let listener = (event, details) => {
+        if (details.nativeTab === nativeTab) {
+          this.off("tab-detached", listener);
+
+          Promise.resolve().then(() => {
+            this.emitAttached(details.adoptedBy);
+          });
+        }
+      };
+
+      this.on("tab-detached", listener);
+    } else {
+      for (let nativeTab of window.gBrowser.tabs) {
+        this.emitCreated(nativeTab);
+      }
+
+      // emitActivated to trigger tab.onActivated/tab.onHighlighted for a newly opened window.
+      this.emitActivated(window.gBrowser.tabs[0]);
+    }
+  }
+
+  /**
+   * A private method which is called whenever a browser window is closed,
+   * and dispatches the necessary events for it.
+   *
+   * @param {DOMWindow} window
+   *        The window being closed.
+   * @private
+   */
+  _handleWindowClose(window) {
+    for (let nativeTab of window.gBrowser.tabs) {
+      if (this.adoptedTabs.has(nativeTab)) {
+        this.emitDetached(nativeTab, this.adoptedTabs.get(nativeTab));
+      } else {
+        this.emitRemoved(nativeTab, true);
+      }
+    }
+  }
+
+  /**
+   * Emits a "tab-activated" event for the given tab element.
+   *
+   * @param {NativeTab} nativeTab
+   *        The tab element which has been activated.
+   * @private
+   */
+  emitActivated(nativeTab) {
+    this.emit("tab-activated", {
+      tabId: this.getId(nativeTab),
+      windowId: windowTracker.getId(nativeTab.ownerGlobal)});
+  }
+
+  /**
+   * Emits a "tab-attached" event for the given tab element.
+   *
+   * @param {NativeTab} nativeTab
+   *        The tab element in the window to which the tab is being attached.
+   * @private
+   */
+  emitAttached(nativeTab) {
+    let newWindowId = windowTracker.getId(nativeTab.ownerGlobal);
+    let tabId = this.getId(nativeTab);
+
+    this.emit("tab-attached", {nativeTab, tabId, newWindowId, newPosition: nativeTab._tPos});
+  }
+
+  /**
+   * Emits a "tab-detached" event for the given tab element.
+   *
+   * @param {NativeTab} nativeTab
+   *        The tab element in the window from which the tab is being detached.
+   * @param {NativeTab} adoptedBy
+   *        The tab element in the window to which detached tab is being moved,
+   *        and will adopt this tab's contents.
+   * @private
+   */
+  emitDetached(nativeTab, adoptedBy) {
+    let oldWindowId = windowTracker.getId(nativeTab.ownerGlobal);
+    let tabId = this.getId(nativeTab);
+
+    this.emit("tab-detached", {nativeTab, adoptedBy, tabId, oldWindowId, oldPosition: nativeTab._tPos});
+  }
+
+  /**
+   * Emits a "tab-created" event for the given tab element.
+   *
+   * @param {NativeTab} nativeTab
+   *        The tab element which is being created.
+   * @param {NativeTab} [currentTab]
+   *        The tab element for the currently active tab.
+   * @private
+   */
+  emitCreated(nativeTab, currentTab) {
+    this.emit("tab-created", {nativeTab, currentTab});
+  }
+
+  /**
+   * Emits a "tab-removed" event for the given tab element.
+   *
+   * @param {NativeTab} nativeTab
+   *        The tab element which is being removed.
+   * @param {boolean} isWindowClosing
+   *        True if the tab is being removed because the browser window is
+   *        closing.
+   * @private
+   */
+  emitRemoved(nativeTab, isWindowClosing) {
+    let windowId = windowTracker.getId(nativeTab.ownerGlobal);
+    let tabId = this.getId(nativeTab);
+
+    // When addons run in-process, `window.close()` is synchronous. Most other
+    // addon-invoked calls are asynchronous since they go through a proxy
+    // context via the message manager. This includes event registrations such
+    // as `tabs.onRemoved.addListener`.
+    //
+    // So, even if `window.close()` were to be called (in-process) after calling
+    // `tabs.onRemoved.addListener`, then the tab would be closed before the
+    // event listener is registered. To make sure that the event listener is
+    // notified, we dispatch `tabs.onRemoved` asynchronously.
+    Services.tm.dispatchToMainThread(() => {
+      this.emit("tab-removed", {nativeTab, tabId, windowId, isWindowClosing});
+    });
+  }
+
+  getBrowserData(browser) {
+    if (browser.ownerGlobal.location.href === "about:addons") {
+      // When we're loaded into a <browser> inside about:addons, we need to go up
+      // one more level.
+      browser = browser.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDocShell)
+                       .chromeEventHandler;
+    }
+
+    let result = {
+      tabId: -1,
+      windowId: -1,
+    };
+
+    let {gBrowser} = browser.ownerGlobal;
+    // Some non-browser windows have gBrowser but not
+    // getTabForBrowser!
+    if (gBrowser && gBrowser.getTabForBrowser) {
+      result.windowId = windowTracker.getId(browser.ownerGlobal);
+
+      let nativeTab = gBrowser.getTabForBrowser(browser);
+      if (nativeTab) {
+        result.tabId = this.getId(nativeTab);
+      }
+    }
+
+    return result;
+  }
+
+  get activeTab() {
+    let window = windowTracker.topWindow;
+    if (window && window.gBrowser) {
+      return window.gBrowser.selectedTab;
+    }
+    return null;
+  }
+}
+
+windowTracker = new WindowTracker();
+tabTracker = new TabTracker();
+
+Object.assign(global, {tabTracker, windowTracker});
+
+class Tab extends TabBase {
+  get _favIconUrl() {
+    return this.window.gBrowser.getIcon(this.nativeTab);
+  }
+
+  get audible() {
+    return this.nativeTab.soundPlaying;
+  }
+
+  get browser() {
+    return this.nativeTab.linkedBrowser;
+  }
+
+  get frameLoader() {
+    // If we don't have a frameLoader yet, just return a dummy with no width and
+    // height.
+    return super.frameLoader || {lazyWidth: 0, lazyHeight: 0};
+  }
+
+  get cookieStoreId() {
+    return getCookieStoreIdForTab(this, this.nativeTab);
+  }
+
+  get openerTabId() {
+    let opener = this.nativeTab.openerTab;
+    if (opener && opener.parentNode && opener.ownerDocument == this.nativeTab.ownerDocument) {
+      return tabTracker.getId(opener);
+    }
+    return null;
+  }
+
+  get height() {
+    return this.frameLoader.lazyHeight;
+  }
+
+  get index() {
+    return this.nativeTab._tPos;
+  }
+
+  get mutedInfo() {
+    let {nativeTab} = this;
+
+    let mutedInfo = {muted: nativeTab.muted};
+    if (nativeTab.muteReason === null) {
+      mutedInfo.reason = "user";
+    } else if (nativeTab.muteReason) {
+      mutedInfo.reason = "extension";
+      mutedInfo.extensionId = nativeTab.muteReason;
+    }
+
+    return mutedInfo;
+  }
+
+  get lastAccessed() {
+    return this.nativeTab.lastAccessed;
+  }
+
+  get pinned() {
+    return this.nativeTab.pinned;
+  }
+
+  get active() {
+    return this.nativeTab.selected;
+  }
+
+  get selected() {
+    return this.nativeTab.selected;
+  }
+
+  get status() {
+    if (this.nativeTab.getAttribute("busy") === "true") {
+      return "loading";
+    }
+    return "complete";
+  }
+
+  get width() {
+    return this.frameLoader.lazyWidth;
+  }
+
+  get window() {
+    return this.nativeTab.ownerGlobal;
+  }
+
+  get windowId() {
+    return windowTracker.getId(this.window);
+  }
+
+  /**
+   * Converts session store data to an object compatible with the return value
+   * of the convert() method, representing that data.
+   *
+   * @param {Extension} extension
+   *        The extension for which to convert the data.
+   * @param {Object} tabData
+   *        Session store data for a closed tab, as returned by
+   *        `SessionStore.getClosedTabData()`.
+   * @param {DOMWindow} [window = null]
+   *        The browser window which the tab belonged to before it was closed.
+   *        May be null if the window the tab belonged to no longer exists.
+   *
+   * @returns {Object}
+   * @static
+   */
+  static convertFromSessionStoreClosedData(extension, tabData, window = null) {
+    let result = {
+      sessionId: String(tabData.closedId),
+      index: tabData.pos ? tabData.pos : 0,
+      windowId: window && windowTracker.getId(window),
+      highlighted: false,
+      active: false,
+      pinned: false,
+      incognito: Boolean(tabData.state && tabData.state.isPrivate),
+      lastAccessed: tabData.state ? tabData.state.lastAccessed : tabData.lastAccessed,
+    };
+
+    if (extension.tabManager.hasTabPermission(tabData)) {
+      let entries = tabData.state ? tabData.state.entries : tabData.entries;
+      let entry = entries[entries.length - 1];
+      result.url = entry.url;
+      result.title = entry.title;
+      if (tabData.image) {
+        result.favIconUrl = tabData.image;
+      }
+    }
+
+    return result;
+  }
+}
+
+class Window extends WindowBase {
+  /**
+   * Update the geometry of the browser window.
+   *
+   * @param {Object} options
+   *        An object containing new values for the window's geometry.
+   * @param {integer} [options.left]
+   *        The new pixel distance of the left side of the browser window from
+   *        the left of the screen.
+   * @param {integer} [options.top]
+   *        The new pixel distance of the top side of the browser window from
+   *        the top of the screen.
+   * @param {integer} [options.width]
+   *        The new pixel width of the window.
+   * @param {integer} [options.height]
+   *        The new pixel height of the window.
+   */
+  updateGeometry(options) {
+    let {window} = this;
+
+    if (options.left !== null || options.top !== null) {
+      let left = options.left !== null ? options.left : window.screenX;
+      let top = options.top !== null ? options.top : window.screenY;
+      window.moveTo(left, top);
+    }
+
+    if (options.width !== null || options.height !== null) {
+      let width = options.width !== null ? options.width : window.outerWidth;
+      let height = options.height !== null ? options.height : window.outerHeight;
+      window.resizeTo(width, height);
+    }
+  }
+
+  get title() {
+    return this.window.document.title;
+  }
+
+  setTitlePreface(titlePreface) {
+    this.window.document.documentElement.setAttribute("titlepreface", titlePreface);
+  }
+
+  get focused() {
+    return this.window.document.hasFocus();
+  }
+
+  get top() {
+    return this.window.screenY;
+  }
+
+  get left() {
+    return this.window.screenX;
+  }
+
+  get width() {
+    return this.window.outerWidth;
+  }
+
+  get height() {
+    return this.window.outerHeight;
+  }
+
+  get incognito() {
+    return PrivateBrowsingUtils.isWindowPrivate(this.window);
+  }
+
+  get alwaysOnTop() {
+    return this.xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ;
+  }
+
+  get isLastFocused() {
+    return this.window === windowTracker.topWindow;
+  }
+
+  static getState(window) {
+    const STATES = {
+      [window.STATE_MAXIMIZED]: "maximized",
+      [window.STATE_MINIMIZED]: "minimized",
+      [window.STATE_NORMAL]: "normal",
+    };
+    let state = STATES[window.windowState];
+    if (window.fullScreen) {
+      state = "fullscreen";
+    }
+    return state;
+  }
+
+  get state() {
+    return Window.getState(this.window);
+  }
+
+  set state(state) {
+    let {window} = this;
+    if (state !== "fullscreen" && window.fullScreen) {
+      window.fullScreen = false;
+    }
+
+    switch (state) {
+      case "maximized":
+        window.maximize();
+        break;
+
+      case "minimized":
+      case "docked":
+        window.minimize();
+        break;
+
+      case "normal":
+        // Restore sometimes returns the window to its previous state, rather
+        // than to the "normal" state, so it may need to be called anywhere from
+        // zero to two times.
+        window.restore();
+        if (window.windowState !== window.STATE_NORMAL) {
+          window.restore();
+        }
+        if (window.windowState !== window.STATE_NORMAL) {
+          // And on OS-X, where normal vs. maximized is basically a heuristic,
+          // we need to cheat.
+          window.sizeToContent();
+        }
+        break;
+
+      case "fullscreen":
+        window.fullScreen = true;
+        break;
+
+      default:
+        throw new Error(`Unexpected window state: ${state}`);
+    }
+  }
+
+  * getTabs() {
+    let {tabManager} = this.extension;
+
+    for (let nativeTab of this.window.gBrowser.tabs) {
+      yield tabManager.getWrapper(nativeTab);
+    }
+  }
+
+  /**
+   * Converts session store data to an object compatible with the return value
+   * of the convert() method, representing that data.
+   *
+   * @param {Extension} extension
+   *        The extension for which to convert the data.
+   * @param {Object} windowData
+   *        Session store data for a closed window, as returned by
+   *        `SessionStore.getClosedWindowData()`.
+   *
+   * @returns {Object}
+   * @static
+   */
+  static convertFromSessionStoreClosedData(extension, windowData) {
+    let result = {
+      sessionId: String(windowData.closedId),
+      focused: false,
+      incognito: false,
+      type: "normal", // this is always "normal" for a closed window
+      // Surely this does not actually work?
+      state: this.getState(windowData),
+      alwaysOnTop: false,
+    };
+
+    if (windowData.tabs.length) {
+      result.tabs = windowData.tabs.map(tabData => {
+        return Tab.convertFromSessionStoreClosedData(extension, tabData);
+      });
+    }
+
+    return result;
+  }
+}
+
+Object.assign(global, {Tab, Window});
+
+class TabManager extends TabManagerBase {
+  get(tabId, default_ = undefined) {
+    let nativeTab = tabTracker.getTab(tabId, default_);
+
+    if (nativeTab) {
+      return this.getWrapper(nativeTab);
+    }
+    return default_;
+  }
+
+  addActiveTabPermission(nativeTab = tabTracker.activeTab) {
+    return super.addActiveTabPermission(nativeTab);
+  }
+
+  revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
+    return super.revokeActiveTabPermission(nativeTab);
+  }
+
+  wrapTab(nativeTab) {
+    return new Tab(this.extension, nativeTab, tabTracker.getId(nativeTab));
+  }
+}
+
+class WindowManager extends WindowManagerBase {
+  get(windowId, context) {
+    let window = windowTracker.getWindow(windowId, context);
+
+    return this.getWrapper(window);
+  }
+
+  * getAll() {
+    for (let window of windowTracker.browserWindows()) {
+      yield this.getWrapper(window);
+    }
+  }
+
+  wrapWindow(window) {
+    return new Window(this.extension, window, windowTracker.getId(window));
+  }
+}
+
+
+extensions.on("startup", (type, extension) => { // eslint-disable-line mozilla/balanced-listeners
+  defineLazyGetter(extension, "tabManager",
+                   () => new TabManager(extension));
+  defineLazyGetter(extension, "windowManager",
+                   () => new WindowManager(extension));
+});
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -1,17 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 /* exported browserActionFor, sidebarActionFor, pageActionFor */
 /* global browserActionFor:false, sidebarActionFor:false, pageActionFor:false */
 
 // The ext-* files are imported into the same scopes.
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
                                   "resource://gre/modules/Timer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
                                   "resource://gre/modules/Timer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -1,15 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-browserAction.js */
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
                                   "resource://gre/modules/ExtensionParent.jsm");
 
 var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 this.commands = class extends ExtensionAPI {
   onManifestEntry(entryName) {
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -1,15 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-devtools.js */
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
 
 var {
   IconDetails,
--- a/browser/components/extensions/ext-devtools.js
+++ b/browser/components/extensions/ext-devtools.js
@@ -1,17 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 /* exported getDevToolsTargetForContext */
 /* global getTargetTabIdForToolbox, getDevToolsTargetForContext */
 
 // The ext-* files are imported into the same scopes.
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 /**
  * This module provides helpers used by the other specialized `ext-devtools-*.js` modules
  * and the implementation of the `devtools_page`.
  */
 
 XPCOMUtils.defineLazyModuleGetter(this, "DevToolsShim",
                                   "chrome://devtools-shim/content/DevToolsShim.jsm");
--- a/browser/components/extensions/ext-menus.js
+++ b/browser/components/extensions/ext-menus.js
@@ -1,14 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 var {
   ExtensionError,
 } = ExtensionUtils;
 
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -1,15 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-browserAction.js */
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 XPCOMUtils.defineLazyModuleGetter(this, "PanelPopup",
                                   "resource:///modules/ExtensionPopups.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
                                   "resource://gre/modules/TelemetryStopwatch.jsm");
 
 
 var {
--- a/browser/components/extensions/ext-sessions.js
+++ b/browser/components/extensions/ext-sessions.js
@@ -1,14 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 var {
   ExtensionError,
   promiseObserved,
 } = ExtensionUtils;
 
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
                                   "resource:///modules/sessionstore/SessionStore.jsm");
--- a/browser/components/extensions/ext-sidebarAction.js
+++ b/browser/components/extensions/ext-sidebarAction.js
@@ -1,14 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -1,14 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
   const stringSvc = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);
   return stringSvc.createBundle("chrome://global/locale/extensions.properties");
 });
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
deleted file mode 100644
--- a/browser/components/extensions/ext-utils.js
+++ /dev/null
@@ -1,841 +0,0 @@
-/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set sts=2 sw=2 et tw=80: */
-"use strict";
-
-/* exported WindowEventManager, makeWidgetId */
-/* global EventEmitter:false, TabContext:false, WindowEventManager:false,
-          makeWidgetId:false, tabTracker:true, windowTracker:true */
-/* import-globals-from ../../../toolkit/components/extensions/ext-toolkit.js */
-
-/* globals TabBase, WindowBase, TabTrackerBase, WindowTrackerBase, TabManagerBase, WindowManagerBase */
-
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
-
-var {
-  ExtensionError,
-  defineLazyGetter,
-} = ExtensionUtils;
-
-let tabTracker;
-let windowTracker;
-
-// This file provides some useful code for the |tabs| and |windows|
-// modules. All of the code is installed on |global|, which is a scope
-// shared among the different ext-*.js scripts.
-
-global.makeWidgetId = id => {
-  id = id.toLowerCase();
-  // FIXME: This allows for collisions.
-  return id.replace(/[^a-z0-9_-]/g, "_");
-};
-
-// Manages tab-specific context data, and dispatching tab select events
-// across all windows.
-global.TabContext = class extends EventEmitter {
-  constructor(getDefaults, extension) {
-    super();
-
-    this.extension = extension;
-    this.getDefaults = getDefaults;
-
-    this.tabData = new WeakMap();
-    this.lastLocation = new WeakMap();
-
-    windowTracker.addListener("progress", this);
-    windowTracker.addListener("TabSelect", this);
-  }
-
-  get(nativeTab) {
-    if (!this.tabData.has(nativeTab)) {
-      this.tabData.set(nativeTab, this.getDefaults(nativeTab));
-    }
-
-    return this.tabData.get(nativeTab);
-  }
-
-  clear(nativeTab) {
-    this.tabData.delete(nativeTab);
-  }
-
-  handleEvent(event) {
-    if (event.type == "TabSelect") {
-      let nativeTab = event.target;
-      this.emit("tab-select", nativeTab);
-      this.emit("location-change", nativeTab);
-    }
-  }
-
-  onStateChange(browser, webProgress, request, stateFlags, statusCode) {
-    let flags = Ci.nsIWebProgressListener;
-
-    if (!(~stateFlags & (flags.STATE_IS_WINDOW | flags.STATE_START) ||
-          this.lastLocation.has(browser))) {
-      this.lastLocation.set(browser, request.URI);
-    }
-  }
-
-  onLocationChange(browser, webProgress, request, locationURI, flags) {
-    let gBrowser = browser.ownerGlobal.gBrowser;
-    let lastLocation = this.lastLocation.get(browser);
-    if (browser === gBrowser.selectedBrowser &&
-        !(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) {
-      let nativeTab = gBrowser.getTabForBrowser(browser);
-      this.emit("location-change", nativeTab, true);
-    }
-    this.lastLocation.set(browser, browser.currentURI);
-  }
-
-  shutdown() {
-    windowTracker.removeListener("progress", this);
-    windowTracker.removeListener("TabSelect", this);
-  }
-};
-
-
-class WindowTracker extends WindowTrackerBase {
-  addProgressListener(window, listener) {
-    window.gBrowser.addTabsProgressListener(listener);
-  }
-
-  removeProgressListener(window, listener) {
-    window.gBrowser.removeTabsProgressListener(listener);
-  }
-}
-
-/**
- * An event manager API provider which listens for a DOM event in any browser
- * window, and calls the given listener function whenever an event is received.
- * That listener function receives a `fire` object, which it can use to dispatch
- * events to the extension, and a DOM event object.
- *
- * @param {BaseContext} context
- *        The extension context which the event manager belongs to.
- * @param {string} name
- *        The API name of the event manager, e.g.,"runtime.onMessage".
- * @param {string} event
- *        The name of the DOM event to listen for.
- * @param {function} listener
- *        The listener function to call when a DOM event is received.
- */
-global.WindowEventManager = class extends EventManager {
-  constructor(context, name, event, listener) {
-    super(context, name, fire => {
-      let listener2 = listener.bind(null, fire);
-
-      windowTracker.addListener(event, listener2);
-      return () => {
-        windowTracker.removeListener(event, listener2);
-      };
-    });
-  }
-};
-
-class TabTracker extends TabTrackerBase {
-  constructor() {
-    super();
-
-    this._tabs = new WeakMap();
-    this._tabIds = new Map();
-    this._nextId = 1;
-
-    this._handleTabDestroyed = this._handleTabDestroyed.bind(this);
-  }
-
-  init() {
-    if (this.initialized) {
-      return;
-    }
-    this.initialized = true;
-
-    this.adoptedTabs = new WeakMap();
-
-    this._handleWindowOpen = this._handleWindowOpen.bind(this);
-    this._handleWindowClose = this._handleWindowClose.bind(this);
-
-    windowTracker.addListener("TabClose", this);
-    windowTracker.addListener("TabOpen", this);
-    windowTracker.addListener("TabSelect", this);
-    windowTracker.addOpenListener(this._handleWindowOpen);
-    windowTracker.addCloseListener(this._handleWindowClose);
-
-    /* eslint-disable mozilla/balanced-listeners */
-    this.on("tab-detached", this._handleTabDestroyed);
-    this.on("tab-removed", this._handleTabDestroyed);
-    /* eslint-enable mozilla/balanced-listeners */
-  }
-
-  getId(nativeTab) {
-    if (this._tabs.has(nativeTab)) {
-      return this._tabs.get(nativeTab);
-    }
-
-    this.init();
-
-    let id = this._nextId++;
-    this.setId(nativeTab, id);
-    return id;
-  }
-
-  setId(nativeTab, id) {
-    this._tabs.set(nativeTab, id);
-    this._tabIds.set(id, nativeTab);
-  }
-
-  _handleTabDestroyed(event, {nativeTab}) {
-    let id = this._tabs.get(nativeTab);
-    if (id) {
-      this._tabs.delete(nativeTab);
-      if (this._tabIds.get(id) === nativeTab) {
-        this._tabIds.delete(id);
-      }
-    }
-  }
-
-  /**
-   * Returns the XUL <tab> element associated with the given tab ID. If no tab
-   * with the given ID exists, and no default value is provided, an error is
-   * raised, belonging to the scope of the given context.
-   *
-   * @param {integer} tabId
-   *        The ID of the tab to retrieve.
-   * @param {*} default_
-   *        The value to return if no tab exists with the given ID.
-   * @returns {Element<tab>}
-   *        A XUL <tab> element.
-   */
-  getTab(tabId, default_ = undefined) {
-    let nativeTab = this._tabIds.get(tabId);
-    if (nativeTab) {
-      return nativeTab;
-    }
-    if (default_ !== undefined) {
-      return default_;
-    }
-    throw new ExtensionError(`Invalid tab ID: ${tabId}`);
-  }
-
-  /**
-   * Sets the opener of `tab` to the ID `openerTab`. Both tabs must be in the
-   * same window, or this function will throw a type error.
-   *
-   * @param {Element} tab The tab for which to set the owner.
-   * @param {Element} openerTab The opener of <tab>.
-   */
-  setOpener(tab, openerTab) {
-    if (tab.ownerDocument !== openerTab.ownerDocument) {
-      throw new Error("Tab must be in the same window as its opener");
-    }
-    tab.openerTab = openerTab;
-  }
-
-  /**
-   * @param {Event} event
-   *        The DOM Event to handle.
-   * @private
-   */
-  handleEvent(event) {
-    let nativeTab = event.target;
-
-    switch (event.type) {
-      case "TabOpen":
-        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: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;
-
-        // We need to delay sending this event until the next tick, since the
-        // tab does not have its final index when the TabOpen event is dispatched.
-        Promise.resolve().then(() => {
-          if (event.detail.adoptedTab) {
-            this.emitAttached(event.originalTarget);
-          } else {
-            this.emitCreated(event.originalTarget, currentTab);
-          }
-        });
-        break;
-
-      case "TabClose":
-        let {adoptedBy} = event.detail;
-        if (adoptedBy) {
-          // This tab is being closed because it was adopted by a new window.
-          // Copy its ID to the new tab, in case it was created as the first tab
-          // of a new window, and did not have an `adoptedTab` detail when it was
-          // opened.
-          this.setId(adoptedBy, this.getId(nativeTab));
-
-          this.emitDetached(nativeTab, adoptedBy);
-        } else {
-          this.emitRemoved(nativeTab, false);
-        }
-        break;
-
-      case "TabSelect":
-        // Because we are delaying calling emitCreated above, we also need to
-        // delay sending this event because it shouldn't fire before onCreated.
-        Promise.resolve().then(() => {
-          this.emitActivated(nativeTab);
-        });
-        break;
-    }
-  }
-
-  /**
-   * A private method which is called whenever a new browser window is opened,
-   * and dispatches the necessary events for it.
-   *
-   * @param {DOMWindow} window
-   *        The window being opened.
-   * @private
-   */
-  _handleWindowOpen(window) {
-    if (window.arguments && window.arguments[0] instanceof window.XULElement) {
-      // If the first window argument is a XUL element, it means the
-      // window is about to adopt a tab from another window to replace its
-      // initial tab.
-      //
-      // Note that this event handler depends on running before the
-      // delayed startup code in browser.js, which is currently triggered
-      // by the first MozAfterPaint event. That code handles finally
-      // adopting the tab, and clears it from the arguments list in the
-      // process, so if we run later than it, we're too late.
-      let nativeTab = window.arguments[0];
-      let adoptedBy = window.gBrowser.tabs[0];
-
-      this.adoptedTabs.set(nativeTab, adoptedBy);
-      this.setId(adoptedBy, this.getId(nativeTab));
-
-      // We need to be sure to fire this event after the onDetached event
-      // for the original tab.
-      let listener = (event, details) => {
-        if (details.nativeTab === nativeTab) {
-          this.off("tab-detached", listener);
-
-          Promise.resolve().then(() => {
-            this.emitAttached(details.adoptedBy);
-          });
-        }
-      };
-
-      this.on("tab-detached", listener);
-    } else {
-      for (let nativeTab of window.gBrowser.tabs) {
-        this.emitCreated(nativeTab);
-      }
-
-      // emitActivated to trigger tab.onActivated/tab.onHighlighted for a newly opened window.
-      this.emitActivated(window.gBrowser.tabs[0]);
-    }
-  }
-
-  /**
-   * A private method which is called whenever a browser window is closed,
-   * and dispatches the necessary events for it.
-   *
-   * @param {DOMWindow} window
-   *        The window being closed.
-   * @private
-   */
-  _handleWindowClose(window) {
-    for (let nativeTab of window.gBrowser.tabs) {
-      if (this.adoptedTabs.has(nativeTab)) {
-        this.emitDetached(nativeTab, this.adoptedTabs.get(nativeTab));
-      } else {
-        this.emitRemoved(nativeTab, true);
-      }
-    }
-  }
-
-  /**
-   * Emits a "tab-activated" event for the given tab element.
-   *
-   * @param {NativeTab} nativeTab
-   *        The tab element which has been activated.
-   * @private
-   */
-  emitActivated(nativeTab) {
-    this.emit("tab-activated", {
-      tabId: this.getId(nativeTab),
-      windowId: windowTracker.getId(nativeTab.ownerGlobal)});
-  }
-
-  /**
-   * Emits a "tab-attached" event for the given tab element.
-   *
-   * @param {NativeTab} nativeTab
-   *        The tab element in the window to which the tab is being attached.
-   * @private
-   */
-  emitAttached(nativeTab) {
-    let newWindowId = windowTracker.getId(nativeTab.ownerGlobal);
-    let tabId = this.getId(nativeTab);
-
-    this.emit("tab-attached", {nativeTab, tabId, newWindowId, newPosition: nativeTab._tPos});
-  }
-
-  /**
-   * Emits a "tab-detached" event for the given tab element.
-   *
-   * @param {NativeTab} nativeTab
-   *        The tab element in the window from which the tab is being detached.
-   * @param {NativeTab} adoptedBy
-   *        The tab element in the window to which detached tab is being moved,
-   *        and will adopt this tab's contents.
-   * @private
-   */
-  emitDetached(nativeTab, adoptedBy) {
-    let oldWindowId = windowTracker.getId(nativeTab.ownerGlobal);
-    let tabId = this.getId(nativeTab);
-
-    this.emit("tab-detached", {nativeTab, adoptedBy, tabId, oldWindowId, oldPosition: nativeTab._tPos});
-  }
-
-  /**
-   * Emits a "tab-created" event for the given tab element.
-   *
-   * @param {NativeTab} nativeTab
-   *        The tab element which is being created.
-   * @param {NativeTab} [currentTab]
-   *        The tab element for the currently active tab.
-   * @private
-   */
-  emitCreated(nativeTab, currentTab) {
-    this.emit("tab-created", {nativeTab, currentTab});
-  }
-
-  /**
-   * Emits a "tab-removed" event for the given tab element.
-   *
-   * @param {NativeTab} nativeTab
-   *        The tab element which is being removed.
-   * @param {boolean} isWindowClosing
-   *        True if the tab is being removed because the browser window is
-   *        closing.
-   * @private
-   */
-  emitRemoved(nativeTab, isWindowClosing) {
-    let windowId = windowTracker.getId(nativeTab.ownerGlobal);
-    let tabId = this.getId(nativeTab);
-
-    // When addons run in-process, `window.close()` is synchronous. Most other
-    // addon-invoked calls are asynchronous since they go through a proxy
-    // context via the message manager. This includes event registrations such
-    // as `tabs.onRemoved.addListener`.
-    //
-    // So, even if `window.close()` were to be called (in-process) after calling
-    // `tabs.onRemoved.addListener`, then the tab would be closed before the
-    // event listener is registered. To make sure that the event listener is
-    // notified, we dispatch `tabs.onRemoved` asynchronously.
-    Services.tm.dispatchToMainThread(() => {
-      this.emit("tab-removed", {nativeTab, tabId, windowId, isWindowClosing});
-    });
-  }
-
-  getBrowserData(browser) {
-    if (browser.ownerGlobal.location.href === "about:addons") {
-      // When we're loaded into a <browser> inside about:addons, we need to go up
-      // one more level.
-      browser = browser.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell)
-                       .chromeEventHandler;
-    }
-
-    let result = {
-      tabId: -1,
-      windowId: -1,
-    };
-
-    let {gBrowser} = browser.ownerGlobal;
-    // Some non-browser windows have gBrowser but not
-    // getTabForBrowser!
-    if (gBrowser && gBrowser.getTabForBrowser) {
-      result.windowId = windowTracker.getId(browser.ownerGlobal);
-
-      let nativeTab = gBrowser.getTabForBrowser(browser);
-      if (nativeTab) {
-        result.tabId = this.getId(nativeTab);
-      }
-    }
-
-    return result;
-  }
-
-  get activeTab() {
-    let window = windowTracker.topWindow;
-    if (window && window.gBrowser) {
-      return window.gBrowser.selectedTab;
-    }
-    return null;
-  }
-}
-
-windowTracker = new WindowTracker();
-tabTracker = new TabTracker();
-
-Object.assign(global, {tabTracker, windowTracker});
-
-class Tab extends TabBase {
-  get _favIconUrl() {
-    return this.window.gBrowser.getIcon(this.nativeTab);
-  }
-
-  get audible() {
-    return this.nativeTab.soundPlaying;
-  }
-
-  get browser() {
-    return this.nativeTab.linkedBrowser;
-  }
-
-  get frameLoader() {
-    // If we don't have a frameLoader yet, just return a dummy with no width and
-    // height.
-    return super.frameLoader || {lazyWidth: 0, lazyHeight: 0};
-  }
-
-  get cookieStoreId() {
-    return getCookieStoreIdForTab(this, this.nativeTab);
-  }
-
-  get openerTabId() {
-    let opener = this.nativeTab.openerTab;
-    if (opener && opener.parentNode && opener.ownerDocument == this.nativeTab.ownerDocument) {
-      return tabTracker.getId(opener);
-    }
-    return null;
-  }
-
-  get height() {
-    return this.frameLoader.lazyHeight;
-  }
-
-  get index() {
-    return this.nativeTab._tPos;
-  }
-
-  get mutedInfo() {
-    let {nativeTab} = this;
-
-    let mutedInfo = {muted: nativeTab.muted};
-    if (nativeTab.muteReason === null) {
-      mutedInfo.reason = "user";
-    } else if (nativeTab.muteReason) {
-      mutedInfo.reason = "extension";
-      mutedInfo.extensionId = nativeTab.muteReason;
-    }
-
-    return mutedInfo;
-  }
-
-  get lastAccessed() {
-    return this.nativeTab.lastAccessed;
-  }
-
-  get pinned() {
-    return this.nativeTab.pinned;
-  }
-
-  get active() {
-    return this.nativeTab.selected;
-  }
-
-  get selected() {
-    return this.nativeTab.selected;
-  }
-
-  get status() {
-    if (this.nativeTab.getAttribute("busy") === "true") {
-      return "loading";
-    }
-    return "complete";
-  }
-
-  get width() {
-    return this.frameLoader.lazyWidth;
-  }
-
-  get window() {
-    return this.nativeTab.ownerGlobal;
-  }
-
-  get windowId() {
-    return windowTracker.getId(this.window);
-  }
-
-  /**
-   * Converts session store data to an object compatible with the return value
-   * of the convert() method, representing that data.
-   *
-   * @param {Extension} extension
-   *        The extension for which to convert the data.
-   * @param {Object} tabData
-   *        Session store data for a closed tab, as returned by
-   *        `SessionStore.getClosedTabData()`.
-   * @param {DOMWindow} [window = null]
-   *        The browser window which the tab belonged to before it was closed.
-   *        May be null if the window the tab belonged to no longer exists.
-   *
-   * @returns {Object}
-   * @static
-   */
-  static convertFromSessionStoreClosedData(extension, tabData, window = null) {
-    let result = {
-      sessionId: String(tabData.closedId),
-      index: tabData.pos ? tabData.pos : 0,
-      windowId: window && windowTracker.getId(window),
-      highlighted: false,
-      active: false,
-      pinned: false,
-      incognito: Boolean(tabData.state && tabData.state.isPrivate),
-      lastAccessed: tabData.state ? tabData.state.lastAccessed : tabData.lastAccessed,
-    };
-
-    if (extension.tabManager.hasTabPermission(tabData)) {
-      let entries = tabData.state ? tabData.state.entries : tabData.entries;
-      let entry = entries[entries.length - 1];
-      result.url = entry.url;
-      result.title = entry.title;
-      if (tabData.image) {
-        result.favIconUrl = tabData.image;
-      }
-    }
-
-    return result;
-  }
-}
-
-class Window extends WindowBase {
-  /**
-   * Update the geometry of the browser window.
-   *
-   * @param {Object} options
-   *        An object containing new values for the window's geometry.
-   * @param {integer} [options.left]
-   *        The new pixel distance of the left side of the browser window from
-   *        the left of the screen.
-   * @param {integer} [options.top]
-   *        The new pixel distance of the top side of the browser window from
-   *        the top of the screen.
-   * @param {integer} [options.width]
-   *        The new pixel width of the window.
-   * @param {integer} [options.height]
-   *        The new pixel height of the window.
-   */
-  updateGeometry(options) {
-    let {window} = this;
-
-    if (options.left !== null || options.top !== null) {
-      let left = options.left !== null ? options.left : window.screenX;
-      let top = options.top !== null ? options.top : window.screenY;
-      window.moveTo(left, top);
-    }
-
-    if (options.width !== null || options.height !== null) {
-      let width = options.width !== null ? options.width : window.outerWidth;
-      let height = options.height !== null ? options.height : window.outerHeight;
-      window.resizeTo(width, height);
-    }
-  }
-
-  get title() {
-    return this.window.document.title;
-  }
-
-  setTitlePreface(titlePreface) {
-    this.window.document.documentElement.setAttribute("titlepreface", titlePreface);
-  }
-
-  get focused() {
-    return this.window.document.hasFocus();
-  }
-
-  get top() {
-    return this.window.screenY;
-  }
-
-  get left() {
-    return this.window.screenX;
-  }
-
-  get width() {
-    return this.window.outerWidth;
-  }
-
-  get height() {
-    return this.window.outerHeight;
-  }
-
-  get incognito() {
-    return PrivateBrowsingUtils.isWindowPrivate(this.window);
-  }
-
-  get alwaysOnTop() {
-    return this.xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ;
-  }
-
-  get isLastFocused() {
-    return this.window === windowTracker.topWindow;
-  }
-
-  static getState(window) {
-    const STATES = {
-      [window.STATE_MAXIMIZED]: "maximized",
-      [window.STATE_MINIMIZED]: "minimized",
-      [window.STATE_NORMAL]: "normal",
-    };
-    let state = STATES[window.windowState];
-    if (window.fullScreen) {
-      state = "fullscreen";
-    }
-    return state;
-  }
-
-  get state() {
-    return Window.getState(this.window);
-  }
-
-  set state(state) {
-    let {window} = this;
-    if (state !== "fullscreen" && window.fullScreen) {
-      window.fullScreen = false;
-    }
-
-    switch (state) {
-      case "maximized":
-        window.maximize();
-        break;
-
-      case "minimized":
-      case "docked":
-        window.minimize();
-        break;
-
-      case "normal":
-        // Restore sometimes returns the window to its previous state, rather
-        // than to the "normal" state, so it may need to be called anywhere from
-        // zero to two times.
-        window.restore();
-        if (window.windowState !== window.STATE_NORMAL) {
-          window.restore();
-        }
-        if (window.windowState !== window.STATE_NORMAL) {
-          // And on OS-X, where normal vs. maximized is basically a heuristic,
-          // we need to cheat.
-          window.sizeToContent();
-        }
-        break;
-
-      case "fullscreen":
-        window.fullScreen = true;
-        break;
-
-      default:
-        throw new Error(`Unexpected window state: ${state}`);
-    }
-  }
-
-  * getTabs() {
-    let {tabManager} = this.extension;
-
-    for (let nativeTab of this.window.gBrowser.tabs) {
-      yield tabManager.getWrapper(nativeTab);
-    }
-  }
-
-  /**
-   * Converts session store data to an object compatible with the return value
-   * of the convert() method, representing that data.
-   *
-   * @param {Extension} extension
-   *        The extension for which to convert the data.
-   * @param {Object} windowData
-   *        Session store data for a closed window, as returned by
-   *        `SessionStore.getClosedWindowData()`.
-   *
-   * @returns {Object}
-   * @static
-   */
-  static convertFromSessionStoreClosedData(extension, windowData) {
-    let result = {
-      sessionId: String(windowData.closedId),
-      focused: false,
-      incognito: false,
-      type: "normal", // this is always "normal" for a closed window
-      // Surely this does not actually work?
-      state: this.getState(windowData),
-      alwaysOnTop: false,
-    };
-
-    if (windowData.tabs.length) {
-      result.tabs = windowData.tabs.map(tabData => {
-        return Tab.convertFromSessionStoreClosedData(extension, tabData);
-      });
-    }
-
-    return result;
-  }
-}
-
-Object.assign(global, {Tab, Window});
-
-class TabManager extends TabManagerBase {
-  get(tabId, default_ = undefined) {
-    let nativeTab = tabTracker.getTab(tabId, default_);
-
-    if (nativeTab) {
-      return this.getWrapper(nativeTab);
-    }
-    return default_;
-  }
-
-  addActiveTabPermission(nativeTab = tabTracker.activeTab) {
-    return super.addActiveTabPermission(nativeTab);
-  }
-
-  revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
-    return super.revokeActiveTabPermission(nativeTab);
-  }
-
-  wrapTab(nativeTab) {
-    return new Tab(this.extension, nativeTab, tabTracker.getId(nativeTab));
-  }
-}
-
-class WindowManager extends WindowManagerBase {
-  get(windowId, context) {
-    let window = windowTracker.getWindow(windowId, context);
-
-    return this.getWrapper(window);
-  }
-
-  * getAll() {
-    for (let window of windowTracker.browserWindows()) {
-      yield this.getWrapper(window);
-    }
-  }
-
-  wrapWindow(window) {
-    return new Window(this.extension, window, windowTracker.getId(window));
-  }
-}
-
-
-extensions.on("startup", (type, extension) => { // eslint-disable-line mozilla/balanced-listeners
-  defineLazyGetter(extension, "tabManager",
-                   () => new TabManager(extension));
-  defineLazyGetter(extension, "windowManager",
-                   () => new WindowManager(extension));
-});
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -1,14 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 // The ext-* files are imported into the same scopes.
-/* import-globals-from ext-utils.js */
+/* import-globals-from ext-browser.js */
 
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 var {
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -1,8 +1,7 @@
 category webextension-modules browser chrome://browser/content/ext-browser.json
 
-category webextension-scripts browser chrome://browser/content/ext-browser.js
-category webextension-scripts utils chrome://browser/content/ext-utils.js
+category webextension-scripts c-browser chrome://browser/content/ext-browser.js
 category webextension-scripts-devtools browser chrome://browser/content/ext-c-browser.js
 category webextension-scripts-addon browser chrome://browser/content/ext-c-browser.js
 
 category webextension-schemas menus_internal chrome://browser/content/schemas/menus_internal.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -27,17 +27,16 @@ browser.jar:
     content/browser/ext-history.js
     content/browser/ext-menus.js
     content/browser/ext-omnibox.js
     content/browser/ext-pageAction.js
     content/browser/ext-sessions.js
     content/browser/ext-sidebarAction.js
     content/browser/ext-tabs.js
     content/browser/ext-url-overrides.js
-    content/browser/ext-utils.js
     content/browser/ext-windows.js
     content/browser/ext-c-browser.js
     content/browser/ext-c-devtools-inspectedWindow.js
     content/browser/ext-c-devtools-panels.js
     content/browser/ext-c-devtools.js
     content/browser/ext-c-menus.js
     content/browser/ext-c-omnibox.js
     content/browser/ext-c-tabs.js
--- a/toolkit/components/extensions/ext-toolkit.js
+++ b/toolkit/components/extensions/ext-toolkit.js
@@ -12,16 +12,17 @@
           isDefaultCookieStoreId: false, isPrivateCookieStoreId:false,
           EventManager: false, InputEventManager: false */
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
                                   "resource://gre/modules/ContextualIdentityService.jsm");
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 
+global.EventEmitter = ExtensionUtils.EventEmitter;
 global.EventManager = ExtensionCommon.EventManager;
 global.InputEventManager = class extends EventManager {
   constructor(...args) {
     super(...args);
     this.inputHandling = true;
   }
 };
 
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -1,13 +1,14 @@
 # scripts
 category webextension-modules toolkit chrome://extensions/content/ext-toolkit.json
 
-category webextension-scripts tabs-base chrome://extensions/content/ext-tabs-base.js
-category webextension-scripts toolkit chrome://extensions/content/ext-toolkit.js
+category webextension-scripts a-toolkit chrome://extensions/content/ext-toolkit.js
+category webextension-scripts b-tabs-base chrome://extensions/content/ext-tabs-base.js
+
 category webextension-scripts-content toolkit chrome://extensions/content/ext-c-toolkit.js
 category webextension-scripts-devtools toolkit chrome://extensions/content/ext-c-toolkit.js
 category webextension-scripts-addon toolkit chrome://extensions/content/ext-c-toolkit.js
 
 category webextension-schemas events chrome://extensions/content/schemas/events.json
 category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
 category webextension-schemas types chrome://extensions/content/schemas/types.json