Bug 1386427 - Part 4: Implement basic storage.managed functionality draft
authorTomislav Jovanovic <tomica@gmail.com>
Sat, 16 Sep 2017 19:42:40 +0200
changeset 667985 244b473fd75d9a5bbf13ae0d01a549193cd61ed6
parent 667984 fb75d92336b18d0ffe97659a0da893dddabf3382
child 732572 b59c314ee6b85d49d928fd567a2f52ecd9b159da
push id80910
push userbmo:tomica@gmail.com
push dateThu, 21 Sep 2017 00:19:34 +0000
bugs1386427
milestone57.0a1
Bug 1386427 - Part 4: Implement basic storage.managed functionality 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
@@ -55,16 +55,18 @@ skip-if = true # This test no longer tes
 [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]