Bug 1208874 - [webext] Generate an extension-specific UUID (r=kmag)
--- 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>