Bug 1477015 - Select the storage.local backend during the extension startup when ExtensionStorageIDB is disabled. r=aswan a=lizzard
authorLuca Greco <lgreco@mozilla.com>
Tue, 07 Aug 2018 14:23:38 +0200
changeset 478385 1302ee69d602
parent 478384 d8cf50cb1c98
child 478386 fe256fdb428c
push id9645
push userarchaeopteryx@coole-files.de
push date2018-08-12 08:18 +0000
treeherdermozilla-beta@fe256fdb428c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan, lizzard
bugs1477015
milestone62.0
Bug 1477015 - Select the storage.local backend during the extension startup when ExtensionStorageIDB is disabled. r=aswan a=lizzard
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/child/ext-storage.js
toolkit/components/extensions/test/xpcshell/test_ext_storage_tab.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -82,16 +82,18 @@ ChromeUtils.import("resource://gre/modul
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
   spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
   uuidGen: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "processCount", "dom.ipc.processCount.extension");
+XPCOMUtils.defineLazyPreferenceGetter(this, "isStorageIDBEnabled",
+                                      "extensions.webextensions.ExtensionStorageIDB.enabled");
 
 var {
   GlobalManager,
   ParentAPIManager,
   StartupCache,
   apiManager: Management,
 } = ExtensionParent;
 
@@ -1288,16 +1290,17 @@ class Extension extends ExtensionData {
     this.version = addonData.version;
     this.baseURL = this.getURL("");
     this.baseURI = Services.io.newURI(this.baseURL).QueryInterface(Ci.nsIURL);
     this.principal = this.createPrincipal();
 
     this.views = new Set();
     this._backgroundPageFrameLoader = null;
 
+    this.storageIDBBackend = null;
     this.onStartup = null;
 
     this.hasShutdown = false;
     this.onShutdown = new Set();
 
     this.uninstallURL = null;
 
     this.whiteListedHosts = null;
@@ -1528,16 +1531,17 @@ class Extension extends ExtensionData {
       whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern),
       localeData: this.localeData.serialize(),
       childModules: this.modules && this.modules.child,
       dependencies: this.dependencies,
       permissions: this.permissions,
       principal: this.principal,
       optionalPermissions: this.manifest.optional_permissions,
       schemaURLs: this.schemaURLs,
+      storageIDBBackend: this.storageIDBBackend,
     };
   }
 
   get contentScripts() {
     return this.manifest.content_scripts || [];
   }
 
   broadcast(msg, data) {
@@ -1725,16 +1729,20 @@ class Extension extends ExtensionData {
       GlobalManager.init(this);
 
       this.policy.active = false;
       this.policy = processScript.initExtension(this);
       this.policy.extension = this;
 
       this.updatePermissions(this.startupReason);
 
+      if (this.hasPermission("storage") && !isStorageIDBEnabled) {
+        this.storageIDBBackend = false;
+      }
+
       // The "startup" Management event sent on the extension instance itself
       // is emitted just before the Management "startup" event,
       // and it is used to run code that needs to be executed before
       // any of the "startup" listeners.
       this.emit("startup", this);
       Management.emit("startup", this);
 
       await this.runManifest(this.manifest);
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -601,16 +601,18 @@ class BrowserExtensionContent extends Ev
     this.id = data.id;
     this.uuid = data.uuid;
     this.instanceId = data.instanceId;
 
     this.childModules = data.childModules;
     this.dependencies = data.dependencies;
     this.schemaURLs = data.schemaURLs;
 
+    this.storageIDBBackend = data.storageIDBBackend;
+
     this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
     Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
 
     defineLazyGetter(this, "scripts", () => {
       return data.contentScripts.map(scriptData => new ExtensionContent.Script(this, scriptData));
     });
 
     this.webAccessibleResources = data.webAccessibleResources.map(res => new MatchGlob(res));
--- a/toolkit/components/extensions/child/ext-storage.js
+++ b/toolkit/components/extensions/child/ext-storage.js
@@ -124,16 +124,17 @@ this.storage = class extends ExtensionAP
           await context.childManager.callParentAsyncFunction(
             "storage.local.IDBBackend.fireOnChanged", [changes]);
         }
       },
     };
   }
 
   getAPI(context) {
+    const {extension} = context;
     const serialize = ExtensionStorage.serializeForContext.bind(null, context);
     const deserialize = ExtensionStorage.deserializeForContext.bind(null, context);
 
     function sanitize(items) {
       // The schema validator already takes care of arrays (which are only allowed
       // to contain strings). Strings and null are safe values.
       if (typeof items != "object" || items === null || Array.isArray(items)) {
         return items;
@@ -173,29 +174,41 @@ this.storage = class extends ExtensionAP
           // in the meantime).
           return context.childManager.callParentAsyncFunction(
             "storage.local.IDBBackend.hasListeners", []);
         },
         serialize,
       });
     };
 
+    // Synchronously select the backend if the IndexedDB backend is not enabled.
+    let selectedBackend;
+    if (extension.storageIDBBackend === false) {
+      selectedBackend = this.getLocalFileBackend(context, {deserialize, serialize});
+    }
+
     // Generate the backend-agnostic local API wrapped methods.
     const local = {};
     for (let method of ["get", "set", "remove", "clear"]) {
       local[method] = async function(...args) {
-        if (!promiseStorageLocalBackend) {
-          promiseStorageLocalBackend = getStorageLocalBackend();
+        // Discover the selected backend if it is not known yet.
+        if (!selectedBackend) {
+          if (!promiseStorageLocalBackend) {
+            promiseStorageLocalBackend = getStorageLocalBackend().catch(err => {
+              // Clear the cached promise if it has been rejected.
+              promiseStorageLocalBackend = null;
+              throw err;
+            });
+          }
+
+          // Get the selected backend and cache it for the next API calls from this context.
+          selectedBackend = await promiseStorageLocalBackend;
         }
-        const backend = await promiseStorageLocalBackend.catch(err => {
-          // Clear the cached promise if it has been rejected.
-          promiseStorageLocalBackend = null;
-          throw err;
-        });
-        return backend[method](...args);
+
+        return selectedBackend[method](...args);
       };
     }
 
     return {
       storage: {
         local,
 
         sync: {
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_tab.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_tab.js
@@ -99,28 +99,41 @@ add_task(async function test_storage_loc
 add_task(async function test_storage_local_idb_backend_from_tab() {
   return runWithPrefs([[ExtensionStorageIDB.BACKEND_ENABLED_PREF, true]],
                       test_multiple_pages);
 });
 
 async function test_storage_local_call_from_destroying_context() {
   let extension = ExtensionTestUtils.loadExtension({
     async background() {
+      let numberOfChanges = 0;
+      browser.storage.onChanged.addListener((changes, areaName) => {
+        if (areaName !== "local") {
+          browser.test.fail(`Received unexpected storage changes for "${areaName}"`);
+        }
+
+        numberOfChanges++;
+      });
+
       browser.test.onMessage.addListener(async ({msg, values}) => {
         switch (msg) {
           case "storage-set": {
             await browser.storage.local.set(values);
             browser.test.sendMessage("storage-set:done");
             break;
           }
           case "storage-get": {
             const res = await browser.storage.local.get();
             browser.test.sendMessage("storage-get:done", res);
             break;
           }
+          case "storage-changes": {
+            browser.test.sendMessage("storage-changes-count", numberOfChanges);
+            break;
+          }
           default:
             browser.test.fail(`Received unexpected message: ${msg}`);
         }
       });
 
       browser.test.sendMessage("ext-page-url", browser.runtime.getURL("tab.html"));
     },
     files: {
@@ -130,46 +143,66 @@ async function test_storage_local_call_f
             <meta charset="utf-8">
             <script src="tab.js"></script>
           </head>
         </html>`,
 
       "tab.js"() {
         browser.test.log("Extension tab - calling storage.local API method");
         // Call the storage.local API from a tab that is going to be quickly closed.
-        browser.storage.local.get({}).then(() => {
-          // This call should never be reached (because the tab should have been
-          // destroyed in the meantime).
-          browser.test.fail("Extension tab - Unexpected storage.local promise resolved");
+        browser.storage.local.set({
+          "test-key-from-destroying-context": "testvalue2",
         });
         // Navigate away from the extension page, so that the storage.local API call will be unable
         // to send the call to the caller context (because it has been destroyed in the meantime).
         window.location = "about:blank";
       },
     },
     manifest: {
       permissions: ["storage"],
     },
   });
 
   await extension.startup();
   const url = await extension.awaitMessage("ext-page-url");
 
   let contentPage = await ExtensionTestUtils.loadContentPage(url, {extension});
-  let expectedData = {"test-key": "test-value"};
+  let expectedBackgroundPageData = {"test-key-from-background-page": "test-value"};
+  let expectedTabData = {"test-key-from-destroying-context": "testvalue2"};
 
   info("Call storage.local.set from the background page and wait it to be completed");
-  extension.sendMessage({msg: "storage-set", values: expectedData});
+  extension.sendMessage({msg: "storage-set", values: expectedBackgroundPageData});
   await extension.awaitMessage("storage-set:done");
 
   info("Call storage.local.get from the background page and wait it to be completed");
   extension.sendMessage({msg: "storage-get"});
   let res = await extension.awaitMessage("storage-get:done");
 
-  Assert.deepEqual(res, expectedData, "Got the expected data set in the storage.local backend");
+  if (!ExtensionStorageIDB.isBackendEnabled) {
+    // When the JSONFile backend is enabled, the data stored from the destroying context
+    // is expected to be stored.
+    Assert.deepEqual(res, {
+      ...expectedBackgroundPageData,
+      ...expectedTabData,
+    }, "Got the expected data set in the storage.local backend");
+
+    extension.sendMessage({msg: "storage-changes"});
+    equal(await extension.awaitMessage("storage-changes-count"), 2,
+          "Got the expected number of storage.onChanged event received");
+  } else {
+    // When the IndexedDb backend is enabled, the data stored from the destroying context
+    // will not be stored.
+    Assert.deepEqual(res, {
+      ...expectedBackgroundPageData,
+    }, "Got the expected data set in the storage.local backend");
+
+    extension.sendMessage({msg: "storage-changes"});
+    equal(await extension.awaitMessage("storage-changes-count"), 1,
+          "Got the expected number of storage.onChanged event received");
+  }
 
   contentPage.close();
 
   await extension.unload();
 }
 
 add_task(async function test_storage_local_file_backend_destroyed_context_promise() {
   return runWithPrefs([[ExtensionStorageIDB.BACKEND_ENABLED_PREF, false]],