Bug 1213990 Clear storage when webextension is uninstalled r?kmag
MozReview-Commit-ID: BeMOxOCSeru
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4634,16 +4634,20 @@ pref("browser.meta_refresh_when_inactive
// XPInstall prefs
pref("xpinstall.whitelist.required", true);
// Only Firefox requires add-on signatures
pref("xpinstall.signatures.required", false);
pref("extensions.alwaysUnpack", false);
pref("extensions.minCompatiblePlatformVersion", "2.0");
pref("extensions.webExtensionsMinPlatformVersion", "42.0a1");
+// Other webextensions prefs
+pref("extensions.webextensions.keepStorageOnUninstall", false);
+pref("extensions.webextensions.keepUuidOnUninstall", false);
+
pref("network.buffer.cache.count", 24);
pref("network.buffer.cache.size", 32768);
// Desktop Notification
pref("notification.feature.enabled", false);
// Web Notification
pref("dom.webnotifications.enabled", true);
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -27,16 +27,18 @@ Cu.import("resource://gre/modules/XPCOMU
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
"resource://gre/modules/ExtensionAPI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
+ "resource://gre/modules/ExtensionStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
"resource://gre/modules/MatchPattern.jsm");
@@ -84,16 +86,18 @@ var {
Messenger,
injectAPI,
instanceOf,
flushJarCache,
} = ExtensionUtils;
const LOGGER_ID_BASE = "addons.webextension.";
const UUID_MAP_PREF = "extensions.webextensions.uuids";
+const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
+const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
const COMMENT_REGEXP = new RegExp(String.raw`
^
(
(?:
[^"] |
" (?:[^"\\] | \\.)* "
)*?
@@ -530,34 +534,59 @@ function getExtensionUUID(id) {
return UUIDMap.get(id, true);
}
// For extensions that have called setUninstallURL(), send an event
// so the browser can display the URL.
var UninstallObserver = {
initialized: false,
- init: function() {
+ init() {
if (!this.initialized) {
AddonManager.addAddonListener(this);
+ XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false);
+ XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false);
this.initialized = true;
}
},
- uninit: function() {
- if (this.initialized) {
- AddonManager.removeAddonListener(this);
- this.initialized = false;
+ onUninstalling(addon) {
+ let extension = GlobalManager.extensionMap.get(addon.id);
+ if (extension) {
+ // Let any other interested listeners respond
+ // (e.g., display the uninstall URL)
+ Management.emit("uninstall", extension);
}
},
- onUninstalling: function(addon) {
- let extension = GlobalManager.extensionMap.get(addon.id);
- if (extension) {
- Management.emit("uninstall", extension);
+ onUninstalled(addon) {
+ let uuid = UUIDMap.get(addon.id, false);
+ if (!uuid) {
+ return;
+ }
+
+ if (!this.leaveStorage) {
+ // Clear browser.local.storage
+ ExtensionStorage.clear(addon.id);
+
+ // Clear any IndexedDB storage created by the extension
+ let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`);
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(
+ baseURI, {addonId: addon.id}
+ );
+ Services.qms.clearStoragesForPrincipal(principal);
+
+ // Clear localStorage created by the extension
+ let attrs = JSON.stringify({addonId: addon.id});
+ Services.obs.notifyObservers(null, "clear-origin-data", attrs);
+ }
+
+ if (!this.leaveUuid) {
+ // Clear the entry in the UUID map
+ UUIDMap.remove(addon.id);
}
},
};
// Responsible for loading extension APIs into the right globals.
GlobalManager = {
// Map[extension ID -> Extension]. Determines which extension is
// responsible for content under a particular extension ID.
@@ -574,17 +603,16 @@ GlobalManager = {
this.extensionMap.set(extension.id, extension);
},
uninit(extension) {
this.extensionMap.delete(extension.id);
if (this.extensionMap.size == 0 && this.initialized) {
Services.obs.removeObserver(this, "content-document-global-created");
- UninstallObserver.uninit();
this.initialized = false;
}
},
getExtension(extensionId) {
return this.extensionMap.get(extensionId);
},
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -19,8 +19,9 @@ skip-if = (os == 'android') # browser.ta
skip-if = os != "mac" && os != "linux"
[test_ext_cookies_expiry.html]
skip-if = buildapp == 'b2g'
[test_ext_cookies_permissions.html]
skip-if = buildapp == 'b2g'
[test_ext_jsversion.html]
skip-if = buildapp == 'b2g'
[test_ext_schema.html]
+[test_chrome_ext_storage_cleanup.html]
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
@@ -13,47 +13,38 @@
<script type="text/javascript">
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/TestUtils.jsm");
-const {
- GlobalManager,
- UninstallObserver,
-} = Cu.import("resource://gre/modules/Extension.jsm");
+const {GlobalManager} = Cu.import("resource://gre/modules/Extension.jsm");
/* eslint-disable mozilla/balanced-listeners */
add_task(function* testShutdownCleanup() {
is(GlobalManager.initialized, false,
"GlobalManager start as not initialized");
- is(UninstallObserver.initialized, false,
- "UninstallObserver start as not initialized");
let extension = ExtensionTestUtils.loadExtension({
background: "new " + function() {
browser.test.notifyPass("background page loaded");
},
});
yield extension.startup();
yield extension.awaitFinish("background page loaded");
is(GlobalManager.initialized, true,
"GlobalManager has been initialized once an extension is started");
- is(UninstallObserver.initialized, true,
- "UninstallObserver has been initialized once an extension is started");
yield extension.unload();
is(GlobalManager.initialized, false,
"GlobalManager has been uninitialized once all the webextensions have been stopped");
- is(UninstallObserver.initialized, false,
- "UninstallObserver has been uninitialized once all the webextensions have been stopped");
});
</script>
</body>
</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
@@ -0,0 +1,161 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// Test that storage used by a webextension (through localStorage,
+// indexedDB, and browser.storage.local) gets cleaned up when the
+// extension is uninstalled.
+add_task(function* test_uninstall() {
+ function writeData() {
+ localStorage.setItem("hello", "world");
+
+ let idbPromise = new Promise((resolve, reject) => {
+ let req = indexedDB.open("test");
+ req.onerror = e => {
+ reject(new Error(`indexedDB open failed with ${e.errorCode}`));
+ };
+
+ req.onupgradeneeded = e => {
+ let db = e.target.result;
+ db.createObjectStore("store", {keyPath: "name"});
+ };
+
+ req.onsuccess = e => {
+ let db = e.target.result;
+ let transaction = db.transaction("store", "readwrite");
+ let addreq = transaction.objectStore("store")
+ .add({name: "hello", value: "world"});
+ addreq.onerror = e => {
+ reject(new Error(`add to indexedDB failed with ${e.errorCode}`));
+ };
+ addreq.onsuccess = e => {
+ resolve();
+ };
+ };
+ });
+
+ let browserStoragePromise = browser.storage.local.set({hello: "world"});
+
+ Promise.all([idbPromise, browserStoragePromise]).then(() => {
+ browser.test.sendMessage("finished");
+ });
+ }
+
+ function readData() {
+ let matchLocalStorage = (localStorage.getItem("hello") == "world");
+
+ let idbPromise = new Promise((resolve, reject) => {
+ let req = indexedDB.open("test");
+ req.onerror = e => {
+ reject(new Error(`indexedDB open failed with ${e.errorCode}`));
+ };
+
+ req.onupgradeneeded = e => {
+ // no database, data is not present
+ resolve(false);
+ };
+
+ req.onsuccess = e => {
+ let db = e.target.result;
+ let transaction = db.transaction("store", "readwrite");
+ let addreq = transaction.objectStore("store").get("hello");
+ addreq.onerror = e => {
+ reject(new Error(`read from indexedDB failed with ${e.errorCode}`));
+ };
+ addreq.onsuccess = e => {
+ let match = (addreq.result.value == "world");
+ resolve(match);
+ };
+ };
+ });
+
+ let browserStoragePromise = browser.storage.local.get("hello").then(result => {
+ return (Object.keys(result).length == 1 && result.hello == "world");
+ });
+
+ Promise.all([idbPromise, browserStoragePromise])
+ .then(([matchIDB, matchBrowserStorage]) => {
+ let result = {matchLocalStorage, matchIDB, matchBrowserStorage};
+ browser.test.sendMessage("results", result);
+ });
+ }
+
+ const ID = "storage.cleanup@tests.mozilla.org";
+
+ // Use a test-only pref to leave the addonid->uuid mapping around after
+ // uninstall so that we can re-attach to the same storage. Also set
+ // the pref to prevent cleaning up storage on uninstall so we can test
+ // that the "keep uuid" logic works correctly. Do the storage flag in
+ // a separate prefEnv so we can pop it below, leaving the uuid flag set.
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.keepUuidOnUninstall", true]],
+ });
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.keepStorageOnUninstall", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${writeData})()`,
+ manifest: {
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ }, ID);
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+ yield extension.unload();
+
+ // Check that we can still see data we wrote to storage but clear the
+ // "leave storage" flag so our storaged gets cleared on uninstall.
+ // This effectively tests the keepUuidOnUninstall logic, which ensures
+ // that when we read storage again and check that it is cleared, that
+ // it is actually a meaningful test!
+ yield SpecialPowers.popPrefEnv();
+ extension = ExtensionTestUtils.loadExtension({
+ background: `(${readData})()`,
+ manifest: {
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ }, ID);
+
+ yield extension.startup();
+ let results = yield extension.awaitMessage("results");
+ is(results.matchLocalStorage, true, "localStorage data is still present");
+ is(results.matchIDB, true, "indexedDB data is still present");
+ is(results.matchBrowserStorage, true, "browser.storage.local data is still present");
+
+ yield extension.unload();
+
+ // Read again. This time, our data should be gone.
+ extension = ExtensionTestUtils.loadExtension({
+ background: `(${readData})()`,
+ manifest: {
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ }, ID);
+
+ yield extension.startup();
+ results = yield extension.awaitMessage("results");
+ is(results.matchLocalStorage, false, "localStorage data was cleared");
+ is(results.matchIDB, false, "indexedDB data was cleared");
+ is(results.matchBrowserStorage, false, "browser.storage.local data was cleared");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>