Bug 1386427 - Part 4: Implement basic storage.managed functionality r=kmag
authorTomislav Jovanovic <tomica@gmail.com>
Sat, 16 Sep 2017 19:42:40 +0200
changeset 431660 a439e2ac43058989193daa201b764b96821bd8fa
parent 431659 603d09a85dd63adb4a8f3927b7d8dacc440437d0
child 431661 d88e5dec2638ae6d34527726ff0a9cb8d9336036
push id7785
push userryanvm@gmail.com
push dateThu, 21 Sep 2017 13:39:55 +0000
treeherdermozilla-beta@06d4034a8a03 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1386427
milestone57.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 1386427 - Part 4: Implement basic storage.managed functionality r=kmag MozReview-Commit-ID: Auy1ujS8wyz
toolkit/components/extensions/ExtensionStorage.jsm
toolkit/components/extensions/ext-c-storage.js
toolkit/components/extensions/ext-storage.js
toolkit/components/extensions/schemas/native_manifest.json
toolkit/components/extensions/schemas/storage.json
toolkit/components/extensions/test/xpcshell/test_ext_storage_managed.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/toolkit/components/extensions/ExtensionStorage.jsm
+++ b/toolkit/components/extensions/ExtensionStorage.jsm
@@ -103,19 +103,22 @@ this.ExtensionStorage = {
     OS.File.makeDir(this.getExtensionDir(extensionId), {
       ignoreExisting: true,
       from: OS.Constants.Path.profileDir,
     });
 
     let jsonFile = new JSONFile({path: this.getStorageFile(extensionId)});
     await jsonFile.load();
 
-    jsonFile.data = new SerializeableMap(Object.entries(jsonFile.data));
+    jsonFile.data = this._serializableMap(jsonFile.data);
+    return jsonFile;
+  },
 
-    return jsonFile;
+  _serializableMap(data) {
+    return new SerializeableMap(Object.entries(data));
   },
 
   /**
    * Returns a Promise for initialized JSONFile instance for the
    * extension's storage file.
    *
    * @param {string} extensionId
    *        The ID of the extension for which to return a file.
@@ -276,18 +279,20 @@ this.ExtensionStorage = {
    * @returns {Promise<object>}
    *        An object which a property for each requested key,
    *        containing that key's storage value. Values are
    *        StructuredCloneHolder objects which can be deserialized to
    *        the original storage value.
    */
   async get(extensionId, keys) {
     let jsonFile = await this.getFile(extensionId);
-    let {data} = jsonFile;
+    return this._filterProperties(jsonFile.data, keys);
+  },
 
+  async _filterProperties(data, keys) {
     let result = {};
     if (keys === null) {
       Object.assign(result, data.toJSON());
     } else if (typeof(keys) == "object" && !Array.isArray(keys)) {
       for (let prop in keys) {
         if (data.has(prop)) {
           result[prop] = serialize(data.get(prop));
         } else {
--- a/toolkit/components/extensions/ext-c-storage.js
+++ b/toolkit/components/extensions/ext-c-storage.js
@@ -128,16 +128,33 @@ this.storage = class extends ExtensionAP
           set: function(items) {
             items = sanitize(items);
             return context.childManager.callParentAsyncFunction("storage.sync.set", [
               items,
             ]);
           },
         },
 
+        managed: {
+          get(keys) {
+            return context.childManager.callParentAsyncFunction("storage.managed.get", [
+              serialize(keys),
+            ]).then(deserialize);
+          },
+          set(items) {
+            return Promise.reject({message: "storage.managed is read-only"});
+          },
+          remove(keys) {
+            return Promise.reject({message: "storage.managed is read-only"});
+          },
+          clear() {
+            return Promise.reject({message: "storage.managed is read-only"});
+          },
+        },
+
         onChanged: new EventManager(context, "storage.onChanged", fire => {
           let onChanged = (data, area) => {
             let changes = new context.cloneScope.Object();
             for (let [key, value] of Object.entries(data)) {
               changes[key] = deserialize(value);
             }
             fire.raw(changes, area);
           };
--- a/toolkit/components/extensions/ext-storage.js
+++ b/toolkit/components/extensions/ext-storage.js
@@ -1,34 +1,45 @@
 "use strict";
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-toolkit.js */
 
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
-                                  "resource://gre/modules/ExtensionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "extensionStorageSync",
-                                  "resource://gre/modules/ExtensionStorageSync.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
-                                  "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
+  ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
+  extensionStorageSync: "resource://gre/modules/ExtensionStorageSync.jsm",
+  NativeManifests: "resource://gre/modules/NativeManifests.jsm",
+});
 
 var {
   ExtensionError,
 } = ExtensionUtils;
 
 const enforceNoTemporaryAddon = extensionId => {
   const EXCEPTION_MESSAGE =
         "The storage API will not work with a temporary addon ID. " +
         "Please add an explicit addon ID to your manifest. " +
         "For more information see https://bugzil.la/1323228.";
   if (AddonManagerPrivate.isTemporaryInstallID(extensionId)) {
     throw new ExtensionError(EXCEPTION_MESSAGE);
   }
 };
 
+// WeakMap[extension -> Promise<SerializableMap?>]
+const managedStorage = new WeakMap();
+
+const lookupManagedStorage = async (extensionId, context) => {
+  let info = await NativeManifests.lookupManifest("storage", extensionId, context);
+  if (info) {
+    return ExtensionStorage._serializableMap(info.manifest.data);
+  }
+  return null;
+};
+
 this.storage = class extends ExtensionAPI {
   getAPI(context) {
     let {extension} = context;
     return {
       storage: {
         local: {
           get: function(spec) {
             return ExtensionStorage.get(extension.id, spec);
@@ -58,16 +69,34 @@ this.storage = class extends ExtensionAP
             return extensionStorageSync.remove(extension, keys, context);
           },
           clear: function() {
             enforceNoTemporaryAddon(extension.id);
             return extensionStorageSync.clear(extension, context);
           },
         },
 
+        managed: {
+          async get(keys) {
+            enforceNoTemporaryAddon(extension.id);
+            let lookup = managedStorage.get(extension);
+
+            if (!lookup) {
+              lookup = lookupManagedStorage(extension.id, context);
+              managedStorage.set(extension, lookup);
+            }
+
+            let data = await lookup;
+            if (!data) {
+              return Promise.reject({message: "Managed storage manifest not found"});
+            }
+            return ExtensionStorage._filterProperties(data, keys);
+          },
+        },
+
         onChanged: new EventManager(context, "storage.onChanged", fire => {
           let listenerLocal = changes => {
             fire.raw(changes, "local");
           };
           let listenerSync = changes => {
             fire.async(changes, "sync");
           };
 
--- a/toolkit/components/extensions/schemas/native_manifest.json
+++ b/toolkit/components/extensions/schemas/native_manifest.json
@@ -39,17 +39,20 @@
             "properties": {
               "name": {
                 "$ref": "manifest.ExtensionID"
               },
               "description": {
                 "type": "string"
               },
               "data": {
-                "type": "object"
+                "type": "object",
+                "additionalProperties": {
+                  "type": "any"
+                }
               },
               "type": {
                 "type": "string",
                 "enum": [
                   "storage"
                 ]
               }
             }
--- a/toolkit/components/extensions/schemas/storage.json
+++ b/toolkit/components/extensions/schemas/storage.json
@@ -215,15 +215,20 @@
         "properties": {
           "QUOTA_BYTES": {
             "value": 5242880,
             "description": "The maximum amount (in bytes) of data that can be stored in local storage, as measured by the JSON stringification of every value plus every key's length. This value will be ignored if the extension has the <code>unlimitedStorage</code> permission. Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError)."
           }
         }
       },
       "managed": {
-        "unsupported": true,
         "$ref": "StorageArea",
-        "description": "Items in the <code>managed</code> storage area are set by the domain administrator, and are read-only for the extension; trying to modify this namespace results in an error."
+        "description": "Items in the <code>managed</code> storage area are set by administrators or native applications, and are read-only for the extension; trying to modify this namespace results in an error.",
+        "properties": {
+          "QUOTA_BYTES": {
+            "value": 5242880,
+            "description": "The maximum size (in bytes) of the managed storage JSON manifest file. Files larger than this limit will fail to load."
+          }
+        }
       }
     }
   }
 ]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_managed.js
@@ -0,0 +1,120 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  MockRegistry: "resource://testing-common/MockRegistry.jsm",
+  OS: "resource://gre/modules/osfile.jsm",
+});
+
+const MANIFEST = {
+  name: "test-storage-managed@mozilla.com",
+  description: "",
+  type: "storage",
+  data: {
+    null: null,
+    str: "hello",
+    obj: {
+      a: [2, 3],
+      b: true,
+    },
+  },
+};
+
+add_task(async function setup() {
+  await ExtensionTestUtils.startAddonManager();
+
+  let tmpDir = FileUtils.getDir("TmpD", ["native-manifests"]);
+  tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+  let dirProvider = {
+    getFile(property) {
+      if (property.endsWith("NativeManifests")) {
+        return tmpDir.clone();
+      }
+    },
+  };
+  Services.dirsvc.registerProvider(dirProvider);
+
+  let typeSlug = AppConstants.platform === "linux" ? "managed-storage" : "ManagedStorage";
+  OS.File.makeDir(OS.Path.join(tmpDir.path, typeSlug));
+
+  let path = OS.Path.join(tmpDir.path, typeSlug, `${MANIFEST.name}.json`);
+  await OS.File.writeAtomic(path, JSON.stringify(MANIFEST));
+
+  let registry;
+  if (AppConstants.platform === "win") {
+    registry = new MockRegistry();
+    registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+      `Software\\\Mozilla\\\ManagedStorage\\${MANIFEST.name}`, "", path);
+  }
+
+  do_register_cleanup(() => {
+    Services.dirsvc.unregisterProvider(dirProvider);
+    tmpDir.remove(true);
+    if (registry) {
+      registry.shutdown();
+    }
+  });
+});
+
+add_task(async function test_storage_managed() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: {gecko: {id: MANIFEST.name}},
+      permissions: ["storage"],
+    },
+
+    async background() {
+      await browser.test.assertRejects(
+        browser.storage.managed.set({a: 1}),
+        /storage.managed is read-only/,
+        "browser.storage.managed.set() rejects because it's read only");
+
+      await browser.test.assertRejects(
+        browser.storage.managed.remove("str"),
+        /storage.managed is read-only/,
+        "browser.storage.managed.remove() rejects because it's read only");
+
+      await browser.test.assertRejects(
+        browser.storage.managed.clear(),
+        /storage.managed is read-only/,
+        "browser.storage.managed.clear() rejects because it's read only");
+
+      browser.test.sendMessage("results", await Promise.all([
+        browser.storage.managed.get(),
+        browser.storage.managed.get("str"),
+        browser.storage.managed.get(["null", "obj"]),
+        browser.storage.managed.get({str: "a", num: 2}),
+      ]));
+    },
+  });
+
+  await extension.startup();
+  deepEqual(await extension.awaitMessage("results"), [
+    MANIFEST.data,
+    {str: "hello"},
+    {null: null, obj: MANIFEST.data.obj},
+    {str: "hello", num: 2},
+  ]);
+  await extension.unload();
+});
+
+add_task(async function test_manifest_not_found() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["storage"],
+    },
+
+    async background() {
+      await browser.test.assertRejects(
+        browser.storage.managed.get({a: 1}),
+        /Managed storage manifest not found/,
+        "browser.storage.managed.get() rejects when without manifest");
+
+      browser.test.notifyPass();
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish();
+  await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -56,16 +56,18 @@ skip-if = true # bug 1315829
 [test_ext_runtime_sendMessage_no_receiver.js]
 [test_ext_runtime_sendMessage_self.js]
 [test_ext_shutdown_cleanup.js]
 [test_ext_simple.js]
 [test_ext_startup_cache.js]
 skip-if = os == "android"
 [test_ext_startup_perf.js]
 [test_ext_storage.js]
+[test_ext_storage_managed.js]
+skip-if = os == "android"
 [test_ext_storage_sync.js]
 head = head.js head_sync.js
 skip-if = os == "android"
 [test_ext_storage_sync_crypto.js]
 skip-if = os == "android"
 [test_ext_storage_telemetry.js]
 skip-if = os == "android" # checking for telemetry needs to be updated: 1384923
 [test_ext_topSites.js]