Bug 1356891: Get rid of getAPILevelForWindow. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 16 Apr 2017 08:32:59 -0700
changeset 563381 45ef9d8bcfadc8f696df5ca7abfdcac19a2fc042
parent 563380 212c23df9dd7d77a61054850ecfe623461d068fb
child 624448 409888484c76a14f94c85bb3a8af504628297212
push id54271
push usermaglione.k@gmail.com
push dateSun, 16 Apr 2017 16:28:54 +0000
reviewersmixedpuppy
bugs1356891
milestone55.0a1
Bug 1356891: Get rid of getAPILevelForWindow. r?mixedpuppy MozReview-Commit-ID: 4IMnEiC5VAh
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/extension-process-script.js
toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -171,93 +171,16 @@ var Service = {
   // attached to the URI.
   extensionURIToAddonID(uri) {
     let uuid = uri.host;
     let extension = this.uuidMap.get(uuid);
     return extension ? extension.id : undefined;
   },
 };
 
-// API Levels Helpers
-
-// Find the add-on associated with this document via the
-// principal's addonId attribute. This value is computed by
-// extensionURIToAddonID, which ensures that we don't inject our
-// API into webAccessibleResources or remote web pages.
-function getAddonIdForWindow(window) {
-  return Cu.getObjectPrincipal(window).addonId;
-}
-
-const API_LEVELS = Object.freeze({
-  NO_PRIVILEGES: 0,
-  CONTENTSCRIPT_PRIVILEGES: 1,
-  FULL_PRIVILEGES: 2,
-});
-
-// Finds the API Level ("FULL_PRIVILEGES", "CONTENTSCRIPT_PRIVILEGES", "NO_PRIVILEGES")
-// with a given a window object.
-function getAPILevelForWindow(window, addonId) {
-  const {NO_PRIVILEGES, CONTENTSCRIPT_PRIVILEGES, FULL_PRIVILEGES} = API_LEVELS;
-
-  // Non WebExtension URLs and WebExtension URLs from a different extension
-  // has no access to APIs.
-  if (!addonId || getAddonIdForWindow(window) != addonId) {
-    return NO_PRIVILEGES;
-  }
-
-  if (!ExtensionManagement.isExtensionProcess) {
-    return CONTENTSCRIPT_PRIVILEGES;
-  }
-
-  let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
-
-  // Handling of ExtensionPages running inside sub-frames.
-  if (docShell.sameTypeParent) {
-    let parentWindow = docShell.sameTypeParent.QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIDOMWindow);
-
-    // The option page iframe embedded in the about:addons tab should have
-    // full API level privileges. (see Bug 1256282 for rationale)
-    let parentDocument = parentWindow.document;
-    let parentIsSystemPrincipal = Services.scriptSecurityManager
-                                          .isSystemPrincipal(parentDocument.nodePrincipal);
-
-    if (parentDocument.location.href == "about:addons" && parentIsSystemPrincipal) {
-      return FULL_PRIVILEGES;
-    }
-
-    // NOTE: Special handling for devtools panels using a chrome iframe here
-    // for the devtools panel, it is needed because a content iframe breaks
-    // switching between docked and undocked mode (see bug 1075490).
-    let devtoolsBrowser = parentDocument.querySelector(
-      "browser[webextension-view-type='devtools_panel']");
-    if (devtoolsBrowser && devtoolsBrowser.contentWindow === window &&
-        parentIsSystemPrincipal) {
-      return FULL_PRIVILEGES;
-    }
-
-    // The addon iframes embedded in a addon page from with the same addonId
-    // should have the same privileges of the sameTypeParent.
-    // (see Bug 1258347 for rationale)
-    let parentSameAddonPrivileges = getAPILevelForWindow(parentWindow, addonId);
-    if (parentSameAddonPrivileges > NO_PRIVILEGES) {
-      return parentSameAddonPrivileges;
-    }
-
-    // In all the other cases, WebExtension URLs loaded into sub-frame UI
-    // will have "content script API level privileges".
-    // (see Bug 1214658 for rationale)
-    return CONTENTSCRIPT_PRIVILEGES;
-  }
-
-  // WebExtension URLs loaded into top frames UI could have full API level privileges.
-  return FULL_PRIVILEGES;
-}
-
 let cacheInvalidated = 0;
 function onCacheInvalidate() {
   cacheInvalidated++;
 }
 Services.obs.addObserver(onCacheInvalidate, "startupcache-invalidate");
 
 ExtensionManagement = {
   get cacheInvalidated() {
@@ -274,18 +197,13 @@ ExtensionManagement = {
   startupExtension: Service.startupExtension.bind(Service),
   shutdownExtension: Service.shutdownExtension.bind(Service),
 
   registerAPI: APIs.register.bind(APIs),
   unregisterAPI: APIs.unregister.bind(APIs),
 
   getURLForExtension,
 
-  // exported API Level Helpers
-  getAddonIdForWindow,
-  getAPILevelForWindow,
-  API_LEVELS,
-
   APIs,
 };
 
 XPCOMUtils.defineLazyPreferenceGetter(ExtensionManagement, "useRemoteWebExtensions",
                                       "extensions.webextensions.remote", false);
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -443,36 +443,77 @@ DocumentManager = {
   preloadScripts(uri, loadInfo) {
     for (let script of this.contentScripts) {
       if (script.matchesLoadInfo(uri, loadInfo)) {
         script.preload();
       }
     }
   },
 
+  /**
+   * Checks that all parent frames for the given withdow either have the
+   * same add-on ID, or are special chrome-privileged documents such as
+   * about:addons or developer tools panels.
+   *
+   * @param {Window} window
+   *        The window to check.
+   * @param {string} addonId
+   *        The add-on ID to check.
+   * @returns {boolean}
+   */
+  checkParentFrames(window, addonId) {
+    while (window.parent !== window) {
+      window = window.parent;
+
+      let principal = window.document.nodePrincipal;
+
+      if (Services.scriptSecurityManager.isSystemPrincipal(principal)) {
+        // The add-on manager is a special case, since it contains extension
+        // options pages in same-type <browser> frames.
+        if (window.location.href === "about:addons") {
+          return true;
+        }
+
+        // NOTE: Special handling for devtools panels using a chrome iframe here
+        // for the devtools panel, it is needed because a content iframe breaks
+        // switching between docked and undocked mode (see bug 1075490).
+        let {frameElement} = window;
+        if (frameElement &&
+            frameElement.mozMatchesSelector("browser[webextension-view-type='devtools_panel']")) {
+          return true;
+        }
+      }
+
+      if (principal.addonId !== addonId) {
+        return false;
+      }
+    }
+
+    return true;
+  },
+
   loadInto(window) {
-    let extensionId = ExtensionManagement.getAddonIdForWindow(window);
-    if (!extensionId) {
+    let {addonId} = Cu.getObjectPrincipal(window);
+    if (!addonId) {
       return;
     }
 
-    let extension = ExtensionManager.get(extensionId);
+    let extension = ExtensionManager.get(addonId);
     if (!extension) {
-      throw new Error(`No registered extension for ID ${extensionId}`);
+      throw new Error(`No registered extension for ID ${addonId}`);
     }
 
-    let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
-    const levels = ExtensionManagement.API_LEVELS;
-
-    if (apiLevel === levels.CONTENTSCRIPT_PRIVILEGES) {
-      ExtensionContent.initExtensionContext(extension.realExtension, window);
-    } else if (apiLevel === levels.FULL_PRIVILEGES) {
+    if (this.checkParentFrames(window, addonId) && ExtensionManagement.isExtensionProcess) {
+      // We're in a top-level extension frame, or a sub-frame thereof,
+      // in the extension process. Inject the full extension page API.
       ExtensionPageChild.initExtensionContext(extension.realExtension, window);
     } else {
-      throw new Error(`Unexpected window with extension ID ${extensionId}`);
+      // We're in a content sub-frame or not in the extension process.
+      // Only inject a minimal content script API.
+      ExtensionContent.initExtensionContext(extension.realExtension, window);
     }
   },
 
   // Helpers
 
   * enumerateWindows(docShell) {
     if (docShell) {
       let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
deleted file mode 100644
--- a/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js
+++ /dev/null
@@ -1,70 +0,0 @@
-"use strict";
-
-const {Service} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
-Cu.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
-                                   "@mozilla.org/uuid-generator;1",
-                                   "nsIUUIDGenerator");
-
-function createWindowWithAddonId(addonId) {
-  const uuid = uuidGen.generateUUID().number.slice(1, -1);
-
-  const url = `moz-extension://${uuid}/`;
-
-  // Set the add-on ID for this moz-extensions: URL.
-  Service.uuidMap.set(uuid, {id: addonId});
-  do_register_cleanup(() => {
-    Service.uuidMap.delete(uuid);
-  });
-
-  let baseURI = Services.io.newURI(url);
-  let principal = Services.scriptSecurityManager
-                          .createCodebasePrincipal(baseURI, {});
-  let chromeNav = Services.appShell.createWindowlessBrowser(true);
-  let interfaceRequestor = chromeNav.QueryInterface(Ci.nsIInterfaceRequestor);
-  let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
-  docShell.createAboutBlankContentViewer(principal);
-
-  return {chromeNav, window: docShell.contentViewer.DOMDocument.defaultView};
-}
-
-add_task(function* test_eventpages() {
-  Service.init();
-
-  const {getAPILevelForWindow, getAddonIdForWindow} = ExtensionManagement;
-  const {NO_PRIVILEGES, FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS;
-  const FAKE_ADDON_ID = "fakeAddonId";
-  const OTHER_ADDON_ID = "otherFakeAddonId";
-  const EMPTY_ADDON_ID = "";
-
-  let fakeAddonId = createWindowWithAddonId(FAKE_ADDON_ID);
-  equal(getAddonIdForWindow(fakeAddonId.window), FAKE_ADDON_ID,
-        "the window has the expected addonId");
-
-  let apiLevel = getAPILevelForWindow(fakeAddonId.window, FAKE_ADDON_ID);
-  equal(apiLevel, FULL_PRIVILEGES,
-        "apiLevel for the window with the right addonId should be FULL_PRIVILEGES");
-
-  apiLevel = getAPILevelForWindow(fakeAddonId.window, OTHER_ADDON_ID);
-  equal(apiLevel, NO_PRIVILEGES,
-        "apiLevel for the window with a different addonId should be NO_PRIVILEGES");
-
-  fakeAddonId.chromeNav.close();
-
-  // NOTE: check that window with an empty addon Id (which are window that are
-  // not Extensions pages) always get no WebExtensions APIs.
-  let emptyAddonId = createWindowWithAddonId(EMPTY_ADDON_ID);
-  equal(getAddonIdForWindow(emptyAddonId.window), EMPTY_ADDON_ID,
-        "the window has the expected addonId");
-
-  apiLevel = getAPILevelForWindow(emptyAddonId.window, EMPTY_ADDON_ID);
-  equal(apiLevel, NO_PRIVILEGES,
-        "apiLevel for empty addonId should be NO_PRIVILEGES");
-
-  apiLevel = getAPILevelForWindow(emptyAddonId.window, OTHER_ADDON_ID);
-  equal(apiLevel, NO_PRIVILEGES,
-        "apiLevel for an 'empty addonId' window should be always NO_PRIVILEGES");
-
-  emptyAddonId.chromeNav.close();
-});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -75,17 +75,16 @@ skip-if = "android" # Bug 1350559
 [test_ext_storage_sync.js]
 head = head.js head_sync.js
 skip-if = os == "android"
 [test_ext_storage_sync_crypto.js]
 skip-if = os == "android"
 [test_ext_themes_supported_properties.js]
 [test_ext_topSites.js]
 skip-if = os == "android"
-[test_getAPILevelForWindow.js]
 [test_ext_legacy_extension_context.js]
 [test_ext_legacy_extension_embedding.js]
 [test_locale_converter.js]
 [test_locale_data.js]
 [test_native_messaging.js]
 skip-if = os == "android"
 [test_proxy_scripts.js]
 [include:xpcshell-content.ini]