Bug 1213990 Clear storage when webextension is uninstalled r=kmag
☠☠ backed out by 265e7ad32cbe ☠ ☠
authorAndrew Swan <aswan@mozilla.com>
Mon, 01 Aug 2016 16:30:18 -0700
changeset 351474 c9b70a1998fcba46f84850089d29152eff7d9c6f
parent 351473 ffc2455a91358bc34705e590c6bbb60797ed5aa8
child 351475 fe7e2efb803f645c50bca64ddb89cd0129f0b4b4
push id1324
push usermtabara@mozilla.com
push dateMon, 16 Jan 2017 13:07:44 +0000
treeherdermozilla-release@a01c49833940 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1213990
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1213990 Clear storage when webextension is uninstalled r=kmag MozReview-Commit-ID: BeMOxOCSeru
modules/libpref/init/all.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4625,16 +4625,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
@@ -20,18 +20,19 @@ const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.importGlobalProperties(["TextEncoder"]);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/ExtensionContent.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
+                                  "resource://gre/modules/ExtensionStorage.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");
 XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.jsm");
@@ -72,16 +73,18 @@ var {
   injectAPI,
   instanceOf,
   extend,
   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`
     ^
     (
       (?:
         [^"] |
         " (?:[^"\\] | \\.)* "
       )*?
@@ -510,34 +513,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.
@@ -554,17 +582,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
@@ -20,9 +20,10 @@ skip-if = (os == 'android') # browser.ta
 [test_chrome_native_messaging_paths.html]
 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]
\ No newline at end of file
+[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>