Bug 1208874 - [webext] Generate an extension-specific UUID (r=kmag)
authorBill McCloskey <billm@mozilla.com>
Tue, 10 Nov 2015 14:24:34 -0800
changeset 308652 9e61ae324d135478280c09b5f493e9d375109334
parent 308651 4371f650013131bdbd0efded140e64e8fb5aa9c4
child 308653 f3ab1f50450af2a5380be725373ba4672326f401
push id7513
push useratolfsen@mozilla.com
push dateFri, 13 Nov 2015 14:03:43 +0000
reviewerskmag
bugs1208874
milestone45.0a1
Bug 1208874 - [webext] Generate an extension-specific UUID (r=kmag)
testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
testing/specialpowers/content/specialpowersAPI.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/test/mochitest/mochitest.ini
toolkit/components/extensions/test/mochitest/test_ext_localStorage.html
--- a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
+++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
@@ -1,11 +1,11 @@
 var ExtensionTestUtils = {};
 
-ExtensionTestUtils.loadExtension = function(ext)
+ExtensionTestUtils.loadExtension = function(ext, id = null)
 {
   var testResolve;
   var testDone = new Promise(resolve => { testResolve = resolve; });
 
   var messageHandler = new Map();
 
   function testHandler(kind, pass, msg, ...args) {
     if (kind == "test-eq") {
@@ -32,17 +32,17 @@ ExtensionTestUtils.loadExtension = funct
       if (!handler) {
         return;
       }
 
       handler(...args);
     },
   };
 
-  var extension = SpecialPowers.loadExtension(ext, handler);
+  var extension = SpecialPowers.loadExtension(id, ext, handler);
 
   extension.awaitMessage = (msg) => {
     return new Promise(resolve => {
       if (messageHandler.has(msg)) {
         throw new Error("only one message handler allowed");
       }
 
       messageHandler.set(msg, (...args) => {
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -1998,19 +1998,21 @@ SpecialPowersAPI.prototype = {
   removeServiceWorkerDataForExampleDomain: function() {
     this.notifyObserversInParentProcess(null, "browser:purge-domain-data", "example.com");
   },
 
   cleanUpSTSData: function(origin, flags) {
     return this._sendSyncMessage('SPCleanUpSTSData', {origin: origin, flags: flags || 0});
   },
 
-  loadExtension: function(ext, handler) {
-    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-    let id = uuidGenerator.generateUUID().number;
+  loadExtension: function(id, ext, handler) {
+    if (!id) {
+      let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+      id = uuidGenerator.generateUUID().number;
+    }
 
     let resolveStartup, resolveUnload, rejectStartup;
     let startupPromise = new Promise((resolve, reject) => {
       resolveStartup = resolve;
       rejectStartup = reject;
     });
     let unloadPromise = new Promise(resolve => { resolveUnload = resolve; });
 
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -32,16 +32,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 Cu.import("resource://gre/modules/ExtensionManagement.jsm");
 
 // Register built-in parts of the API. Other parts may be registered
 // in browser/, mobile/, or b2g/.
 ExtensionManagement.registerScript("chrome://extensions/content/ext-alarms.js");
@@ -644,36 +646,64 @@ ExtensionData.prototype = {
 
     let localeData = yield this.readLocaleFile(locale);
 
     this.selectedLocale = locale;
     return localeData;
   }),
 };
 
+// All moz-extension URIs use a machine-specific UUID rather than the
+// extension's own ID in the host component. This makes it more
+// difficult for web pages to detect whether a user has a given add-on
+// installed (by trying to load a moz-extension URI referring to a
+// web_accessible_resource from the extension). getExtensionUUID
+// returns the UUID for a given add-on ID.
+function getExtensionUUID(id)
+{
+  const PREF_NAME = "extensions.webextensions.uuids";
+
+  let pref = Preferences.get(PREF_NAME, "{}");
+  let map = {};
+  try {
+    map = JSON.parse(pref);
+  } catch (e) {
+    Cu.reportError(`Error parsing ${PREF_NAME}.`);
+  }
+
+  if (id in map) {
+    return map[id];
+  }
+
+  let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+  let uuid = uuidGenerator.generateUUID().number;
+  uuid = uuid.slice(1, -1); // Strip of { and } off the UUID.
+
+  map[id] = uuid;
+  Preferences.set(PREF_NAME, JSON.stringify(map));
+  return uuid;
+}
+
 // We create one instance of this class per extension. |addonData|
 // comes directly from bootstrap.js when initializing.
 this.Extension = function(addonData)
 {
   ExtensionData.call(this, addonData.resourceURI);
 
-  let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-  let uuid = uuidGenerator.generateUUID().number;
-  uuid = uuid.slice(1, -1); // Strip of { and } off the UUID.
-  this.uuid = uuid;
+  this.uuid = getExtensionUUID(addonData.id);
 
   if (addonData.cleanupFile) {
     Services.obs.addObserver(this, "xpcom-shutdown", false);
     this.cleanupFile = addonData.cleanupFile || null;
     delete addonData.cleanupFile;
   }
 
   this.addonData = addonData;
   this.id = addonData.id;
-  this.baseURI = Services.io.newURI("moz-extension://" + uuid, null, null);
+  this.baseURI = Services.io.newURI("moz-extension://" + this.uuid, null, null);
   this.baseURI.QueryInterface(Ci.nsIURL);
   this.principal = this.createPrincipal();
 
   this.views = new Set();
 
   this.onStartup = null;
 
   this.hasShutdown = false;
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -16,16 +16,17 @@ support-files =
   file_script_xhr.js
   file_sample.html
 
 [test_ext_simple.html]
 [test_ext_geturl.html]
 [test_ext_contentscript.html]
 [test_ext_webrequest.html]
 [test_ext_generate.html]
+[test_ext_localStorage.html]
 [test_ext_notifications.html]
 [test_ext_runtime_connect.html]
 [test_ext_runtime_disconnect.html]
 [test_ext_runtime_getPlatformInfo.html]
 [test_ext_sandbox_var.html]
 [test_ext_sendmessage_reply.html]
 [test_ext_sendmessage_doublereply.html]
 [test_ext_storage.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_localStorage.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>WebExtension test</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="application/javascript;version=1.8">
+"use strict";
+
+function backgroundScript() {
+  var hasRun = localStorage.getItem("has-run");
+  var result;
+  if (!hasRun) {
+    localStorage.setItem("has-run", "yup");
+    localStorage.setItem("test-item", "item1");
+    result = "item1";
+  } else {
+    var data = localStorage.getItem("test-item");
+    if (data == "item1") {
+      localStorage.setItem("test-item", "item2");
+      result = "item2";
+    } else if (data == "item2") {
+      localStorage.removeItem("test-item");
+      result = "deleted";
+    } else if (!data) {
+      localStorage.clear();
+      result = "cleared";
+    }
+  }
+  browser.test.sendMessage("result", result);
+  browser.test.notifyPass("localStorage");
+}
+
+let extensionData = {
+  background: "(" + backgroundScript.toString() + ")()",
+};
+
+add_task(function* test_contentscript() {
+  let id = "test-webextension@mozilla.com";
+  const RESULTS = ["item1", "item2", "deleted", "cleared", "item1"];
+
+  for (let expected of RESULTS) {
+    let extension = ExtensionTestUtils.loadExtension(extensionData, id);
+    let [, actual] = yield Promise.all([extension.startup(), extension.awaitMessage("result")]);
+    yield extension.awaitFinish("localStorage");
+    yield extension.unload();
+
+    is(actual, expected, "got expected localStorage data");
+  }
+});
+</script>
+
+</body>
+</html>