Bug 1287010 - Prepare for moving content script APIs to schemas draft
authorRob Wu <rob@robwu.nl>
Thu, 18 Aug 2016 17:46:57 -0700
changeset 405206 b1894098615b40d7d1d19779abceb92d8cee3622
parent 405205 41489b4af08983c629ed82947783c7c713a2e0e3
child 405207 064288fe3b4a8a727390334e4d5fbaae2192cad0
push id27432
push userbmo:rob@robwu.nl
push dateThu, 25 Aug 2016 02:36:24 +0000
bugs1287010
milestone51.0a1
Bug 1287010 - Prepare for moving content script APIs to schemas - By default, schema APIs are not injected in content scripts unless the JSON schema sets the "restrictions" attribute to `["content"]`. - Added the "restrictions" attribute to the storage and test schemas. Other APIs will follow in subsequent commits and make use of the primitives introduced in this commit. MozReview-Commit-ID: 1rNjQap0BiM
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/schemas/storage.json
toolkit/components/extensions/schemas/test.json
toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas_restrictions.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -70,16 +70,17 @@ Cu.import("resource://gre/modules/Extens
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
+const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
 
 let schemaURLs = new Set();
 
 if (!AppConstants.RELEASE_BUILD) {
   schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
 }
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
@@ -137,16 +138,24 @@ var Management = new class extends Schem
       }
       return Promise.all(promises);
     });
 
     for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
       this.loadScript(value);
     }
 
+    // TODO(robwu): This should be removed when addons can conceptually run in
+    // a separate process. This category should be used for content scripts only,
+    // but since the current content script API implementations (i18n, extension
+    // and runtime) are also needed in adddons, we re-use the category.
+    for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
+      this.loadScript(value);
+    }
+
     this.initialized = promise;
     return this.initialized;
   }
 
   registerSchemaAPI(namespace, envType, getAPI) {
     if (envType == "addon_parent" || envType == "content_parent") {
       super.registerSchemaAPI(namespace, envType, getAPI);
     }
@@ -599,17 +608,22 @@ GlobalManager = {
           promise = findPathInObject(apis, path)[name](...args);
         } catch (e) {
           promise = Promise.reject(e);
         }
 
         return context.wrapPromise(promise || Promise.resolve(), callback);
       },
 
-      shouldInject(namespace, name) {
+      shouldInject(namespace, name, restrictions) {
+        // Do not generate content script APIs, unless explicitly allowed.
+        if (context.envType === "content_parent" &&
+            (!restrictions || !restrictions.includes("content"))) {
+          return false;
+        }
         return findPathInObject(apis, [namespace]) != null;
       },
 
       getProperty(pathObj, path, name) {
         return findPathInObject(apis, path)[name];
       },
 
       setProperty(pathObj, path, name, value) {
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -431,26 +431,27 @@ class ExtensionContext extends BaseConte
 
     this.chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "browser"});
 
     // Sandboxes don't get Xrays for some weird compatibility
     // reason. However, we waive here anyway in case that changes.
     Cu.waiveXrays(this.sandbox).chrome = this.chromeObj;
 
     let incognito = PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow);
-    this.childManager = new ChildAPIManager(this, mm, ["storage", "test"], {
+    let localApis = {};
+    apiManager.generateAPIs(this, localApis);
+    this.childManager = new ChildAPIManager(this, mm, localApis, {
       type: "content_script",
       url,
       incognito,
     });
 
     Schemas.inject(this.chromeObj, this.childManager);
 
     injectAPI(api(this), this.chromeObj);
-    injectAPI(apiManager.generateAPIs(this), this.chromeObj);
 
     // This is an iframe with content script API enabled. (See Bug 1214658 for rationale)
     if (isExtensionPage) {
       Cu.waiveXrays(this.contentWindow).chrome = this.chromeObj;
       Cu.waiveXrays(this.contentWindow).browser = this.chromeObj;
     }
   }
 
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1422,20 +1422,24 @@ function detectLanguage(text) {
 let nextId = 1;
 
 // We create one instance of this class for every extension context
 // that needs to use remote APIs. It uses the message manager to
 // communicate with the ParentAPIManager singleton in
 // Extension.jsm. It handles asynchronous function calls as well as
 // event listeners.
 class ChildAPIManager {
-  constructor(context, messageManager, namespaces, contextData) {
+  constructor(context, messageManager, localApis, contextData) {
     this.context = context;
     this.messageManager = messageManager;
-    this.namespaces = namespaces;
+
+    // The root namespace of all locally implemented APIs. If an extension calls
+    // an API that does not exist in this object, then the implementation is
+    // delegated to the ParentAPIManager.
+    this.localApis = localApis;
 
     let id = String(context.extension.id) + "." + String(context.contextId);
     this.id = id;
 
     let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
     Object.assign(data, contextData);
     messageManager.sendAsyncMessage("API:CreateProxyContext", data);
 
@@ -1480,57 +1484,103 @@ class ChildAPIManager {
     this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
   }
 
   get cloneScope() {
     return this.context.cloneScope;
   }
 
   callFunction(pathObj, path, name, args) {
+    if (pathObj) {
+      return pathObj[name](...args);
+    }
     throw new Error("Not implemented");
   }
 
   callFunctionNoReturn(pathObj, path, name, args) {
+    if (pathObj) {
+      pathObj[name](...args);
+      return;
+    }
     this.messageManager.sendAsyncMessage("API:Call", {
       childId: this.id,
       path, name, args,
     });
   }
 
   callAsyncFunction(pathObj, path, name, args, callback) {
+    if (pathObj) {
+      let promise;
+      try {
+        promise = pathObj[name](...args) || Promise.resolve();
+      } catch (e) {
+        promise = Promise.reject(e);
+      }
+      return this.context.wrapPromise(promise, callback);
+    }
     let callId = nextId++;
     let deferred = PromiseUtils.defer();
     this.callPromises.set(callId, deferred);
 
     this.messageManager.sendAsyncMessage("API:Call", {
       childId: this.id,
       callId,
       path, name, args,
     });
 
     return this.context.wrapPromise(deferred.promise, callback);
   }
 
-  shouldInject(namespace, name) {
-    return this.namespaces.includes(namespace);
+  shouldInject(namespace, name, restrictions) {
+    // Do not generate content script APIs, unless explicitly allowed.
+    if (this.context.envType === "content_child" &&
+        (!restrictions || !restrictions.includes("content"))) {
+      return false;
+    }
+    let pathObj = this.localApis;
+    if (pathObj) {
+      for (let part of namespace.split(".")) {
+        pathObj = pathObj[part];
+        if (!pathObj) {
+          break;
+        }
+      }
+      if (pathObj && (name === null || name in pathObj)) {
+        return pathObj;
+      }
+    }
+
+    // No local API found, defer implementation to the parent.
+    return true;
   }
 
   hasPermission(permission) {
     return this.context.extension.permissions.has(permission);
   }
 
   getProperty(pathObj, path, name) {
+    if (pathObj) {
+      return pathObj[name];
+    }
     throw new Error("Not implemented");
   }
 
   setProperty(pathObj, path, name, value) {
+    if (pathObj) {
+      pathObj[name] = value;
+      return;
+    }
     throw new Error("Not implemented");
   }
 
   addListener(pathObj, path, name, listener, args) {
+    if (pathObj) {
+      pathObj[name].addListener(listener, ...args);
+      return;
+    }
     let ref = path.concat(name).join(".");
     let set;
     if (this.listeners.has(ref)) {
       set = this.listeners.get(ref);
     } else {
       set = new Set();
       this.listeners.set(ref, set);
     }
@@ -1543,29 +1593,36 @@ class ChildAPIManager {
       this.messageManager.sendAsyncMessage("API:AddListener", {
         childId: this.id,
         path, name, args,
       });
     }
   }
 
   removeListener(pathObj, path, name, listener) {
+    if (pathObj) {
+      pathObj[name].removeListener(listener);
+      return;
+    }
     let ref = path.concat(name).join(".");
     let set = this.listeners.get(ref) || new Set();
     set.remove(listener);
 
     if (set.size == 0) {
       this.messageManager.sendAsyncMessage("Extension:RemoveListener", {
         childId: this.id,
         path, name,
       });
     }
   }
 
   hasListener(pathObj, path, name, listener) {
+    if (pathObj) {
+      return pathObj[name].hasListener(listener);
+    }
     let ref = path.concat(name).join(".");
     let set = this.listeners.get(ref) || new Set();
     return set.has(listener);
   }
 }
 
 /**
  * This object loads the ext-*.js scripts that define the extension API.
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -357,19 +357,22 @@ class InjectionContext extends Context {
    *        return pathObj[name];
    *      // else the local API does not exist, so fall back or throw an error.
    *    }
    *
    * @abstract
    * @param {string} namespace The namespace of the API. This may contain dots,
    *     e.g. in the case of "devtools.inspectedWindow".
    * @param {string} [name] The name of the property in the namespace.
+   *     `null` if we are checking whether the namespace should be injected.
+   * @param {Array} restrictions An arbitrary list of restrictions as declared
+   *     by the schema for a given API node.
    * @returns {*} An object with the property `name`, `true` or a falsey value.
    */
-  shouldInject(namespace, name) {
+  shouldInject(namespace, name, restrictions) {
     throw new Error("Not implemented");
   }
 
   /**
    * Calls function `path`.`name` and returns its return value.
    *
    * @abstract
    * @param {*} pathObj See `shouldInject`.
@@ -584,16 +587,23 @@ class Entry {
 
     /**
      * @property {string} [preprocessor]
      * If set to a string value, and a preprocessor of the same is
      * defined in the validation context, it will be applied to this
      * value prior to any normalization.
      */
     this.preprocessor = schema.preprocess || null;
+
+    /**
+     * @property {Array<string>} [restrictions]
+     * A list of restrictions to consider before generating the API.
+     * These are not parsed by the schema, but passed to `shouldInject`.
+     */
+    this.restrictions = schema.restrictions || null;
   }
 
   /**
    * Preprocess the given value with the preprocessor declared in
    * `preprocessor`.
    *
    * @param {*} value
    * @param {Context} context
@@ -1223,18 +1233,18 @@ class SubModuleProperty extends Entry {
   // form of sub-module properties, where "$ref" points to a
   // SubModuleType containing a list of functions and "properties" is
   // a list of additional simple properties.
   //
   // name: Name of the property stuff is being added to.
   // namespaceName: Namespace in which the property lives.
   // reference: Name of the type defining the functions to add to the property.
   // properties: Additional properties to add to the module (unsupported).
-  constructor(name, namespaceName, reference, properties) {
-    super();
+  constructor(schema, name, namespaceName, reference, properties) {
+    super(schema);
     this.name = name;
     this.namespaceName = namespaceName;
     this.reference = reference;
     this.properties = properties;
   }
 
   inject(pathObj, path, name, dest, context) {
     let obj = Cu.createObjectIn(dest, {defineAs: name});
@@ -1248,17 +1258,17 @@ class SubModuleProperty extends Entry {
     }
     if (!type || !(type instanceof SubModuleType)) {
       throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
     }
 
     let functions = type.functions;
     for (let fun of functions) {
       let subpath = path.concat(name);
-      let pathObj = context.shouldInject(subpath.join("."), fun.name);
+      let pathObj = context.shouldInject(subpath.join("."), fun.name, fun.restrictions || ns.defaultRestrictions);
       if (pathObj) {
         pathObj = pathObj === true ? null : pathObj;
         fun.inject(pathObj, subpath, fun.name, obj, context);
       }
     }
 
     // TODO: Inject this.properties.
   }
@@ -1462,28 +1472,30 @@ this.Schemas = {
   // This keeps track of all the schemas that have been loaded so far.
   namespaces: new Map(),
 
   register(namespaceName, symbol, value) {
     let ns = this.namespaces.get(namespaceName);
     if (!ns) {
       ns = new Map();
       ns.permissions = null;
+      ns.restrictions = null;
+      ns.defeaultRestrictions = null;
       this.namespaces.set(namespaceName, ns);
     }
     ns.set(symbol, value);
   },
 
   // FIXME: Bug 1265371 - Refactor normalize and parseType in Schemas.jsm to reduce complexity
   parseType(path, type, extraProperties = []) { // eslint-disable-line complexity
     let allowedProperties = new Set(extraProperties);
 
     // Do some simple validation of our own schemas.
     function checkTypeProperties(...extra) {
-      let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated", "preprocess"]);
+      let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated", "preprocess", "restrictions"]);
       for (let prop of Object.keys(type)) {
         if (!allowedSet.has(prop)) {
           throw new Error(`Internal error: Namespace ${path.join(".")} has invalid type property "${prop}" in type "${type.id || JSON.stringify(type)}"`);
         }
       }
     }
 
     if ("choices" in type) {
@@ -1695,17 +1707,17 @@ this.Schemas = {
     }
 
     targetType.extend(parsed);
   },
 
   loadProperty(namespaceName, name, prop) {
     if ("$ref" in prop) {
       if (!prop.unsupported) {
-        this.register(namespaceName, name, new SubModuleProperty(name, namespaceName, prop.$ref,
+        this.register(namespaceName, name, new SubModuleProperty(prop, name, namespaceName, prop.$ref,
                                                                  prop.properties || {}));
       }
     } else if ("value" in prop) {
       this.register(namespaceName, name, new ValueProperty(prop, name, prop.value));
     } else {
       // We ignore the "optional" attribute on properties since we
       // don't inject anything here anyway.
       let type = this.parseType([namespaceName], prop, ["optional", "writable"]);
@@ -1814,20 +1826,20 @@ this.Schemas = {
         this.loadFunction(name, fun);
       }
 
       let events = namespace.events || [];
       for (let event of events) {
         this.loadEvent(name, event);
       }
 
-      if (namespace.permissions) {
-        let ns = this.namespaces.get(name);
-        ns.permissions = namespace.permissions;
-      }
+      let ns = this.namespaces.get(name);
+      ns.permissions = namespace.permissions || null;
+      ns.restrictions = namespace.restrictions || null;
+      ns.defaultRestrictions = namespace.defaultRestrictions || null;
     }
   },
 
   load(url) {
     if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
       return readJSON(url).then(json => {
         this.schemaJSON.set(url, json);
 
@@ -1863,19 +1875,23 @@ this.Schemas = {
   inject(dest, wrapperFuncs) {
     let context = new InjectionContext(wrapperFuncs);
 
     for (let [namespace, ns] of this.namespaces) {
       if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
         continue;
       }
 
+      if (!wrapperFuncs.shouldInject(namespace, null, ns.restrictions)) {
+        continue;
+      }
+
       let obj = Cu.createObjectIn(dest, {defineAs: namespace});
       for (let [name, entry] of ns) {
-        let pathObj = context.shouldInject(namespace, name);
+        let pathObj = context.shouldInject(namespace, name, entry.restrictions || ns.defaultRestrictions);
         if (pathObj) {
           pathObj = pathObj === true ? null : pathObj;
           entry.inject(pathObj, [namespace], name, obj, context);
         }
       }
 
       // Remove the namespace object if it is empty
       if (!Object.keys(obj).length) {
--- a/toolkit/components/extensions/schemas/storage.json
+++ b/toolkit/components/extensions/schemas/storage.json
@@ -1,15 +1,17 @@
 // Copyright 2014 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
     "namespace": "storage",
+    "restrictions": ["content"],
+    "defaultRestrictions": ["content"],
     "description": "Use the <code>browser.storage</code> API to store, retrieve, and track changes to user data.",
     "permissions": ["storage"],
     "types": [
       {
         "id": "StorageChange",
         "type": "object",
         "properties": {
           "oldValue": {
--- a/toolkit/components/extensions/schemas/test.json
+++ b/toolkit/components/extensions/schemas/test.json
@@ -1,15 +1,17 @@
 // Copyright 2014 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
     "namespace": "test",
+    "restrictions": ["content"],
+    "defaultRestrictions": ["content"],
     "description": "none",
     "functions": [
       {
         "name": "notifyFail",
         "type": "function",
         "description": "Notifies the browser process that test code running in the extension failed.  This is only used for internal unit testing.",
         "parameters": [
           {"type": "string", "name": "message"}
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
@@ -88,16 +88,19 @@ add_task(function* testPathObj() {
   let submoduleApi = {
     _count: 0,
     retObj() {
       return "submoduleApi value " + (++submoduleApi._count);
     },
   };
   let localWrapper = {
     shouldInject(ns, name) {
+      if (name === null) {
+        return true;  // Accept any namespace. Properties are checked below.
+      }
       if (ns == "ret_obj") {
         return localApi;
       } else if (ns == "dot.obj") {
         return dotApi;
       } else if (ns == "ret_true" || ns == "ret_true_noval") {
         return true;
       } else if (ns == "ret_false" || ns == "ret_false_noval") {
         return false;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_restrictions.js
@@ -0,0 +1,125 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+let schemaJson = [
+  {
+    namespace: "noRestrict",
+    properties: {
+      prop1: {type: "object"},
+      prop2: {type: "object", restrictions: ["test_zero", "test_one"]},
+      prop3: {type: "number", value: 1},
+      prop4: {type: "number", value: 1, restrictions: ["numeric_one"]},
+    },
+  },
+  {
+    namespace: "defaultRestrict",
+    defaultRestrictions: ["test_two"],
+    properties: {
+      prop1: {type: "object"},
+      prop2: {type: "object", restrictions: ["test_three"]},
+      prop3: {type: "number", value: 1},
+      prop4: {type: "number", value: 1, restrictions: ["numeric_two"]},
+    },
+  },
+  {
+    namespace: "withRestrict",
+    restrictions: ["test_four"],
+    properties: {
+      prop1: {type: "object"},
+      prop2: {type: "object", restrictions: ["test_five"]},
+      prop3: {type: "number", value: 1},
+      prop4: {type: "number", value: 1, restrictions: ["numeric_three"]},
+    },
+  },
+  {
+    namespace: "withRestrictAndDefault",
+    restrictions: ["test_six"],
+    defaultRestrictions: ["test_seven"],
+    properties: {
+      prop1: {type: "object"},
+      prop2: {type: "object", restrictions: ["test_eight"]},
+      prop3: {type: "number", value: 1},
+      prop4: {type: "number", value: 1, restrictions: ["numeric_four"]},
+    },
+  },
+  {
+    namespace: "with_submodule",
+    defaultRestrictions: ["test_nine"],
+    types: [{
+      id: "subtype",
+      type: "object",
+      functions: [{
+        name: "restrictNo",
+        type: "function",
+        parameters: [],
+      }, {
+        name: "restrictYes",
+        restrictions: ["test_ten"],
+        type: "function",
+        parameters: [],
+      }],
+    }],
+    properties: {
+      prop1: {$ref: "subtype"},
+      prop2: {$ref: "subtype", restrictions: ["test_eleven"]},
+    },
+  },
+];
+add_task(function* testPathObj() {
+  let url = "data:," + JSON.stringify(schemaJson);
+  yield Schemas.load(url);
+  let results = {};
+  let localWrapper = {
+    shouldInject(ns, name, restrictions) {
+      restrictions = restrictions ? restrictions.join() : "none";
+      name = name === null ? ns : ns + "." + name;
+      results[name] = restrictions;
+      return true;
+    },
+  };
+
+  let root = {};
+  Schemas.inject(root, localWrapper);
+
+  function verify(path, expected) {
+    let result = results[path];
+    do_check_eq(result, expected);
+  }
+
+  verify("noRestrict", "none");
+  verify("noRestrict.prop1", "none");
+  verify("noRestrict.prop2", "test_zero,test_one");
+  verify("noRestrict.prop3", "none");
+  verify("noRestrict.prop4", "numeric_one");
+
+  verify("defaultRestrict", "none");
+  verify("defaultRestrict.prop1", "test_two");
+  verify("defaultRestrict.prop2", "test_three");
+  verify("defaultRestrict.prop3", "test_two");
+  verify("defaultRestrict.prop4", "numeric_two");
+
+  verify("withRestrict", "test_four");
+  verify("withRestrict.prop1", "none");
+  verify("withRestrict.prop2", "test_five");
+  verify("withRestrict.prop3", "none");
+  verify("withRestrict.prop4", "numeric_three");
+
+  verify("withRestrictAndDefault", "test_six");
+  verify("withRestrictAndDefault.prop1", "test_seven");
+  verify("withRestrictAndDefault.prop2", "test_eight");
+  verify("withRestrictAndDefault.prop3", "test_seven");
+  verify("withRestrictAndDefault.prop4", "numeric_four");
+
+  verify("with_submodule", "none");
+  verify("with_submodule.prop1", "test_nine");
+  verify("with_submodule.prop1.restrictNo", "test_nine");
+  verify("with_submodule.prop1.restrictYes", "test_ten");
+  verify("with_submodule.prop2", "test_eleven");
+  // Note: test_nine inherits restrictions from the namespace, not from
+  // submodule. There is no "defaultRestrictions" for submodule types to not
+  // complicate things.
+  verify("with_submodule.prop1.restrictNo", "test_nine");
+  verify("with_submodule.prop1.restrictYes", "test_ten");
+});
+
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -40,15 +40,16 @@ skip-if = release_build
 [test_ext_runtime_connect_no_receiver.js]
 [test_ext_runtime_getPlatformInfo.js]
 [test_ext_runtime_sendMessage.js]
 [test_ext_runtime_sendMessage_errors.js]
 [test_ext_runtime_sendMessage_no_receiver.js]
 [test_ext_schemas.js]
 [test_ext_schemas_api_injection.js]
 [test_ext_schemas_pathObj.js]
+[test_ext_schemas_restrictions.js]
 [test_ext_simple.js]
 [test_ext_storage.js]
 [test_getAPILevelForWindow.js]
 [test_locale_converter.js]
 [test_locale_data.js]
 [test_native_messaging.js]
 skip-if = os == "android"