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
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
@@ -31,16 +31,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
@@ -53,10 +57,11 @@ support-files =
[browser_hotfix.js]
# 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_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>