Bug 1331618 - allow persistent indexedDB on unlimitedStorage permission. draft
authorLuca Greco <lgreco@mozilla.com>
Fri, 16 Jun 2017 18:26:50 +0200
changeset 596722 7a435c84ff22eb18df79ae677b809d0654fcc90d
parent 596561 d39cd452b52bf82fa4a717172a62d62ab9e5366f
child 634048 13af4c19948cf5fcbf8c245e20b54cc9e2ce4844
push id64734
push userluca.greco@alcacoop.it
push dateMon, 19 Jun 2017 17:34:36 +0000
bugs1331618
milestone56.0a1
Bug 1331618 - allow persistent indexedDB on unlimitedStorage permission. MozReview-Commit-ID: 6VYqywMgSoU
browser/locales/en-US/chrome/browser/browser.properties
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/schemas/manifest.json
toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html
toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage_legacy_persistent_indexedDB.html
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -105,16 +105,17 @@ webextPerms.description.management=Monit
 # LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
 # %S will be replaced with the name of the application
 webextPerms.description.nativeMessaging=Exchange messages with programs other than %S
 webextPerms.description.notifications=Display notifications to you
 webextPerms.description.privacy=Read and modify privacy settings
 webextPerms.description.sessions=Access recently closed tabs
 webextPerms.description.tabs=Access browser tabs
 webextPerms.description.topSites=Access browsing history
+webextPerms.description.unlimitedStorage=Store unlimited amount of client-side data
 webextPerms.description.webNavigation=Access browser activity during navigation
 
 webextPerms.hostDescription.allUrls=Access your data for all websites
 
 # LOCALIZATION NOTE (webextPerms.hostDescription.wildcard)
 # %S will be replaced by the DNS domain for which a webextension
 # is requesting access (e.g., mozilla.org)
 webextPerms.hostDescription.wildcard=Access your data for sites in the %S domain
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -277,16 +277,22 @@ var UninstallObserver = {
         baseURI, {});
       Services.qms.clearStoragesForPrincipal(principal);
 
       // Clear localStorage created by the extension
       let storage = Services.domStorageManager.getStorage(null, principal);
       if (storage) {
         storage.clear();
       }
+
+      // Remove any permissions related to the unlimitedStorage permission
+      // if we are also removing all the data stored by the extension.
+      Services.perms.removeFromPrincipal(principal, "WebExtensions-unlimitedStorage");
+      Services.perms.removeFromPrincipal(principal, "indexedDB");
+      Services.perms.removeFromPrincipal(principal, "persistent-storage");
     }
 
     if (!this.leaveUuid) {
       // Clear the entry in the UUID map
       UUIDMap.remove(addon.id);
     }
   },
 };
@@ -990,16 +996,41 @@ this.Extension = class extends Extension
 
       let match = Locale.findClosestLocale(localeList);
       locale = match ? match.name : this.defaultLocale;
     }
 
     return super.initLocale(locale);
   }
 
+  initUnlimitedStoragePermission() {
+    const principal = this.principal;
+
+    // Check if the site permission has already been set for the extension by the WebExtensions
+    // internals (instead of being manually allowed by the user).
+    const hasSitePermission = Services.perms.testPermissionFromPrincipal(
+      principal, "WebExtensions-unlimitedStorage"
+    );
+
+    if (this.hasPermission("unlimitedStorage")) {
+      // Set the indexedDB permission and a custom "WebExtensions-unlimitedStorage" to remember
+      // that the permission hasn't been selected manually by the user.
+      Services.perms.addFromPrincipal(principal, "WebExtensions-unlimitedStorage",
+                                      Services.perms.ALLOW_ACTION);
+      Services.perms.addFromPrincipal(principal, "indexedDB", Services.perms.ALLOW_ACTION);
+      Services.perms.addFromPrincipal(principal, "persistent-storage", Services.perms.ALLOW_ACTION);
+    } else if (hasSitePermission) {
+      // Remove the indexedDB permission if it has been enabled using the
+      // unlimitedStorage WebExtensions permissions.
+      Services.perms.removeFromPrincipal(principal, "WebExtensions-unlimitedStorage");
+      Services.perms.removeFromPrincipal(principal, "indexedDB");
+      Services.perms.removeFromPrincipal(principal, "persistent-storage");
+    }
+  }
+
   startup() {
     this.startupPromise = this._startup();
     return this.startupPromise;
   }
 
   async _startup() {
     // Create a temporary policy object for the devtools and add-on
     // manager callers that depend on it being available early.
@@ -1052,16 +1083,18 @@ this.Extension = class extends Extension
           .map(path => path.replace(/^\/*/, "/"));
 
       this.webAccessibleResources = resources.map(res => new MatchGlob(res));
 
 
       this.policy.active = false;
       this.policy = processScript.initExtension(this.serialize(), this);
 
+      this.initUnlimitedStoragePermission();
+
       // 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/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -240,17 +240,18 @@
       {
         "id": "Permission",
         "choices": [
           { "$ref": "OptionalPermission" },
           {
             "type": "string",
             "enum": [
               "alarms",
-              "storage"
+              "storage",
+              "unlimitedStorage"
             ]
           }
         ]
       },
       {
         "id": "HttpURL",
         "type": "string",
         "format": "url",
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_unlimitedStorage.js
@@ -0,0 +1,34 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported checkSitePermissions */
+
+const {Services, Cu} = SpecialPowers;
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+
+function checkSitePermissions(uuid, expectedPermAction, assertMessage) {
+  if (!uuid) {
+    throw new Error("checkSitePermissions should not be called with an undefined uuid");
+  }
+
+  const baseURI = NetUtil.newURI(`moz-extension://${uuid}/`);
+  const principal = Services.scriptSecurityManager.createCodebasePrincipal(baseURI, {});
+
+  const sitePermissions = {
+    webextUnlimitedStorage: Services.perms.testPermissionFromPrincipal(
+      principal, "WebExtensions-unlimitedStorage"
+    ),
+    indexedDB: Services.perms.testPermissionFromPrincipal(
+      principal, "indexedDB"
+    ),
+    persistentStorage: Services.perms.testPermissionFromPrincipal(
+      principal, "WebExtensions-unlimitedStorage"
+    ),
+  };
+
+  for (const [sitePermissionName, actualPermAction] of Object.entries(sitePermissions)) {
+    is(actualPermAction, expectedPermAction,
+       `The extension "${sitePermissionName}" SitePermission ${assertMessage} as expected`);
+  }
+}
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 support-files =
   chrome_cleanup_script.js
   head.js
+  head_unlimitedStorage.js
+  head_webrequest.js
   file_mixed.html
-  head_webrequest.js
   file_csp.html
   file_csp.html^headers^
   file_to_drawWindow.html
   file_WebRequest_page3.html
   file_WebRequest_permission_original.html
   file_WebRequest_permission_redirected.html
   file_WebRequest_permission_original.js
   file_WebRequest_permission_redirected.js
@@ -97,16 +98,21 @@ skip-if = os == 'android' # Android supp
 scheme=https
 [test_ext_test.html]
 [test_ext_cookies.html]
 [test_ext_background_api_injection.html]
 [test_ext_background_generated_url.html]
 [test_ext_background_teardown.html]
 [test_ext_tab_teardown.html]
 skip-if = os == 'android' # Bug 1258975 on android.
+[test_ext_unlimitedStorage.html]
+[test_ext_unlimitedStorage_legacy_persistent_indexedDB.html]
+# IndexedDB persistent storage mode is not allowed on Fennec from a non-chrome privileged code
+# (it has only been enabled for apps and privileged code). See Bug 1119462 for additional info.
+skip-if = os == 'android'
 [test_ext_unload_frame.html]
 [test_ext_listener_proxies.html]
 [test_ext_web_accessible_resources.html]
 [test_ext_webrequest_auth.html]
 skip-if = os == 'android'
 [test_ext_webrequest_background_events.html]
 [test_ext_webrequest_basic.html]
 [test_ext_webrequest_filter.html]
@@ -115,9 +121,9 @@ skip-if = os == 'android'
 [test_ext_webrequest_upload.html]
 skip-if = os == 'android' # Currently fails in emulator tests
 [test_ext_webrequest_permission.html]
 [test_ext_webrequest_websocket.html]
 [test_ext_webnavigation.html]
 [test_ext_webnavigation_filters.html]
 [test_ext_window_postMessage.html]
 [test_ext_subframes_privileges.html]
-[test_ext_xhr_capabilities.html]
+[test_ext_xhr_capabilities.html]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage.html
@@ -0,0 +1,140 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for simple WebExtension</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="/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() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      ["dom.storageManager.enabled", true],
+      // Enable the storageManager permission prompt.
+      ["browser.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"],
+      applications: {
+        gecko: {
+          id: EXTENSION_ID,
+        },
+      },
+    },
+
+    background: async function() {
+      const PROMISE_RACE_TIMEOUT = 2000;
+
+      browser.test.sendMessage("extension-uuid", window.location.host);
+
+      const requestStoragePersist = async () => {
+        const persistAllowed = await navigator.storage.persist();
+        if (!persistAllowed) {
+          throw new Error("navigator.storage.persist() has been denied");
+        }
+      };
+
+      await Promise.race([
+        requestStoragePersist(),
+        new Promise((resolve, reject) => {
+          setTimeout(() => {
+            reject(new Error("Timeout opening persistent db from background page"));
+          }, PROMISE_RACE_TIMEOUT);
+        }),
+      ]).then(
+        () => {
+          browser.test.notifyPass("indexeddb-storagePersistent-unlimitedStorage-done");
+        },
+        (error) => {
+          browser.test.fail(`error while testing persistent IndexedDB storage: ${error}`);
+          browser.test.notifyFail("indexeddb-storagePersistent-unlimitedStorage-done");
+        }
+      );
+    },
+  });
+
+  await extension.startup();
+
+  const uuid = await extension.awaitMessage("extension-uuid");
+
+  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_removed_on_update() {
+  const EXTENSION_ID = "test-unlimitedStorage-removed-on-update@mozilla";
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["unlimitedStorage"],
+      applications: {
+        gecko: {
+          id: EXTENSION_ID,
+        },
+      },
+    },
+
+    background: async function() {
+      browser.test.sendMessage("extension-uuid", window.location.host);
+    },
+  });
+
+  await extension.startup();
+
+  const uuid = await extension.awaitMessage("extension-uuid");
+
+  checkSitePermissions(uuid, Services.perms.ALLOW_ACTION, "has been allowed");
+
+  await extension.unload();
+
+  // Simulate an update which do not require the unlimitedStorage permission.
+  let updatedExtension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: {
+        gecko: {
+          id: EXTENSION_ID,
+        },
+      },
+    },
+
+    background: async function() {
+      browser.test.sendMessage("updated-extension-uuid", window.location.host);
+    },
+  });
+
+  updatedExtension.startup();
+
+  const updatedExtensionUUID = await updatedExtension.awaitMessage("updated-extension-uuid");
+
+  is(uuid, updatedExtensionUUID, "The updated extension has the same uuid");
+
+  checkSitePermissions(uuid, Services.perms.UNKNOWN_ACTION, "has been cleared");
+
+  await updatedExtension.unload();
+});
+
+</script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_unlimitedStorage_legacy_persistent_indexedDB.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for simple WebExtension</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="/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_legacy_indexedDB_storagePersistent_unlimitedStorage() {
+  const EXTENSION_ID = "test-idbStoragePersistent@mozilla";
+
+  const extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "permanent",
+
+    manifest: {
+      permissions: ["unlimitedStorage"],
+      applications: {
+        gecko: {
+          id: EXTENSION_ID,
+        },
+      },
+    },
+
+    background: async function() {
+      const PROMISE_RACE_TIMEOUT = 2000;
+
+      browser.test.sendMessage("extension-uuid", window.location.host);
+
+      try {
+        await Promise.race([
+          new Promise((resolve, reject) => {
+            const dbReq = indexedDB.open("test-persistent-idb", {version: 1.0, storage: "persistent"});
+
+            dbReq.onerror = evt => {
+              reject(evt.target.error);
+            };
+
+            dbReq.onsuccess = () => {
+              resolve();
+            };
+          }),
+          new Promise((resolve, reject) => {
+            setTimeout(() => {
+              reject(new Error("Timeout opening persistent db from background page"));
+            }, PROMISE_RACE_TIMEOUT);
+          }),
+        ]);
+
+        browser.test.notifyPass("indexeddb-storagePersistent-unlimitedStorage-done");
+      } catch (error) {
+        const loggedError = error instanceof DOMError ? error.message : error;
+        browser.test.fail(`error while testing persistent IndexedDB storage: ${loggedError}`);
+        browser.test.notifyFail("indexeddb-storagePersistent-unlimitedStorage-done");
+      }
+    },
+  });
+
+  await extension.startup();
+
+  const uuid = await extension.awaitMessage("extension-uuid");
+
+  await extension.awaitFinish("indexeddb-storagePersistent-unlimitedStorage-done");
+
+  await extension.unload();
+
+  checkSitePermissions(uuid, Services.perms.UNKNOWN_ACTION, "has been cleared");
+});
+
+</script>
+
+</body>
+</html>