Bug 1496801 - Set persist mode on ExtensionStorageIDB for extensions with the unlimitedStorage permission. r=mixedpuppy,janv
authorLuca Greco <lgreco@mozilla.com>
Sat, 17 Nov 2018 20:07:28 +0000
changeset 504312 2317749c5abfb7eab139a2545d2fa5362d2309f0
parent 504311 d6d4324ac77986649e1e09983b1f4061ff40d395
child 504319 8264fe75578f62fa4f14d48ec8ca86d109e8ddf5
child 504320 73385b8318802d1d7769bada221d96d17eea0ead
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, janv
bugs1496801
milestone65.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 1496801 - Set persist mode on ExtensionStorageIDB for extensions with the unlimitedStorage permission. r=mixedpuppy,janv Differential Revision: https://phabricator.services.mozilla.com/D7915
toolkit/components/extensions/ExtensionStorageIDB.jsm
toolkit/components/extensions/child/ext-storage.js
toolkit/components/extensions/parent/ext-storage.js
toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html
--- a/toolkit/components/extensions/ExtensionStorageIDB.jsm
+++ b/toolkit/components/extensions/ExtensionStorageIDB.jsm
@@ -11,16 +11,20 @@ ChromeUtils.import("resource://gre/modul
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
   getTrimmedString: "resource://gre/modules/ExtensionTelemetry.jsm",
   Services: "resource://gre/modules/Services.jsm",
   OS: "resource://gre/modules/osfile.jsm",
 });
 
+XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+                                   "@mozilla.org/dom/quota-manager-service;1",
+                                   "nsIQuotaManagerService");
+
 // The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
 // storage used by the browser.storage.local API is not directly accessible from the extension code,
 // it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.jsm).
 const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0;
 
 const IDB_NAME = "webExtensions-storage-local";
 const IDB_DATA_STORENAME = "storage-local-data";
 const IDB_VERSION = 1;
@@ -350,17 +354,17 @@ async function migrateJSONFileData(exten
   let dataMigrateCompleted = false;
   let hasOldData = false;
 
   if (ExtensionStorageIDB.isMigratedExtension(extension)) {
     return;
   }
 
   try {
-    idbConn = await ExtensionStorageLocalIDB.openForPrincipal(storagePrincipal);
+    idbConn = await ExtensionStorageIDB.open(storagePrincipal, extension.hasPermission("unlimitedStorage"));
     hasEmptyIDB = await idbConn.isEmpty();
 
     if (!hasEmptyIDB) {
       // If the IDB backend is enabled and there is data already stored in the IDB backend,
       // there is no "going back": any data that has not been migrated will be still on disk
       // but it is not going to be migrated anymore, it could be eventually used to allow
       // a user to manually retrieve the old data file).
       ExtensionStorageIDB.setMigratedExtensionPref(extension, true);
@@ -610,29 +614,48 @@ this.ExtensionStorageIDB = {
       }
 
       this.selectedBackendPromises.set(extension, promise);
     }
 
     return this.selectedBackendPromises.get(extension);
   },
 
+  persist(storagePrincipal) {
+    return new Promise((resolve, reject) => {
+      const request = quotaManagerService.persist(storagePrincipal);
+      request.callback = () => {
+        if (request.resultCode === Cr.NS_OK) {
+          resolve();
+        } else {
+          reject(new Error(`Failed to persist storage for principal: ${storagePrincipal.originNoSuffix}`));
+        }
+      };
+    });
+  },
+
   /**
    * Open a connection to the IDB storage.local db for a given extension.
    * given extension.
    *
    * @param {nsIPrincipal} storagePrincipal
    *        The "internally reserved" extension storagePrincipal to be used to create
    *        the ExtensionStorageLocalIDB instance.
+   * @param {boolean} persisted
+   *        A boolean which indicates if the storage should be set into persistent mode.
    *
    * @returns {Promise<ExtensionStorageLocalIDB>}
    *          Return a promise which resolves to the opened IDB connection.
    */
-  open(storagePrincipal) {
-    return ExtensionStorageLocalIDB.openForPrincipal(storagePrincipal);
+  open(storagePrincipal, persisted) {
+    if (!storagePrincipal) {
+      return Promise.reject(new Error("Unexpected empty principal"));
+    }
+    let setPersistentMode = persisted ? this.persist(storagePrincipal) : Promise.resolve();
+    return setPersistentMode.then(() => ExtensionStorageLocalIDB.openForPrincipal(storagePrincipal));
   },
 
   addOnChangedListener(extensionId, listener) {
     let listeners = this.listeners.get(extensionId) || new Set();
     listeners.add(listener);
     this.listeners.set(extensionId, listeners);
   },
 
--- a/toolkit/components/extensions/child/ext-storage.js
+++ b/toolkit/components/extensions/child/ext-storage.js
@@ -52,17 +52,18 @@ this.storage = class extends ExtensionAP
 
   getLocalIDBBackend(context, {fireOnChanged, serialize, storagePrincipal}) {
     let dbPromise;
     async function getDB() {
       if (dbPromise) {
         return dbPromise;
       }
 
-      dbPromise = ExtensionStorageIDB.open(storagePrincipal).catch(err => {
+      const persisted = context.extension.hasPermission("unlimitedStorage");
+      dbPromise = ExtensionStorageIDB.open(storagePrincipal, persisted).catch(err => {
         // Reset the cached promise if it has been rejected, so that the next
         // API call is going to retry to open the DB.
         dbPromise = null;
         throw err;
       });
 
       return dbPromise;
     }
--- a/toolkit/components/extensions/parent/ext-storage.js
+++ b/toolkit/components/extensions/parent/ext-storage.js
@@ -68,17 +68,18 @@ this.storage = class extends ExtensionAP
       storage: {
         local: {
           async callMethodInParentProcess(method, args) {
             const res = await ExtensionStorageIDB.selectBackend({extension});
             if (!res.backendEnabled) {
               return ExtensionStorage[method](extension.id, ...args);
             }
 
-            const db = await ExtensionStorageIDB.open(res.storagePrincipal.deserialize(this));
+            const persisted = extension.hasPermission("unlimitedStorage");
+            const db = await ExtensionStorageIDB.open(res.storagePrincipal.deserialize(this), persisted);
             const changes = await db[method](...args);
             if (changes) {
               ExtensionStorageIDB.notifyListeners(extension.id, changes);
             }
             return changes;
           },
           // Private storage.local JSONFile backend methods (used internally by the child
           // ext-storage.js module).
--- a/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html
@@ -1,54 +1,56 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for simple WebExtension</title>
+  <meta charset="utf-8">
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <script type="text/javascript" src="head_unlimitedStorage.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 
 "use strict";
 
-add_task(async function test_background_storagePersist() {
+async function test_background_storagePersist(EXTENSION_ID) {
   await SpecialPowers.pushPrefEnv({
     "set": [
       ["dom.storageManager.enabled", true],
       ["dom.storageManager.prompt.testing", false],
       ["dom.storageManager.prompt.testing.allow", false],
     ],
   });
 
-  const EXTENSION_ID = "test-storagePersist@mozilla";
-
   const extension = ExtensionTestUtils.loadExtension({
     useAddonManager: "permanent",
 
     manifest: {
-      permissions: ["unlimitedStorage"],
+      permissions: ["storage", "unlimitedStorage"],
       applications: {
         gecko: {
           id: EXTENSION_ID,
         },
       },
     },
 
     background: async function() {
       const PROMISE_RACE_TIMEOUT = 8000;
 
       browser.test.sendMessage("extension-uuid", window.location.host);
 
+      await browser.storage.local.set({testkey: "testvalue"});
+      await browser.test.sendMessage("storage-local-called");
+
       const requestStoragePersist = async () => {
         const persistAllowed = await navigator.storage.persist();
         if (!persistAllowed) {
           throw new Error("navigator.storage.persist() has been denied");
         }
       };
 
       await Promise.race([
@@ -69,20 +71,72 @@ add_task(async function test_background_
       );
     },
   });
 
   await extension.startup();
 
   const uuid = await extension.awaitMessage("extension-uuid");
 
+  await extension.awaitMessage("storage-local-called");
+
+  let chromeScript = SpecialPowers.loadChromeScript(function test_country_data() {
+    const {addMessageListener, sendAsyncMessage} = this;
+
+    addMessageListener("getPersistedStatus", (uuid) => {
+      const {
+        ExtensionStorageIDB,
+      } = ChromeUtils.import("resource://gre/modules/ExtensionStorageIDB.jsm", {});
+
+      const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
+
+      const {WebExtensionPolicy} = Cu.getGlobalForObject(ExtensionStorageIDB);
+      const policy = WebExtensionPolicy.getByHostname(uuid);
+      const storagePrincipal = ExtensionStorageIDB.getStoragePrincipal(policy.extension);
+      const request = Services.qms.persisted(storagePrincipal);
+      request.callback = () => {
+        // request.result will be undeinfed if the request failed (request.resultCode !== Cr.NS_OK).
+        sendAsyncMessage("gotPersistedStatus", request.result);
+      };
+    });
+  });
+
+  const persistedPromise = chromeScript.promiseOneMessage("gotPersistedStatus");
+  chromeScript.sendAsyncMessage("getPersistedStatus", uuid);
+  is(await persistedPromise, true, "Got the expected persist status for the storagePrincipal");
+
   await extension.awaitFinish("indexeddb-storagePersistent-unlimitedStorage-done");
   await extension.unload();
 
   checkSitePermissions(uuid, Services.perms.UNKNOWN_ACTION, "has been cleared");
+}
+
+add_task(async function test_unlimitedStorage() {
+  const EXTENSION_ID = "test-storagePersist@mozilla";
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      ["extensions.webextensions.ExtensionStorageIDB.enabled", true],
+    ],
+  });
+
+  // Verify persist mode enabled when the storage.local IDB database is opened from
+  // the main process (from parent/ext-storage.js).
+  info("Test unlimitedStorage on an extension migrating to the IndexedDB storage.local backend)");
+  await test_background_storagePersist(EXTENSION_ID);
+
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      [`extensions.webextensions.ExtensionStorageIDB.migrated.` + EXTENSION_ID, true],
+    ],
+  });
+
+  // Verify persist mode enabled when the storage.local IDB database is opened from
+  // the child process (from child/ext-storage.js).
+  info("Test unlimitedStorage on an extension migrated to the IndexedDB storage.local backend");
+  await test_background_storagePersist(EXTENSION_ID);
 });
 
 add_task(async function test_unlimitedStorage_removed_on_update() {
   const EXTENSION_ID = "test-unlimitedStorage-removed-on-update@mozilla";
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["unlimitedStorage"],