Bug 1331618 - allow persistent indexedDB on unlimitedStorage permission.
MozReview-Commit-ID: 6VYqywMgSoU
--- 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>