Bug 1190688: Part 1 - [webext] Implement the activeTab permission. r=billm
authorKris Maglione <maglione.k@gmail.com>
Tue, 01 Dec 2015 20:37:41 -0800
changeset 275261 72b3671ac8ef311a3469cb6ea74fe3bdd8ff21f1
parent 275260 efe43efb067aedd1a2550aaa3ca21a6bd402afea
child 275262 89e666316ea0c2cc12976b2bd7f6dc54c566e94f
push id29751
push usercbook@mozilla.com
push dateThu, 03 Dec 2015 10:59:01 +0000
treeherdermozilla-central@31fc97d173b3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1190688
milestone45.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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