Bug 1498343 - Make it easier to recognize an extension that have access to the userScript.setScriptAPIs API method. r=mixedpuppy
authorLuca Greco <lgreco@mozilla.com>
Wed, 17 Oct 2018 12:39:20 +0000
changeset 490508 681ffcabae79527e14f5f795aaf360eeb2e85399
parent 490507 a0aa0b0aa078f7135e7d7032687e4a38e6c378f8
child 490509 fb372f5f22b3a8813b3c687e5a8d4ce3177fbdea
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersmixedpuppy
bugs1498343
milestone64.0a1
Bug 1498343 - Make it easier to recognize an extension that have access to the userScript.setScriptAPIs API method. r=mixedpuppy Depends on D8538 Differential Revision: https://phabricator.services.mozilla.com/D8539
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/schemas/user_scripts_content.json
toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js
toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -854,19 +854,30 @@ class ExtensionData {
 
     this.webAccessibleResources = manifestData.webAccessibleResources.map(res => new MatchGlob(res));
     this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions, {restrictSchemes: !this.hasPermission("mozillaAddons")});
 
     return this.manifest;
   }
 
   hasPermission(perm, includeOptional = false) {
+    // If the permission is a "manifest property" permission, we check if the extension
+    // does have the required property in its manifest.
     let manifest_ = "manifest:";
     if (perm.startsWith(manifest_)) {
-      return this.manifest[perm.substr(manifest_.length)] != null;
+      // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested").
+      let value = this.manifest;
+      for (let prop of perm.substr(manifest_.length).split(".")) {
+        if (!value) {
+          break;
+        }
+        value = value[prop];
+      }
+
+      return value != null;
     }
 
     if (this.permissions.has(perm)) {
       return true;
     }
 
     if (includeOptional && this.manifest.optional_permissions.includes(perm)) {
       return true;
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -768,19 +768,30 @@ class BrowserExtensionContent extends Ev
     return this.localeData.localizeMessage(...args);
   }
 
   localize(...args) {
     return this.localeData.localize(...args);
   }
 
   hasPermission(perm) {
-    let match = /^manifest:(.*)/.exec(perm);
-    if (match) {
-      return this.manifest[match[1]] != null;
+    // If the permission is a "manifest property" permission, we check if the extension
+    // does have the required property in its manifest.
+    let manifest_ = "manifest:";
+    if (perm.startsWith(manifest_)) {
+      // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested").
+      let value = this.manifest;
+      for (let prop of perm.substr(manifest_.length).split(".")) {
+        if (!value) {
+          break;
+        }
+        value = value[prop];
+      }
+
+      return value != null;
     }
     return this.permissions.has(perm);
   }
 }
 
 /**
  * An object that runs an remote implementation of an API.
  */
--- a/toolkit/components/extensions/schemas/user_scripts_content.json
+++ b/toolkit/components/extensions/schemas/user_scripts_content.json
@@ -13,16 +13,17 @@
         "type": "object",
         "description": "A set of API methods provided by the extensions to its userScripts",
         "additionalProperties": { "type": "function" }
       }
     ],
     "functions": [
       {
         "name": "setScriptAPIs",
+        "permissions": ["manifest:user_scripts.api_script"],
         "allowedContexts": ["content", "content_only"],
         "type": "function",
         "description": "Provides a set of custom API methods available to the registered userScripts",
         "parameters": [
           {
             "name": "exportedAPIMethods",
             "$ref": "ExportedAPIMethods"
           }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js
@@ -0,0 +1,128 @@
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+const {ExtensionAPI} = ExtensionCommon;
+
+Cu.importGlobalProperties(["Blob", "URL"]);
+
+add_task(async function() {
+  const schema = [
+    {
+      namespace: "manifest",
+      types: [
+        {
+          "$extend": "WebExtensionManifest",
+          properties: {
+            a_manifest_property: {
+              type: "object",
+              optional: true,
+              properties: {
+                nested: {
+                  optional: true,
+                  type: "any",
+                },
+              },
+              additionalProperties: {"$ref": "UnrecognizedProperty"},
+            },
+          },
+        },
+      ],
+    },
+    {
+      namespace: "testManifestPermission",
+      permissions: ["manifest:a_manifest_property"],
+      functions: [
+        {
+          name: "testMethod",
+          type: "function",
+          async: true,
+          parameters: [],
+          permissions: ["manifest:a_manifest_property.nested"],
+        },
+      ],
+    },
+  ];
+
+  class FakeAPI extends ExtensionAPI {
+    getAPI(context) {
+      return {
+        testManifestPermission: {
+          get testProperty() {
+            return "value";
+          },
+          testMethod() {
+            return Promise.resolve("value");
+          },
+        },
+      };
+    }
+  }
+
+  const modules = {
+    testNamespace: {
+      url: URL.createObjectURL(new Blob([FakeAPI.toString()])),
+      schema: `data:,${JSON.stringify(schema)}`,
+      scopes: ["addon_parent", "addon_child"],
+      paths: [["testManifestPermission"]],
+    },
+  };
+
+  Services.catMan.addCategoryEntry("webextension-modules", "test-manifest-permission",
+                                   `data:,${JSON.stringify(modules)}`, false, false);
+
+  async function testExtension(extensionDef, assertFn) {
+    let extension = ExtensionTestUtils.loadExtension(extensionDef);
+
+    await extension.startup();
+    await assertFn(extension);
+    await extension.unload();
+  }
+
+  await testExtension({
+    manifest: {
+      a_manifest_property: {},
+    },
+    background() {
+      // Test hasPermission method implemented in ExtensionChild.jsm.
+      browser.test.assertTrue("testManifestPermission" in browser, "The API namespace is defined as expected");
+      browser.test.assertEq(
+        undefined, browser.testManifestPermission && browser.testManifestPermission.testMethod,
+        "The property with nested manifest property permission should not be available ");
+      browser.test.notifyPass("test-extension-manifest-without-nested-prop");
+    },
+  }, async (extension) => {
+    await extension.awaitFinish("test-extension-manifest-without-nested-prop");
+
+    // Test hasPermission method implemented in Extension.jsm.
+    equal(extension.extension.hasPermission("manifest:a_manifest_property"), true,
+          "Got the expected Extension's hasPermission result on existing property");
+    equal(extension.extension.hasPermission("manifest:a_manifest_property.nested"), false,
+          "Got the expected Extension's hasPermission result on existing subproperty");
+  });
+
+  await testExtension({
+    manifest: {
+      a_manifest_property: {
+        nested: {},
+      },
+    },
+    background() {
+      // Test hasPermission method implemented in ExtensionChild.jsm.
+      browser.test.assertTrue("testManifestPermission" in browser, "The API namespace is defined as expected");
+      browser.test.assertEq(
+        "function", browser.testManifestPermission && typeof browser.testManifestPermission.testMethod,
+        "The property with nested manifest property permission should be available ");
+      browser.test.notifyPass("test-extension-manifest-with-nested-prop");
+    },
+  }, async (extension) => {
+    await extension.awaitFinish("test-extension-manifest-with-nested-prop");
+
+    // Test hasPermission method implemented in Extension.jsm.
+    equal(extension.extension.hasPermission("manifest:a_manifest_property"), true,
+          "Got the expected Extension's hasPermission result on existing property");
+    equal(extension.extension.hasPermission("manifest:a_manifest_property.nested"), true,
+          "Got the expected Extension's hasPermission result on existing subproperty");
+    equal(extension.extension.hasPermission("manifest:a_manifest_property.unexisting"), false,
+          "Got the expected Extension's hasPermission result on non existing subproperty");
+  });
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
@@ -594,17 +594,17 @@ add_task(async function test_userScripts
 
       browser.test.sendMessage("content-script:done");
     }
 
     let extension = ExtensionTestUtils.loadExtension({
       background,
       manifest: {
         permissions: ["http://*/*/file_sample.html"],
-        user_scripts: {},
+        user_scripts: {api_script: ""},
         content_scripts: [
           {
             matches:  ["http://*/*/file_sample.html"],
             js: ["content_script.js"],
             run_at: "document_start",
           },
         ],
       },
@@ -625,16 +625,50 @@ add_task(async function test_userScripts
     await extension.unload();
     await contentPage.close();
   }
 
   await runWithPrefs([["extensions.webextensions.userScripts.enabled", false]],
                      run_userScript_on_pref_disabled_test);
 });
 
+// This test verify that userScripts.setScriptAPIs is not available without
+// a "user_scripts.api_script" property in the manifest.
+add_task(async function test_user_script_api_script_required() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      content_scripts: [
+        {
+          matches: ["http://localhost/*/file_sample.html"],
+          js: ["content_script.js"],
+          run_at: "document_start",
+        },
+      ],
+      user_scripts: {},
+    },
+    files: {
+      "content_script.js": function() {
+        browser.test.assertEq(undefined, browser.userScripts && browser.userScripts.setScriptAPIs,
+                              "Got an undefined setScriptAPIs as expected");
+        browser.test.sendMessage("no-setScriptAPIs:done");
+      },
+    },
+  });
+
+  await extension.startup();
+
+  let url = `${BASE_URL}/file_sample.html`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(url);
+
+  await extension.awaitMessage("no-setScriptAPIs:done");
+
+  await extension.unload();
+  await contentPage.close();
+});
+
 add_task(async function test_scriptMetaData() {
   function getTestCases(isUserScriptsRegister) {
     return [
       // When scriptMetadata is not set (or undefined), it is treated as if it were null.
       // In the API script, the metadata is then expected to be null.
       isUserScriptsRegister ? undefined : null,
 
       // Falsey
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -38,16 +38,17 @@ tags = webextensions in-process-webexten
 [test_ext_manifest_minimum_chrome_version.js]
 [test_ext_manifest_minimum_opera_version.js]
 [test_ext_manifest_themes.js]
 [test_ext_schemas.js]
 [test_ext_schemas_roots.js]
 [test_ext_schemas_async.js]
 [test_ext_schemas_allowed_contexts.js]
 [test_ext_schemas_interactive.js]
+[test_ext_schemas_manifest_permissions.js]
 [test_ext_schemas_privileged.js]
 [test_ext_schemas_revoke.js]
 [test_ext_unknown_permissions.js]
 [test_locale_converter.js]
 [test_locale_data.js]
 [test_ext_ipcBlob.js]
 
 [test_ext_runtime_sendMessage_args.js]