Bug 1190688: Part 1 - [webext] Implement the activeTab permission. r=billm
☠☠ backed out by 689eebc89b6d ☠ ☠
authorKris Maglione <maglione.k@gmail.com>
Tue, 01 Dec 2015 20:37:41 -0800
changeset 275202 4a10c564dfca3189ab521c3ccfa92330163189b1
parent 275201 ebe2433705a55c4b91d5726ed81574e3bcb5c6fb
child 275203 1d5e9f3d094d075cfda767dd2108b07c7f8557de
push id16511
push usermaglione.k@gmail.com
push dateWed, 02 Dec 2015 16:52:18 +0000
treeherderfx-team@1d5e9f3d094d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1190688
milestone45.0a1
Bug 1190688: Part 1 - [webext] Implement the activeTab permission. r=billm
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -27,16 +27,18 @@ var nextActionId = 0;
 // Responsible for the browser_action section of the manifest as well
 // as the associated popup.
 function BrowserAction(options, extension)
 {
   this.extension = extension;
   this.id = makeWidgetId(extension.id) + "-browser-action";
   this.widget = null;
 
+  this.tabManager = TabManager.for(extension);
+
   let title = extension.localize(options.default_title || "");
   let popup = extension.localize(options.default_popup || "");
   if (popup) {
     popup = extension.baseURI.resolve(popup);
   }
 
   this.defaults = {
     enabled: true,
@@ -69,16 +71,17 @@ BrowserAction.prototype = {
 
         this.updateButton(node, this.defaults);
 
         let tabbrowser = document.defaultView.gBrowser;
 
         node.addEventListener("command", event => {
           let tab = tabbrowser.selectedTab;
           let popup = this.getProperty(tab, "popup");
+          this.tabManager.addActiveTabPermission(tab);
           if (popup) {
             this.togglePopup(node, popup);
           } else {
             this.emit("click");
           }
         });
 
         return node;
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -38,18 +38,21 @@ var menuBuilder = {
   build: function(contextData) {
     // TODO: icons should be set for items
     let xulMenu = contextData.menu;
     xulMenu.addEventListener("popuphidden", this);
     let doc = xulMenu.ownerDocument;
     for (let [ext, menuItemMap] of contextMenuMap) {
       let parentMap = new Map();
       let topLevelItems = new Set();
-      for (let [id, item] of menuItemMap) {
-        dump(id + " : " + item + "\n");
+      for (let entry of menuItemMap) {
+        // We need a closure over |item|, and we don't currently get a
+        // fresh binding per loop if we declare it in the loop head.
+        let [id, item] = entry;
+
         if (item.enabledForContext(contextData)) {
           let element;
           if (item.isMenu) {
             element = doc.createElement("menu");
             // Menu elements need to have a menupopup child for
             // its menu items.
             let menupopup = doc.createElement("menupopup");
             element.appendChild(menupopup);
@@ -74,25 +77,23 @@ var menuBuilder = {
             // If parentId is set we have to look up its parent
             // and appened to its menupopup.
             let parentElement = parentMap.get(parentId);
             parentElement.appendChild(element);
           } else {
             topLevelItems.add(element);
           }
 
-          if (item.onclick) {
-            function clickHandlerForItem(item) {
-              return event => {
-                let clickData = item.getClickData(contextData, event);
-                runSafe(item.extContext, item.onclick, clickData);
-              }
+          element.addEventListener("command", event => {
+            item.tabManager.addActiveTabPermission();
+            if (item.onclick) {
+              let clickData = item.getClickData(contextData, event);
+              runSafe(item.extContext, item.onclick, clickData);
             }
-            element.addEventListener("command", clickHandlerForItem(item));
-          }
+          });
         }
       }
       if (topLevelItems.size > 1) {
         // If more than one top level items are visible, callopse them.
         let top = doc.createElement("menu");
         top.setAttribute("label", ext.name);
         top.setAttribute("ext-type", "top-level-menu");
         let menupopup = doc.createElement("menupopup");
@@ -161,16 +162,18 @@ function getContexts(contextData) {
   return contexts;
 }
 
 function MenuItem(extension, extContext, createProperties)
 {
   this.extension = extension;
   this.extContext = extContext;
 
+  this.tabManager = TabManager.for(extension);
+
   this.init(createProperties);
 }
 
 MenuItem.prototype = {
   // init is called from the MenuItem ctor and from update. The |update|
   // flag is for the later case.
   init(createProperties, update=false) {
     let item = this;
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -15,16 +15,18 @@ var pageActionMap = new WeakMap();
 
 // Handles URL bar icons, including the |page_action| manifest entry
 // and associated API.
 function PageAction(options, extension)
 {
   this.extension = extension;
   this.id = makeWidgetId(extension.id) + "-page-action";
 
+  this.tabManager = TabManager.for(extension);
+
   let title = extension.localize(options.default_title || "");
   let popup = extension.localize(options.default_popup || "");
   if (popup) {
     popup = extension.baseURI.resolve(popup);
   }
 
   this.defaults = {
     show: false,
@@ -134,16 +136,18 @@ PageAction.prototype = {
   // window.
   // If the page action has a |popup| property, a panel is opened to
   // that URL. Otherwise, a "click" event is emitted, and dispatched to
   // the any click listeners in the add-on.
   handleClick(window) {
     let tab = window.gBrowser.selectedTab;
     let popup = this.tabContext.get(tab).popup;
 
+    this.tabManager.addActiveTabPermission(tab);
+
     if (popup) {
       openPanel(this.getButton(window), popup, this.extension);
     } else {
       this.emit("click", tab);
     }
   },
 
   handleLocationChange(eventType, tab, fromBrowse) {
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -452,23 +452,18 @@ extensions.registerAPI((extension, conte
           if (pattern && !pattern.matches(Services.io.newURI(tab.url, null, null))) {
             return false;
           }
 
           return true;
         }
 
         let result = [];
-        let e = Services.wm.getEnumerator("navigator:browser");
-        while (e.hasMoreElements()) {
-          let window = e.getNext();
-          if (window.document.readyState != "complete") {
-            continue;
-          }
-          let tabs = TabManager.getTabs(extension, window);
+        for (let window of WindowListManager.browserWindows()) {
+          let tabs = TabManager.for(extension).getTabs(window);
           for (let tab of tabs) {
             if (matches(window, tab)) {
               result.push(tab);
             }
           }
         }
         runSafe(context, callback, result);
       },
@@ -484,19 +479,25 @@ extensions.registerAPI((extension, conte
           // We need to send the inner window ID to make sure we only
           // execute the script if the window is currently navigated to
           // the document that we expect.
           //
           // TODO: When we add support for callbacks, non-matching
           // window IDs and insufficient permissions need to result in a
           // callback with |lastError| set.
           innerWindowID: tab.linkedBrowser.innerWindowID,
+        };
 
-          matchesHost: extension.whiteListedHosts.serialize(),
-        };
+        if (TabManager.for(extension).hasActiveTabPermission(tab)) {
+          // If we have the "activeTab" permission for this tab, ignore
+          // the host whitelist.
+          options.matchesHost = ["<all_urls>"];
+        } else {
+          options.matchesHost = extension.whiteListedHosts.serialize();
+        }
 
         if (details.code) {
           options[kind + 'Code'] = details.code;
         }
         if (details.file) {
           let url = context.uri.resolve(details.file);
           if (extension.isExtensionURL(url)) {
             // We should really set |lastError| here, and go straight to
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -262,17 +262,97 @@ TabContext.prototype = {
   },
 
   shutdown() {
     AllWindowEvents.removeListener("progress", this);
     AllWindowEvents.removeListener("TabSelect", this);
   },
 };
 
-// Manages mapping between XUL tabs and extension tab IDs.
+// Manages tab mappings and permissions for a specific extension.
+function ExtensionTabManager(extension) {
+  this.extension = extension;
+
+  // A mapping of tab objects to the inner window ID the extension currently has
+  // the active tab permission for. The active permission for a given tab is
+  // valid only for the inner window that was active when the permission was
+  // granted. If the tab navigates, the inner window ID changes, and the
+  // permission automatically becomes stale.
+  //
+  // WeakMap[tab => inner-window-id<int>]
+  this.hasTabPermissionFor = new WeakMap();
+}
+
+ExtensionTabManager.prototype = {
+  addActiveTabPermission(tab = TabManager.activeTab) {
+    if (this.extension.hasPermission("activeTab")) {
+      // Note that, unlike Chrome, we don't currently clear this permission with
+      // the tab navigates. If the inner window is revived from BFCache before
+      // we've granted this permission to a new inner window, the extension
+      // maintains its permissions for it.
+      this.hasTabPermissionFor.set(tab, tab.linkedBrowser.innerWindowID);
+    }
+  },
+
+  // Returns true if the extension has the "activeTab" permission for this tab.
+  // This is somewhat more permissive than the generic "tabs" permission, as
+  // checked by |hasTabPermission|, in that it also allows programmatic script
+  // injection without an explicit host permission.
+  hasActiveTabPermission(tab) {
+    // This check is redundant with addTabPermission, but cheap.
+    if (this.extension.hasPermission("activeTab")) {
+      return (this.hasTabPermissionFor.has(tab) &&
+              this.hasTabPermissionFor.get(tab) === tab.linkedBrowser.innerWindowID);
+    }
+    return false;
+  },
+
+  hasTabPermission(tab) {
+    return this.extension.hasPermission("tabs") || this.hasActiveTabPermission(tab);
+  },
+
+  convert(tab) {
+    let window = tab.ownerDocument.defaultView;
+    let windowActive = window == WindowManager.topWindow;
+
+    let result = {
+      id: TabManager.getId(tab),
+      index: tab._tPos,
+      windowId: WindowManager.getId(window),
+      selected: tab.selected,
+      highlighted: tab.selected,
+      active: tab.selected,
+      pinned: tab.pinned,
+      status: TabManager.getStatus(tab),
+      incognito: PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser),
+      width: tab.linkedBrowser.clientWidth,
+      height: tab.linkedBrowser.clientHeight,
+    };
+
+    if (this.hasTabPermission(tab)) {
+      result.url = tab.linkedBrowser.currentURI.spec;
+      if (tab.linkedBrowser.contentTitle) {
+        result.title = tab.linkedBrowser.contentTitle;
+      }
+      let icon = window.gBrowser.getIcon(tab);
+      if (icon) {
+        result.favIconUrl = icon;
+      }
+    }
+
+    return result;
+  },
+
+  getTabs(window) {
+    return Array.from(window.gBrowser.tabs, tab => this.convert(tab));
+  },
+};
+
+
+// Manages global mappings between XUL tabs and extension tab IDs.
 global.TabManager = {
   _tabs: new WeakMap(),
   _nextId: 1,
 
   getId(tab) {
     if (this._tabs.has(tab)) {
       return this._tabs.get(tab);
     }
@@ -317,54 +397,36 @@ global.TabManager = {
     return null;
   },
 
   getStatus(tab) {
     return tab.getAttribute("busy") == "true" ? "loading" : "complete";
   },
 
   convert(extension, tab) {
-    let window = tab.ownerDocument.defaultView;
-    let windowActive = window == WindowManager.topWindow;
-    let result = {
-      id: this.getId(tab),
-      index: tab._tPos,
-      windowId: WindowManager.getId(window),
-      selected: tab.selected,
-      highlighted: tab.selected,
-      active: tab.selected,
-      pinned: tab.pinned,
-      status: this.getStatus(tab),
-      incognito: PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser),
-      width: tab.linkedBrowser.clientWidth,
-      height: tab.linkedBrowser.clientHeight,
-    };
-
-    if (extension.hasPermission("tabs")) {
-      result.url = tab.linkedBrowser.currentURI.spec;
-      if (tab.linkedBrowser.contentTitle) {
-        result.title = tab.linkedBrowser.contentTitle;
-      }
-      let icon = window.gBrowser.getIcon(tab);
-      if (icon) {
-        result.favIconUrl = icon;
-      }
-    }
-
-    return result;
-  },
-
-  getTabs(extension, window) {
-    if (!window.gBrowser) {
-      return [];
-    }
-    return Array.map(window.gBrowser.tabs, tab => this.convert(extension, tab));
+    return TabManager.for(extension).convert(tab);
   },
 };
 
+// WeakMap[Extension -> ExtensionTabManager]
+let tabManagers = new WeakMap();
+
+// Returns the extension-specific tab manager for the given extension, or
+// creates one if it doesn't already exist.
+TabManager.for = function (extension) {
+  if (!tabManagers.has(extension)) {
+    tabManagers.set(extension, new ExtensionTabManager(extension));
+  }
+  return tabManagers.get(extension);
+};
+
+extensions.on("shutdown", (type, extension) => {
+  tabManagers.delete(extension);
+});
+
 // Manages mapping between XUL windows and extension window IDs.
 global.WindowManager = {
   _windows: new WeakMap(),
   _nextId: 0,
 
   WINDOW_ID_NONE: -1,
   WINDOW_ID_CURRENT: -2,
 
@@ -406,17 +468,17 @@ global.WindowManager = {
       incognito: PrivateBrowsingUtils.isWindowPrivate(window),
 
       // We fudge on these next two.
       type: this.windowType(window),
       state: window.fullScreen ? "fullscreen" : "normal",
     };
 
     if (getInfo && getInfo.populate) {
-      results.tabs = TabManager.getTabs(extension, window);
+      results.tabs = TabManager.for(extension).getTabs(window);
     }
 
     return result;
   },
 };
 
 // Manages listeners for window opening and closing. A window is
 // considered open when the "load" event fires on it. A window is