Bug 1550605 add networkStatus api r=rpl,mayhemer a=RyanVM
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 18 Jul 2019 17:37:53 +0000
changeset 544879 5db1324ed737a73c76f7f6c620c6b0f87f4a411d
parent 544878 3e3f6ab6901ca09877ab2c982ce0d2d5bdaba8ca
child 544880 5aa01b3fc6f442e420192a2f86606abb657bdfd3
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrpl, mayhemer, RyanVM
bugs1550605
milestone69.0
Bug 1550605 add networkStatus api r=rpl,mayhemer a=RyanVM Differential Revision: https://phabricator.services.mozilla.com/D30572
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ext-toolkit.json
toolkit/components/extensions/jar.mn
toolkit/components/extensions/parent/ext-networkStatus.js
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/network_status.json
toolkit/components/extensions/test/xpcshell/test_ext_networkStatus.js
toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -141,16 +141,17 @@ const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
 
 // Permissions that are only available to privileged extensions.
 const PRIVILEGED_PERMS = new Set([
   "mozillaAddons",
   "geckoViewAddons",
   "telemetry",
   "urlbar",
   "normandyAddonStudy",
+  "networkStatus",
 ]);
 
 /**
  * Classify an individual permission from a webextension manifest
  * as a host/origin permission, an api permission, or a regular permission.
  *
  * @param {string} perm  The permission string to classify
  * @param {boolean} restrictSchemes
--- a/toolkit/components/extensions/ext-toolkit.json
+++ b/toolkit/components/extensions/ext-toolkit.json
@@ -101,16 +101,24 @@
   "management": {
     "url": "chrome://extensions/content/parent/ext-management.js",
     "schema": "chrome://extensions/content/schemas/management.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["management"]
     ]
   },
+  "networkStatus": {
+    "url": "chrome://extensions/content/parent/ext-networkStatus.js",
+    "schema": "chrome://extensions/content/schemas/network_status.json",
+    "scopes": ["addon_parent"],
+    "paths": [
+      ["networkStatus"]
+    ]
+  },
   "notifications": {
     "url": "chrome://extensions/content/parent/ext-notifications.js",
     "schema": "chrome://extensions/content/schemas/notifications.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["notifications"]
     ]
   },
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -24,16 +24,17 @@ toolkit.jar:
     content/extensions/parent/ext-geckoProfiler.js (parent/ext-geckoProfiler.js)
 #endif
     content/extensions/parent/ext-i18n.js (parent/ext-i18n.js)
 #ifndef ANDROID
     content/extensions/parent/ext-identity.js (parent/ext-identity.js)
 #endif
     content/extensions/parent/ext-idle.js (parent/ext-idle.js)
     content/extensions/parent/ext-management.js (parent/ext-management.js)
+    content/extensions/parent/ext-networkStatus.js (parent/ext-networkStatus.js)
     content/extensions/parent/ext-notifications.js (parent/ext-notifications.js)
     content/extensions/parent/ext-permissions.js (parent/ext-permissions.js)
     content/extensions/parent/ext-privacy.js (parent/ext-privacy.js)
     content/extensions/parent/ext-protocolHandlers.js (parent/ext-protocolHandlers.js)
     content/extensions/parent/ext-proxy.js (parent/ext-proxy.js)
     content/extensions/parent/ext-runtime.js (parent/ext-runtime.js)
     content/extensions/parent/ext-storage.js (parent/ext-storage.js)
     content/extensions/parent/ext-tabs-base.js (parent/ext-tabs-base.js)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-networkStatus.js
@@ -0,0 +1,89 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et 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/. */
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "gNetworkLinkService",
+  "@mozilla.org/network/network-link-service;1",
+  "nsINetworkLinkService"
+);
+
+function getLinkType() {
+  switch (gNetworkLinkService.linkType) {
+    case gNetworkLinkService.LINK_TYPE_UNKNOWN:
+      return "unknown";
+    case gNetworkLinkService.LINK_TYPE_ETHERNET:
+      return "ethernet";
+    case gNetworkLinkService.LINK_TYPE_USB:
+      return "usb";
+    case gNetworkLinkService.LINK_TYPE_WIFI:
+      return "wifi";
+    case gNetworkLinkService.LINK_TYPE_WIMAX:
+      return "wimax";
+    case gNetworkLinkService.LINK_TYPE_2G:
+      return "2g";
+    case gNetworkLinkService.LINK_TYPE_3G:
+      return "3g";
+    case gNetworkLinkService.LINK_TYPE_4G:
+      return "4g";
+    default:
+      return "unknown";
+  }
+}
+
+function getLinkStatus() {
+  if (!gNetworkLinkService.linkStatusKnown) {
+    return "unknown";
+  }
+  return gNetworkLinkService.isLinkUp ? "up" : "down";
+}
+
+function getLinkInfo() {
+  return {
+    id: gNetworkLinkService.networkID || undefined,
+    status: getLinkStatus(),
+    type: getLinkType(),
+  };
+}
+
+this.networkStatus = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      networkStatus: {
+        getLinkInfo,
+        onConnectionChanged: new EventManager({
+          context,
+          name: "networkStatus.onConnectionChanged",
+          register: fire => {
+            let observerStatus = (subject, topic, data) => {
+              fire.async(getLinkInfo());
+            };
+
+            Services.obs.addObserver(
+              observerStatus,
+              "network:link-status-changed"
+            );
+            Services.obs.addObserver(
+              observerStatus,
+              "network:link-type-changed"
+            );
+            return () => {
+              Services.obs.removeObserver(
+                observerStatus,
+                "network:link-status-changed"
+              );
+              Services.obs.removeObserver(
+                observerStatus,
+                "network:link-type-changed"
+              );
+            };
+          },
+        }).api(),
+      },
+    };
+  }
+};
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -26,16 +26,17 @@ toolkit.jar:
     content/extensions/schemas/i18n.json
 #ifndef ANDROID
     content/extensions/schemas/identity.json
 #endif
     content/extensions/schemas/idle.json
     content/extensions/schemas/management.json
     content/extensions/schemas/manifest.json
     content/extensions/schemas/native_manifest.json
+    content/extensions/schemas/network_status.json
     content/extensions/schemas/notifications.json
     content/extensions/schemas/permissions.json
     content/extensions/schemas/proxy.json
     content/extensions/schemas/privacy.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/storage.json
     content/extensions/schemas/telemetry.json
     content/extensions/schemas/test.json
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/network_status.json
@@ -0,0 +1,66 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "networkStatus"
+          ]
+        }]
+      }
+    ]
+  },
+  {
+    "namespace": "networkStatus",
+    "description": "This API provides the ability to determine the status of and detect changes in the network connection. This API can only be used in privileged extensions.",
+    "permissions": ["networkStatus"],
+    "types": [
+      {
+        "id": "NetworkLinkInfo",
+        "type": "object",
+        "properties": {
+          "status": {
+            "type": "string",
+            "enum": ["unknown", "up", "down"],
+            "description": "Status of the network link, if \"unknown\" then link is usually assumed to be \"up\""
+          },
+          "type": {
+            "type": "string",
+            "enum": ["unknown", "ethernet", "usb", "wifi", "wimax", "2g", "3g", "4g"],
+            "description": "If known, the type of network connection that is avialable."
+          },
+          "id": {
+            "type": "string",
+            "optional": true,
+            "description": "If known, the network id or name."
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "getLinkInfo",
+        "type": "function",
+        "description": "Returns the $(ref:NetworkLinkInfo} of the current network connection.",
+        "async": true,
+        "parameters": []
+      }
+    ],
+    "events": [
+      {
+        "name": "onConnectionChanged",
+        "type": "function",
+        "description": "Fired when the network connection state changes.",
+        "parameters": [
+          {
+            "name": "details",
+            "$ref": "NetworkLinkInfo"
+          }
+        ]
+      }
+    ]
+  }
+]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_networkStatus.js
@@ -0,0 +1,192 @@
+"use strict";
+
+PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
+
+const Cm = Components.manager;
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
+  Ci.nsIUUIDGenerator
+);
+
+var mockNetworkStatusService = {
+  contractId: "@mozilla.org/network/network-link-service;1",
+
+  _mockClassId: uuidGenerator.generateUUID(),
+
+  _originalClassId: "",
+
+  QueryInterface: ChromeUtils.generateQI([Ci.nsINetworkLinkService]),
+
+  createInstance(outer, iiD) {
+    if (outer) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(iiD);
+  },
+
+  register() {
+    let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+    if (!registrar.isCIDRegistered(this._mockClassId)) {
+      this._originalClassId = registrar.contractIDToCID(this.contractId);
+      registrar.registerFactory(
+        this._mockClassId,
+        "Unregister after testing",
+        this.contractId,
+        this
+      );
+    }
+  },
+
+  unregister() {
+    let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+    registrar.unregisterFactory(this._mockClassId, this);
+    registrar.registerFactory(this._originalClassId, "", this.contractId, null);
+  },
+
+  _isLinkUp: true,
+  _linkStatusKnown: false,
+  _linkType: Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN,
+
+  get isLinkUp() {
+    return this._isLinkUp;
+  },
+
+  get linkStatusKnown() {
+    return this._linkStatusKnown;
+  },
+
+  setLinkStatus(status) {
+    switch (status) {
+      case "up":
+        this._isLinkUp = true;
+        this._linkStatusKnown = true;
+        this._networkID = "foo";
+        break;
+      case "down":
+        this._isLinkUp = false;
+        this._linkStatusKnown = true;
+        this._linkType = Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN;
+        this._networkID = undefined;
+        break;
+      case "changed":
+        this._linkStatusKnown = true;
+        this._networkID = "foo";
+        break;
+      case "unknown":
+        this._linkStatusKnown = false;
+        this._linkType = Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN;
+        this._networkID = undefined;
+        break;
+    }
+    Services.obs.notifyObservers(null, "network:link-status-changed", status);
+  },
+
+  get linkType() {
+    return this._linkType;
+  },
+
+  setLinkType(val) {
+    this._linkType = val;
+    this._linkStatusKnown = true;
+    this._isLinkUp = true;
+    this._networkID = "bar";
+    Services.obs.notifyObservers(
+      null,
+      "network:link-type-changed",
+      this._linkType
+    );
+  },
+
+  get networkID() {
+    return this._networkID;
+  },
+};
+
+// nsINetworkLinkService is not directly testable. With the mock service above,
+// we just exercise a couple small things here to validate the api works somewhat.
+add_task(async function test_networkStatus() {
+  mockNetworkStatusService.register();
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: { gecko: { id: "networkstatus@tests.mozilla.org" } },
+      permissions: ["networkStatus"],
+    },
+    isPrivileged: true,
+    async background() {
+      browser.networkStatus.onConnectionChanged.addListener(async details => {
+        browser.test.log(`connection status ${JSON.stringify(details)}`);
+        browser.test.sendMessage("connect-changed", {
+          details,
+          linkInfo: await browser.networkStatus.getLinkInfo(),
+        });
+      });
+      browser.test.sendMessage(
+        "linkdata",
+        await browser.networkStatus.getLinkInfo()
+      );
+    },
+  });
+
+  async function test(expected, change) {
+    if (change.status) {
+      info(`test link change status to ${change.status}`);
+      mockNetworkStatusService.setLinkStatus(change.status);
+    } else if (change.link) {
+      info(`test link change type to ${change.link}`);
+      mockNetworkStatusService.setLinkType(change.link);
+    }
+    let { details, linkInfo } = await extension.awaitMessage("connect-changed");
+    equal(details.type, expected.type, "network type is correct");
+    equal(details.status, expected.status, `network status is correct`);
+    equal(details.id, expected.id, "network id");
+    Assert.deepEqual(
+      linkInfo,
+      details,
+      "getLinkInfo should resolve to the same details received from onConnectionChanged"
+    );
+  }
+
+  await extension.startup();
+
+  let data = await extension.awaitMessage("linkdata");
+  equal(data.type, "unknown", "network type is unknown");
+  equal(data.status, "unknown", `network status is ${data.status}`);
+  equal(data.id, undefined, "network id");
+
+  await test(
+    { type: "unknown", status: "up", id: "foo" },
+    { status: "changed" }
+  );
+
+  await test(
+    { type: "wifi", status: "up", id: "bar" },
+    { link: Ci.nsINetworkLinkService.LINK_TYPE_WIFI }
+  );
+
+  await test({ type: "unknown", status: "down" }, { status: "down" });
+
+  await test({ type: "unknown", status: "unknown" }, { status: "unknown" });
+
+  await extension.unload();
+  mockNetworkStatusService.unregister();
+});
+
+add_task(async function test_networkStatus_permission() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: {
+        gecko: { id: "networkstatus-permission@tests.mozilla.org" },
+      },
+      permissions: ["networkStatus"],
+    },
+    async background() {
+      browser.test.assertEq(
+        undefined,
+        browser.networkStatus,
+        "networkStatus is privileged"
+      );
+    },
+  });
+  await extension.startup();
+  await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -567,16 +567,17 @@ const GRANTED_WITHOUT_USER_PROMPT = [
   "contextualIdentities",
   "cookies",
   "geckoProfiler",
   "identity",
   "idle",
   "menus",
   "menus.overrideContext",
   "mozillaAddons",
+  "networkStatus",
   "normandyAddonStudy",
   "search",
   "storage",
   "telemetry",
   "theme",
   "urlbar",
   "webRequest",
   "webRequestBlocking",
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -72,16 +72,17 @@ skip-if = os == "android" # Not shipped 
 [test_ext_idle.js]
 [test_ext_incognito.js]
 [test_ext_localStorage.js]
 [test_ext_management.js]
 skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
 [test_ext_management_uninstall_self.js]
 [test_ext_messaging_startup.js]
 skip-if = appname == "thunderbird" || (os == "android" && debug)
+[test_ext_networkStatus.js]
 [test_ext_onmessage_removelistener.js]
 skip-if = true # This test no longer tests what it is meant to test.
 [test_ext_permission_xhr.js]
 [test_ext_persistent_events.js]
 [test_ext_privacy.js]
 skip-if = appname == "thunderbird" || (os == "android" && debug)
 [test_ext_privacy_disable.js]
 skip-if = appname == "thunderbird"