Bug 1245571: Add a window.navigator.mozAddonManager object that can only be accessed by AMO. r=bz
authorDave Townsend <dtownsend@oxymoronical.com>
Fri, 19 Feb 2016 16:49:30 -0800
changeset 330926 a01d14eb9caf57b73ede38fcfe3620e42f9b32db
parent 330925 6f51002d4589638e0120681f8de5512c48bb5155
child 330927 64cba8d398a38470ece1fe50be8210fff82fc272
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs1245571
milestone48.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 1245571: Add a window.navigator.mozAddonManager object that can only be accessed by AMO. r=bz Since we want to add a few APIs that AMO can use to query and manipulate the user's add-ons we want to expose a custom API. This implements the webidl and a stub implementation in JavaScript. We use the webidl functions for controlling access to the API, only the AMO production domains (and some test domains when testing is enabled) can access it and only when retrieved securely and not in an inner frame of a page that shouldn't have the API. MozReview-Commit-ID: 3HUUrduuHwf
dom/webidl/AddonManager.webidl
dom/webidl/moz.build
toolkit/mozapps/extensions/AddonManagerWebAPI.cpp
toolkit/mozapps/extensions/AddonManagerWebAPI.h
toolkit/mozapps/extensions/amWebAPI.js
toolkit/mozapps/extensions/extensions.manifest
toolkit/mozapps/extensions/moz.build
toolkit/mozapps/extensions/test/browser/browser.ini
toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
toolkit/mozapps/extensions/test/browser/head.js
toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xul
toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html
new file mode 100644
--- /dev/null
+++ b/dom/webidl/AddonManager.webidl
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* We need a JSImplementation but cannot get one without a contract ID. Since
+   This object is only ever created from JS we don't need a real contract ID. */
+[ChromeOnly, JSImplementation="dummy"]
+interface Addon {
+  // The add-on's ID.
+  readonly attribute DOMString id;
+  // The add-on's version.
+  readonly attribute DOMString version;
+  // The add-on's type (extension, theme, etc.).
+  readonly attribute DOMString type;
+  // The add-on's name in the current locale.
+  readonly attribute DOMString name;
+  // The add-on's description in the current locale.
+  readonly attribute DOMString description;
+  // If the user has enabled this add-on, note that it still may not be running
+  // depending on whether enabling requires a restart or if the add-on is
+  // incompatible in some way.
+  readonly attribute boolean isEnabled;
+  // If the add-on is currently active in the browser.
+  readonly attribute boolean isActive;
+};
+
+[HeaderFile="mozilla/AddonManagerWebAPI.h",
+ Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
+ NavigatorProperty="mozAddonManager",
+ JSImplementation="@mozilla.org/addon-web-api/manager;1"]
+interface AddonManager {
+  /**
+   * Gets information about an add-on
+   *
+   * @param  id
+   *         The ID of the add-on to test for.
+   * @return A promise. It will resolve to an Addon if the add-on is installed.
+   */
+  Promise<Addon> getAddonByID(DOMString id);
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -15,16 +15,17 @@ PREPROCESSED_WEBIDL_FILES = [
     'PromiseDebugging.webidl',
     'ServiceWorkerRegistration.webidl',
     'Window.webidl',
 ]
 
 WEBIDL_FILES = [
     'AbstractWorker.webidl',
     'ActivityRequestHandler.webidl',
+    'AddonManager.webidl',
     'AnalyserNode.webidl',
     'Animatable.webidl',
     'Animation.webidl',
     'AnimationEffectReadOnly.webidl',
     'AnimationEffectTiming.webidl',
     'AnimationEffectTimingReadOnly.webidl',
     'AnimationEvent.webidl',
     'AnimationTimeline.webidl',
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.cpp
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AddonManagerWebAPI.h"
+
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/NavigatorBinding.h"
+
+#include "mozilla/Preferences.h"
+#include "nsGlobalWindow.h"
+
+#include "nsIDocShell.h"
+#include "nsIScriptObjectPrincipal.h"
+
+namespace mozilla {
+using namespace mozilla::dom;
+
+// Checks if the given uri is secure and matches one of the hosts allowed to
+// access the API.
+bool
+AddonManagerWebAPI::IsValidSite(nsIURI* uri)
+{
+  if (!uri) {
+    return false;
+  }
+
+  bool isSecure;
+  nsresult rv = uri->SchemeIs("https", &isSecure);
+  if (NS_FAILED(rv) || !isSecure) {
+    return false;
+  }
+
+  nsCString host;
+  rv = uri->GetHost(host);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  if (host.Equals("addons.mozilla.org") ||
+      host.Equals("services.addons.mozilla.org")) {
+    return true;
+  }
+
+  // When testing allow access to the developer sites.
+  if (Preferences::GetBool("extensions.webapi.testing", false)) {
+    if (host.Equals("addons.allizom.org") ||
+        host.Equals("services.addons.allizom.org") ||
+        host.Equals("addons-dev.allizom.org") ||
+        host.Equals("services.addons-dev.allizom.org") ||
+        host.Equals("example.com")) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+bool
+AddonManagerWebAPI::IsAPIEnabled(JSContext* cx, JSObject* obj)
+{
+  nsGlobalWindow* global = xpc::WindowGlobalOrNull(obj);
+  if (!global) {
+    return false;
+  }
+
+  nsCOMPtr<nsPIDOMWindowInner> win = global->AsInner();
+  if (!win) {
+    return false;
+  }
+
+  // Check that the current window and all parent frames are allowed access to
+  // the API.
+  while (win) {
+    nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(win);
+    if (!sop) {
+      return false;
+    }
+
+    nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
+    if (!principal) {
+      return false;
+    }
+
+    // Reaching a window with a system principal means we have reached
+    // privileged UI of some kind so stop at this point and allow access.
+    if (principal->GetIsSystemPrincipal()) {
+      return true;
+    }
+
+    nsCOMPtr<nsIDocShell> docShell = win->GetDocShell();
+    if (!docShell) {
+      // This window has been torn down so don't allow access to the API.
+      return false;
+    }
+
+    if (!IsValidSite(win->GetDocumentURI())) {
+      return false;
+    }
+
+    // Checks whether there is a parent frame of the same type. This won't cross
+    // mozbrowser or chrome boundaries.
+    nsCOMPtr<nsIDocShellTreeItem> parent;
+    nsresult rv = docShell->GetSameTypeParent(getter_AddRefs(parent));
+    if (NS_FAILED(rv)) {
+      return false;
+    }
+
+    if (!parent) {
+      // No parent means we've hit a mozbrowser or chrome boundary so allow
+      // access to the API.
+      return true;
+    }
+
+    nsIDocument* doc = win->GetDoc();
+    if (!doc) {
+      return false;
+    }
+
+    doc = doc->GetParentDocument();
+    if (!doc) {
+      // Getting here means something has been torn down so fail safe.
+      return false;
+    }
+
+
+    win = doc->GetInnerWindow();
+  }
+
+  // Found a document with no inner window, don't grant access to the API.
+  return false;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsPIDOMWindow.h"
+
+namespace mozilla {
+
+class AddonManagerWebAPI {
+public:
+  static bool IsAPIEnabled(JSContext* cx, JSObject* obj);
+
+private:
+  static bool IsValidSite(nsIURI* uri);
+};
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/amWebAPI.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+function WebAPI() {
+}
+
+WebAPI.prototype = {
+  init(window) {
+    this.window = window;
+  },
+
+  getAddonByID(id) {
+    return this.window.Promise.reject("Not yet implemented");
+  },
+
+  classID: Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"),
+  contractID: "@mozilla.org/addon-web-api/manager;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebAPI]);
--- a/toolkit/mozapps/extensions/extensions.manifest
+++ b/toolkit/mozapps/extensions/extensions.manifest
@@ -18,8 +18,10 @@ contract @mozilla.org/addons/web-install
 component {9df8ef2b-94da-45c9-ab9f-132eb55fddf1} amInstallTrigger.js
 contract @mozilla.org/addons/installtrigger;1 {9df8ef2b-94da-45c9-ab9f-132eb55fddf1}
 category JavaScript-global-property InstallTrigger @mozilla.org/addons/installtrigger;1
 #ifndef MOZ_WIDGET_ANDROID
 category addon-provider-module PluginProvider resource://gre/modules/addons/PluginProvider.jsm
 #endif
 category addon-provider-module GMPProvider resource://gre/modules/addons/GMPProvider.jsm
 #endif
+component {8866d8e3-4ea5-48b7-a891-13ba0ac15235} amWebAPI.js
+contract @mozilla.org/addon-web-api/manager;1 {8866d8e3-4ea5-48b7-a891-13ba0ac15235}
--- a/toolkit/mozapps/extensions/moz.build
+++ b/toolkit/mozapps/extensions/moz.build
@@ -18,16 +18,17 @@ XPIDL_SOURCES += [
 ]
 
 XPIDL_MODULE = 'extensions'
 
 EXTRA_COMPONENTS += [
     'addonManager.js',
     'amContentHandler.js',
     'amInstallTrigger.js',
+    'amWebAPI.js',
     'amWebInstallListener.js',
     'nsBlocklistService.js',
     'nsBlocklistServiceContent.js',
 ]
 
 EXTRA_PP_COMPONENTS += [
     'extensions.manifest',
 ]
@@ -38,20 +39,26 @@ EXTRA_JS_MODULES += [
     'DeferredSave.jsm',
     'LightweightThemeManager.jsm',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXPORTS.mozilla += [
     'AddonContentPolicy.h',
+    'AddonManagerWebAPI.h',
     'AddonPathService.h',
 ]
 
 UNIFIED_SOURCES += [
     'AddonContentPolicy.cpp',
+    'AddonManagerWebAPI.cpp',
     'AddonPathService.cpp',
 ]
 
+LOCAL_INCLUDES += [
+    '/dom/base',
+]
+
 FINAL_LIBRARY = 'xul'
 
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'Add-ons Manager')
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -32,16 +32,20 @@ support-files =
   browser_updatessl.rdf
   browser_updatessl.rdf^headers^
   browser_install.rdf
   browser_install.rdf^headers^
   browser_install.xml
   browser_install1_3.xpi
   browser_eula.xml
   browser_purchase.xml
+  webapi_checkavailable.html
+  webapi_checkchromeframe.xul
+  webapi_checkframed.html
+  webapi_checknavigatedwindow.html
   !/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
   !/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
 
@@ -55,10 +59,11 @@ support-files =
 # Verifies the old style of signing hotfixes
 skip-if = require_signing
 [browser_installssl.js]
 [browser_newaddon.js]
 [browser_updatessl.js]
 [browser_task_next_test.js]
 [browser_discovery_install.js]
 [browser_update.js]
+[browser_webapi_access.js]
 
 [include:browser-common.ini]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+function check_frame_availability(browser) {
+  return ContentTask.spawn(browser, null, function*() {
+    let frame = content.document.getElementById("frame");
+    return frame.contentWindow.document.getElementById("result").textContent == "true";
+  });
+}
+
+function check_availability(browser) {
+  return ContentTask.spawn(browser, null, function*() {
+    return content.document.getElementById("result").textContent == "true";
+  });
+}
+
+// Test that initially the API isn't available in the test domain
+add_task(function* test_not_available() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkavailable.html`,
+    function* test_not_available(browser) {
+      let available = yield check_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that with testing on the API is available in the test domain
+add_task(function* test_available() {
+  Services.prefs.setBoolPref("extensions.webapi.testing", true);
+
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkavailable.html`,
+    function* test_not_available(browser) {
+      let available = yield check_availability(browser);
+      ok(available, "API should be available.");
+    })
+});
+
+// Test that the API is not available in a bad domain
+add_task(function* test_bad_domain() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checkavailable.html`,
+    function* test_not_available(browser) {
+      let available = yield check_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that the API is only available in https sites
+add_task(function* test_not_available_http() {
+  yield BrowserTestUtils.withNewTab(`${TESTROOT}webapi_checkavailable.html`,
+    function* test_not_available(browser) {
+      let available = yield check_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that the API is available when in a frame of the test domain
+add_task(function* test_available_framed() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkframed.html`,
+    function* test_available(browser) {
+      let available = yield check_frame_availability(browser);
+      ok(available, "API should be available.");
+    })
+});
+
+// Test that if the external frame is http then the inner frame doesn't have
+// the API
+add_task(function* test_not_available_http_framed() {
+  yield BrowserTestUtils.withNewTab(`${TESTROOT}webapi_checkframed.html`,
+    function* test_not_available(browser) {
+      let available = yield check_frame_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that if the external frame is a bad domain then the inner frame doesn't
+// have the API
+add_task(function* test_not_available_framed() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checkframed.html`,
+    function* test_not_available(browser) {
+      let available = yield check_frame_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that a window navigated to a bad domain doesn't allow access to the API
+add_task(function* test_navigated_window() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checknavigatedwindow.html`,
+    function* test_available(browser) {
+      let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+      yield ContentTask.spawn(browser, null, function*() {
+        yield content.wrappedJSObject.openWindow();
+      });
+
+      // Should be a new tab open
+      let tab = yield tabPromise;
+      let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab));
+
+      ContentTask.spawn(browser, null, function*() {
+        content.wrappedJSObject.navigate();
+      });
+
+      yield loadPromise;
+
+      let available = yield ContentTask.spawn(browser, null, function*() {
+        return content.wrappedJSObject.check();
+      });
+
+      ok(!available, "API should not be available.");
+
+      gBrowser.removeTab(tab);
+    })
+});
+
+// Check that if a page is embedded in a chrome content UI that it can still
+// access the API.
+add_task(function* test_chrome_frame() {
+  yield BrowserTestUtils.withNewTab(`${CHROMEROOT}webapi_checkchromeframe.xul`,
+    function* test_available(browser) {
+      let available = yield check_frame_availability(browser);
+      ok(available, "API should be available.");
+    })
+});
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -21,17 +21,19 @@ var gTestInWindow = /-window$/.test(path
 // Drop the UI type
 if (gTestInWindow) {
   pathParts.splice(pathParts.length - 1, pathParts.length);
 }
 
 const RELATIVE_DIR = pathParts.slice(4).join("/") + "/";
 
 const TESTROOT = "http://example.com/" + RELATIVE_DIR;
+const SECURE_TESTROOT = "https://example.com/" + RELATIVE_DIR;
 const TESTROOT2 = "http://example.org/" + RELATIVE_DIR;
+const SECURE_TESTROOT2 = "https://example.org/" + RELATIVE_DIR;
 const CHROMEROOT = pathParts.join("/") + "/";
 const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
 const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
 const PREF_XPI_ENABLED = "xpinstall.enabled";
 const PREF_UPDATEURL = "extensions.update.url";
 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
 const PREF_CUSTOM_XPINSTALL_CONFIRMATION_UI = "xpinstall.customConfirmationUI";
 const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p id="result"></p>
+<script type="text/javascript">
+document.getElementById("result").textContent = ("mozAddonManager" in window.navigator);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xul
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <browser id="frame" disablehistory="true" flex="1" type="content"
+           src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html"/>
+</window>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<iframe id="frame" height="200" width="200" src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html">
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<script type="text/javascript">
+var nav, win;
+
+function openWindow() {
+  return new Promise(resolve => {
+    win = window.open(window.location);
+
+    win.addEventListener("load", function listener() {
+      nav = win.navigator;
+      resolve();
+    }, false);
+  });
+}
+
+function navigate() {
+  win.location = "http://example.com/";
+}
+
+function check() {
+  return "mozAddonManager" in nav;
+}
+</script>
+</body>
+</html>